Regenerate v1.d.ts from the updated OpenAPI spec and add createPlayout()/ updatePlayoutDetails() to the playouts API client, with URL/body-assert tests following the ffmpegProfiles.test.ts pattern.
339 lines
10 KiB
TypeScript
339 lines
10 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { ApiError, request } from './client';
|
|
import type { components } from './generated/v1';
|
|
|
|
export type PlayoutSummary = components['schemas']['PlayoutListItemResponseModel'];
|
|
export type PlayoutDetail = components['schemas']['PlayoutResponseModel'];
|
|
export type PlayoutItem = components['schemas']['PlayoutItemResponseModel'];
|
|
export type PlayoutsPage = components['schemas']['PagedPlayoutsResponseModel'];
|
|
export type PlayoutItemsPage = components['schemas']['PagedPlayoutItemsResponseModel'];
|
|
export type PlayoutChannelState = components['schemas']['ChannelStateResponseModel'];
|
|
export type PlayoutScheduleKind = components['schemas']['PlayoutScheduleKind'];
|
|
export type CreatePlayoutRequest = components['schemas']['CreatePlayoutRequest'];
|
|
export type UpdatePlayoutDetailsRequest = components['schemas']['UpdatePlayoutDetailsRequest'];
|
|
|
|
export interface PlayoutsScreenData {
|
|
channelStates: PlayoutChannelState[];
|
|
items: PlayoutItem[];
|
|
playout: PlayoutDetail | null;
|
|
playouts: PlayoutSummary[];
|
|
selectedPlayoutId: number | null;
|
|
totalCount: number;
|
|
warningsCount: number;
|
|
}
|
|
|
|
type PlayoutsScreenBase = Omit<PlayoutsScreenData, 'items' | 'playout'>;
|
|
|
|
export type PlayoutsScreenQueryState =
|
|
| {
|
|
data: PlayoutsScreenData;
|
|
error: null;
|
|
itemsLoading: boolean;
|
|
refresh: () => void;
|
|
setActivePlayout: (playoutId: number) => void;
|
|
setShowFiller: (showFiller: boolean) => void;
|
|
showFiller: boolean;
|
|
status: 'success';
|
|
}
|
|
| { data: null; error: string; refresh: () => void; status: 'error' }
|
|
| { data: null; error: null; refresh: () => void; status: 'loading' };
|
|
|
|
type PlayoutsScreenState =
|
|
| { data: PlayoutsScreenData; error: null; itemsLoading: boolean; status: 'success' }
|
|
| { data: null; error: string; status: 'error' }
|
|
| { data: null; error: null; status: 'loading' };
|
|
|
|
export function getPlayouts(): Promise<PlayoutsPage> {
|
|
return request<PlayoutsPage>('/api/playouts');
|
|
}
|
|
|
|
export function getPlayout(playoutId: number): Promise<PlayoutDetail> {
|
|
return request<PlayoutDetail>(`/api/playouts/${playoutId}`);
|
|
}
|
|
|
|
export function getPlayoutItems(playoutId: number, showFiller = false): Promise<PlayoutItemsPage> {
|
|
return request<PlayoutItemsPage>(`/api/playouts/${playoutId}/items${showFiller ? '?showFiller=true' : ''}`);
|
|
}
|
|
|
|
export function getPlayoutWarningsCount(): Promise<number> {
|
|
return request<number>('/api/playouts/warnings/count');
|
|
}
|
|
|
|
export function getPlayoutChannelStates(): Promise<PlayoutChannelState[]> {
|
|
return request<PlayoutChannelState[]>('/api/channels/state');
|
|
}
|
|
|
|
export function resetAllPlayouts(): Promise<void> {
|
|
return request<void>('/api/playouts/reset-all', { method: 'POST' });
|
|
}
|
|
|
|
export function createPlayout(body: CreatePlayoutRequest): Promise<PlayoutDetail> {
|
|
return request<PlayoutDetail>('/api/playouts', {
|
|
body,
|
|
method: 'POST'
|
|
});
|
|
}
|
|
|
|
export function updatePlayoutDetails(playoutId: number, body: UpdatePlayoutDetailsRequest): Promise<PlayoutDetail> {
|
|
return request<PlayoutDetail>(`/api/playouts/${playoutId}`, {
|
|
body,
|
|
method: 'PUT'
|
|
});
|
|
}
|
|
|
|
export function usePlayoutsScreenQuery(pollMs = 30000): PlayoutsScreenQueryState {
|
|
const [state, setState] = useState<PlayoutsScreenState>({
|
|
data: null,
|
|
error: null,
|
|
status: 'loading'
|
|
});
|
|
const [showFiller, setShowFillerState] = useState(false);
|
|
const activeRef = useRef(true);
|
|
const selectedPlayoutIdRef = useRef<number | null>(null);
|
|
const showFillerRef = useRef(false);
|
|
// Snapshot of the last-known base fields (everything but the selected playout's
|
|
// detail/items), kept in sync whenever load()/loadChannelStates() succeed. Reading
|
|
// this lets setActivePlayout/setShowFiller kick off their fetch without doing the
|
|
// fetch from inside a setState updater (impure; StrictMode double-invokes updaters).
|
|
const baseRef = useRef<PlayoutsScreenBase>({
|
|
channelStates: [],
|
|
playouts: [],
|
|
selectedPlayoutId: null,
|
|
totalCount: 0,
|
|
warningsCount: 0
|
|
});
|
|
|
|
useEffect(() => {
|
|
activeRef.current = true;
|
|
|
|
return () => {
|
|
activeRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
const loadSelectedPlayout = useCallback((playoutId: number, base: PlayoutsScreenBase) => {
|
|
baseRef.current = base;
|
|
|
|
Promise.all([getPlayout(playoutId), getPlayoutItems(playoutId, showFillerRef.current)])
|
|
.then(([playout, itemsPage]) => {
|
|
if (!activeRef.current || selectedPlayoutIdRef.current !== playoutId) {
|
|
return;
|
|
}
|
|
|
|
setState((current) => {
|
|
// Merge onto whatever channel state the background poll may have landed
|
|
// while this request was in flight, rather than clobbering it with the
|
|
// snapshot captured when the request started.
|
|
const channelStates = current.status === 'success' ? current.data.channelStates : base.channelStates;
|
|
|
|
return {
|
|
data: {
|
|
...base,
|
|
channelStates,
|
|
items: itemsPage.page ?? [],
|
|
playout,
|
|
selectedPlayoutId: playoutId
|
|
},
|
|
error: null,
|
|
itemsLoading: false,
|
|
status: 'success'
|
|
};
|
|
});
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (activeRef.current && selectedPlayoutIdRef.current === playoutId) {
|
|
setState({ data: null, error: messageFromPlayoutError(error), status: 'error' });
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
const load = useCallback(() => {
|
|
Promise.all([getPlayouts(), getPlayoutWarningsCount(), getPlayoutChannelStates()])
|
|
.then(([playoutsPage, warningsCount, channelStates]) => {
|
|
if (!activeRef.current) {
|
|
return;
|
|
}
|
|
|
|
const playouts = playoutsPage.page ?? [];
|
|
const selectedPlayoutId = selectedPlayoutIdRef.current && playouts.some((playout) => playout.id === selectedPlayoutIdRef.current)
|
|
? selectedPlayoutIdRef.current
|
|
: playouts[0]?.id ?? null;
|
|
selectedPlayoutIdRef.current = selectedPlayoutId;
|
|
|
|
const base: PlayoutsScreenBase = {
|
|
channelStates,
|
|
playouts,
|
|
selectedPlayoutId,
|
|
totalCount: playoutsPage.totalCount,
|
|
warningsCount
|
|
};
|
|
baseRef.current = base;
|
|
|
|
if (selectedPlayoutId == null) {
|
|
setState({
|
|
data: {
|
|
...base,
|
|
items: [],
|
|
playout: null
|
|
},
|
|
error: null,
|
|
itemsLoading: false,
|
|
status: 'success'
|
|
});
|
|
return;
|
|
}
|
|
|
|
loadSelectedPlayout(selectedPlayoutId, base);
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (activeRef.current) {
|
|
setState({ data: null, error: messageFromPlayoutError(error), status: 'error' });
|
|
}
|
|
});
|
|
}, [loadSelectedPlayout]);
|
|
|
|
const loadChannelStates = useCallback(() => {
|
|
getPlayoutChannelStates()
|
|
.then((channelStates) => {
|
|
if (!activeRef.current) {
|
|
return;
|
|
}
|
|
|
|
baseRef.current = { ...baseRef.current, channelStates };
|
|
|
|
setState((current) => {
|
|
if (current.status !== 'success') {
|
|
return current;
|
|
}
|
|
|
|
return {
|
|
data: { ...current.data, channelStates },
|
|
error: null,
|
|
itemsLoading: current.itemsLoading,
|
|
status: 'success'
|
|
};
|
|
});
|
|
})
|
|
.catch(() => {
|
|
// Keep the monitor visible on background polling failures.
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
|
|
const intervalId = window.setInterval(loadChannelStates, pollMs);
|
|
|
|
return () => {
|
|
window.clearInterval(intervalId);
|
|
};
|
|
}, [load, loadChannelStates, pollMs]);
|
|
|
|
const refresh = useCallback(() => {
|
|
setState({ data: null, error: null, status: 'loading' });
|
|
load();
|
|
}, [load]);
|
|
|
|
const setActivePlayout = useCallback((playoutId: number) => {
|
|
selectedPlayoutIdRef.current = playoutId;
|
|
|
|
setState((current) => {
|
|
if (current.status !== 'success') {
|
|
return current;
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
...current.data,
|
|
items: [],
|
|
playout: null,
|
|
selectedPlayoutId: playoutId
|
|
},
|
|
error: null,
|
|
itemsLoading: true,
|
|
status: 'success'
|
|
};
|
|
});
|
|
|
|
const base: PlayoutsScreenBase = { ...baseRef.current, selectedPlayoutId: playoutId };
|
|
loadSelectedPlayout(playoutId, base);
|
|
}, [loadSelectedPlayout]);
|
|
|
|
const setShowFiller = useCallback((next: boolean) => {
|
|
showFillerRef.current = next;
|
|
setShowFillerState(next);
|
|
|
|
const playoutId = selectedPlayoutIdRef.current;
|
|
|
|
if (playoutId == null) {
|
|
return;
|
|
}
|
|
|
|
setState((current) => {
|
|
if (current.status !== 'success') {
|
|
return current;
|
|
}
|
|
|
|
return { ...current, itemsLoading: true };
|
|
});
|
|
|
|
getPlayoutItems(playoutId, next)
|
|
.then((itemsPage) => {
|
|
if (!activeRef.current || selectedPlayoutIdRef.current !== playoutId) {
|
|
return;
|
|
}
|
|
|
|
setState((current) => {
|
|
if (current.status !== 'success') {
|
|
return current;
|
|
}
|
|
|
|
return {
|
|
data: { ...current.data, items: itemsPage.page ?? [] },
|
|
error: null,
|
|
itemsLoading: false,
|
|
status: 'success'
|
|
};
|
|
});
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!activeRef.current || selectedPlayoutIdRef.current !== playoutId) {
|
|
return;
|
|
}
|
|
|
|
setState({ data: null, error: messageFromPlayoutError(error), status: 'error' });
|
|
});
|
|
}, []);
|
|
|
|
if (state.status === 'success') {
|
|
return {
|
|
data: state.data,
|
|
error: null,
|
|
itemsLoading: state.itemsLoading,
|
|
refresh,
|
|
setActivePlayout,
|
|
setShowFiller,
|
|
showFiller,
|
|
status: 'success'
|
|
};
|
|
}
|
|
|
|
if (state.status === 'error') {
|
|
return { data: null, error: state.error, refresh, status: 'error' };
|
|
}
|
|
|
|
return { data: null, error: null, refresh, status: 'loading' };
|
|
}
|
|
|
|
function messageFromPlayoutError(error: unknown, fallback = 'Unable to load playouts'): string {
|
|
if (error instanceof ApiError) {
|
|
return error.detail ?? error.message;
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
|
|
return fallback;
|
|
}
|