From 0c0831c66cca3e6df115e3cdc2edecad79af225f Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 16 Oct 2024 07:17:34 +0200 Subject: [PATCH] feat: added possibility to add, change and remove multiviewers on running production --- src/api/manager/workflow.ts | 56 +++++-- .../[id]/route.ts | 4 +- .../multiviewersOnRunningProduction/route.ts | 2 +- src/app/production/[id]/page.tsx | 36 ++++- src/components/modal/Modal.tsx | 25 +-- .../modal/UpdateMultiviewersModal.tsx | 37 +++++ .../ConfigureMultiviewModal.tsx | 20 ++- .../MultiviewSettings.tsx | 7 +- .../startProduction/StartProductionButton.tsx | 8 +- src/hooks/workflow.ts | 147 ++++++++++-------- src/i18n/locales/en.ts | 5 +- src/i18n/locales/sv.ts | 5 +- src/interfaces/preset.ts | 2 +- 13 files changed, 247 insertions(+), 107 deletions(-) create mode 100644 src/components/modal/UpdateMultiviewersModal.tsx diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index e9f7973..88902e7 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -811,7 +811,7 @@ export async function startProduction( { step: 'pipeline_outputs', success: false }, { step: 'multiviews', success: false } ], - error: 'Unknown error occured' + error: 'Could not start multiviews' }; } return { @@ -951,7 +951,8 @@ export async function postMultiviewersOnRunningProduction( additions: MultiviewSettings[] ) { try { - if (!production.production_settings.pipelines[0].multiviews) { + const multiview = production.production_settings.pipelines[0].multiviews; + if (!multiview) { Log().error( `No multiview settings specified for production: ${production.name}` ); @@ -979,11 +980,33 @@ export async function postMultiviewersOnRunningProduction( throw `Failed to create multiview for pipeline '${productionSettings.pipelines[0].pipeline_name}/${productionSettings.pipelines[0].pipeline_id}': ${error}`; }); - runtimeMultiviews.flatMap((runtimeMultiview, index) => { - const multiview = production.production_settings.pipelines[0].multiviews; - if (multiview && multiview[index]) { - return (multiview[index].multiview_id = runtimeMultiview.id); + const multiviewsWithUpdatedId: MultiviewSettings[] = [ + ...multiview.slice(0, multiview.length - runtimeMultiviews.length), + ...runtimeMultiviews.map((runtimeMultiview, index) => { + return { + ...multiview[multiview.length - runtimeMultiviews.length + index], + multiview_id: runtimeMultiview.id + }; + }) + ]; + + await putProduction(production._id.toString(), { + ...production, + production_settings: { + ...production.production_settings, + pipelines: production.production_settings.pipelines.map((pipeline) => { + return { + ...pipeline, + multiviews: multiviewsWithUpdatedId + }; + }) } + }).catch(async (error) => { + Log().error( + `Failed to save multiviews for pipeline '${productionSettings.pipelines[0].pipeline_name}/${productionSettings.pipelines[0].pipeline_id}' to database`, + error + ); + throw error; }); return { @@ -999,7 +1022,7 @@ export async function postMultiviewersOnRunningProduction( } }; } catch (e) { - Log().error('Could not start multiviews'); + Log().error('Could not create multiviews'); Log().error(e); if (typeof e !== 'string') { return { @@ -1013,7 +1036,7 @@ export async function postMultiviewersOnRunningProduction( } ] }, - error: 'Unknown error occured' + error: 'Could not create multiviews' }; } return { @@ -1023,7 +1046,8 @@ export async function postMultiviewersOnRunningProduction( steps: [ { step: 'create_multiview', - success: false + success: false, + message: e } ] }, @@ -1065,7 +1089,7 @@ export async function putMultiviewersOnRunningProduction( } }; } catch (e) { - Log().error('Could not start multiviews'); + Log().error('Could not update multiviews'); Log().error(e); if (typeof e !== 'string') { return { @@ -1079,7 +1103,7 @@ export async function putMultiviewersOnRunningProduction( } ] }, - error: 'Unknown error occured' + error: 'Could not update multiviews' }; } return { @@ -1089,7 +1113,8 @@ export async function putMultiviewersOnRunningProduction( steps: [ { step: 'update_multiview', - success: false + success: false, + message: e } ] }, @@ -1149,7 +1174,7 @@ export async function deleteMultiviewersOnRunningProduction( } }; } catch (e) { - Log().error('Could not start multiviews'); + Log().error('Could not delete multiviews'); Log().error(e); if (typeof e !== 'string') { return { @@ -1163,7 +1188,7 @@ export async function deleteMultiviewersOnRunningProduction( } ] }, - error: 'Unknown error occured' + error: 'Could not delete multiviews' }; } return { @@ -1173,7 +1198,8 @@ export async function deleteMultiviewersOnRunningProduction( steps: [ { step: 'delete_multiview', - success: false + success: false, + message: e } ] }, diff --git a/src/app/api/manager/multiviewersOnRunningProduction/[id]/route.ts b/src/app/api/manager/multiviewersOnRunningProduction/[id]/route.ts index 0ea9844..3869fab 100644 --- a/src/app/api/manager/multiviewersOnRunningProduction/[id]/route.ts +++ b/src/app/api/manager/multiviewersOnRunningProduction/[id]/route.ts @@ -22,7 +22,7 @@ export async function PUT(request: NextRequest): Promise { Log().error(error); const errorResponse = { ok: false, - error: 'unexpected' + error: 'Could not update multiviewers' }; return new NextResponse(JSON.stringify(errorResponse), { status: 500 }); }); @@ -44,7 +44,7 @@ export async function DELETE(request: NextRequest): Promise { Log().error(error); const errorResponse = { ok: false, - error: 'unexpected' + error: 'Could not remove multiviewers' }; return new NextResponse(JSON.stringify(errorResponse), { status: 500 }); }); diff --git a/src/app/api/manager/multiviewersOnRunningProduction/route.ts b/src/app/api/manager/multiviewersOnRunningProduction/route.ts index e4ee05c..cbc0496 100644 --- a/src/app/api/manager/multiviewersOnRunningProduction/route.ts +++ b/src/app/api/manager/multiviewersOnRunningProduction/route.ts @@ -19,7 +19,7 @@ export async function POST(request: NextRequest): Promise { Log().error(error); const errorResponse = { ok: false, - error: 'unexpected' + error: 'Could not add multiviewers' }; return new NextResponse(JSON.stringify(errorResponse), { status: 500 }); }); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 884eeff..3c102e9 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -53,7 +53,11 @@ import { ConfigureMultiviewButton } from '../../../components/modal/configureMul import { useUpdateSourceInputSlotOnMultiviewLayouts } from '../../../hooks/useUpdateSourceInputSlotOnMultiviewLayouts'; import { useCheckProductionPipelines } from '../../../hooks/useCheckProductionPipelines'; import cloneDeep from 'lodash.clonedeep'; -import { useUpdateMultiviewersOnRunningProduction } from '../../../hooks/workflow'; +import { + useAddMultiviewersOnRunningProduction, + useRemoveMultiviewersOnRunningProduction, + useUpdateMultiviewersOnRunningProduction +} from '../../../hooks/workflow'; import { MultiviewSettings } from '../../../interfaces/multiview'; export default function ProductionConfiguration({ params }: PageProps) { @@ -92,8 +96,12 @@ export default function ProductionConfiguration({ params }: PageProps) { const [updateMultiviewViews] = useMultiviews(); const [updateSourceInputSlotOnMultiviewLayouts] = useUpdateSourceInputSlotOnMultiviewLayouts(); + const [addMultiviewersOnRunningProduction] = + useAddMultiviewersOnRunningProduction(); const [updateMultiviewersOnRunningProduction] = useUpdateMultiviewersOnRunningProduction(); + const [removeMultiviewersOnRunningProduction] = + useRemoveMultiviewersOnRunningProduction(); //FROM LIVE API const [pipelines, loadingPipelines, , refreshPipelines] = usePipelines(); @@ -309,12 +317,26 @@ export default function ProductionConfiguration({ params }: PageProps) { (oldItem) => !presetMultiviewsMap.has(oldItem.multiview_id) ); - updateMultiviewersOnRunningProduction( - (productionSetup?._id.toString(), updatedPreset), - additions, - updates, - removals - ); + if (additions.length > 0) { + addMultiviewersOnRunningProduction( + (productionSetup?._id.toString(), updatedPreset), + additions + ); + } + + if (updates.length > 0) { + updateMultiviewersOnRunningProduction( + (productionSetup?._id.toString(), updatedPreset), + updates + ); + } + + if (removals.length > 0) { + removeMultiviewersOnRunningProduction( + (productionSetup?._id.toString(), updatedPreset), + removals + ); + } } putProduction(productionSetup?._id.toString(), updatedPreset).then(() => { diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx index 007b9df..a06d2a5 100644 --- a/src/components/modal/Modal.tsx +++ b/src/components/modal/Modal.tsx @@ -6,7 +6,7 @@ import { Modal as BaseModal } from '@mui/base'; type BaseModalProps = { open: boolean; forwardRef?: LegacyRef | null; - outsideClick: () => void; + outsideClick?: () => void; className?: string; }; @@ -16,16 +16,21 @@ export function Modal({ open, children, outsideClick, className }: ModalProps) { const element = useRef(null); useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (element.current && !element.current.contains(event.target as Node)) { - outsideClick(); - } - }; + if (outsideClick) { + const handleClickOutside = (event: MouseEvent) => { + if ( + element.current && + !element.current.contains(event.target as Node) + ) { + outsideClick(); + } + }; - document.addEventListener('click', handleClickOutside, true); - return () => { - document.removeEventListener('click', handleClickOutside, true); - }; + document.addEventListener('click', handleClickOutside, true); + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + } }, [outsideClick]); return ( diff --git a/src/components/modal/UpdateMultiviewersModal.tsx b/src/components/modal/UpdateMultiviewersModal.tsx new file mode 100644 index 0000000..526b12b --- /dev/null +++ b/src/components/modal/UpdateMultiviewersModal.tsx @@ -0,0 +1,37 @@ +import { useTranslate } from '../../i18n/useTranslate'; +import { Button } from '../button/Button'; +import { Modal } from './Modal'; + +type UpdateMultiviewersModalProps = { + open: boolean; + onAbort: () => void; + onConfirm: () => void; +}; + +export function UpdateMultiviewersModal({ + open, + onAbort, + onConfirm +}: UpdateMultiviewersModalProps) { + const t = useTranslate(); + return ( + +
+

{t('preset.confirm_update_multiviewers')}

+ +
+ + + +
+
+
+ ); +} diff --git a/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx b/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx index 23cd03f..bed8b0a 100644 --- a/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx +++ b/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx @@ -12,6 +12,7 @@ import { usePutMultiviewLayout } from '../../../hooks/multiviewLayout'; import Decision from '../configureOutputModal/Decision'; import MultiviewLayoutSettings from './MultiviewLayoutSettings/MultiviewLayoutSettings'; import { IconSettings } from '@tabler/icons-react'; +import { UpdateMultiviewersModal } from '../UpdateMultiviewersModal'; type ConfigureMultiviewModalProps = { open: boolean; @@ -33,6 +34,7 @@ export function ConfigureMultiviewModal({ [] ); const [layoutModalOpen, setLayoutModalOpen] = useState(false); + const [confirmUpdateModalOpen, setConfirmUpdateModalOpen] = useState(false); const [newMultiviewLayout, setNewMultiviewLayout] = useState(null); const addNewLayout = usePutMultiviewLayout(); @@ -59,6 +61,14 @@ export function ConfigureMultiviewModal({ }, [multiviews]); const onSave = () => { + if (production?.isActive && !confirmUpdateModalOpen) { + setConfirmUpdateModalOpen(true); + return; + } + if (production?.isActive && confirmUpdateModalOpen) { + setConfirmUpdateModalOpen(false); + } + const presetToUpdate = deepclone(preset); if (!multiviews) { @@ -162,7 +172,7 @@ export function ConfigureMultiviewModal({ }; return ( - clearInputs()}> + {!layoutModalOpen && (
{multiviews && @@ -245,6 +255,14 @@ export function ConfigureMultiviewModal({ onClose={() => (layoutModalOpen ? closeLayoutModal() : clearInputs())} onSave={() => (layoutModalOpen ? onUpdateLayoutPreset() : onSave())} /> + + {confirmUpdateModalOpen && ( + setConfirmUpdateModalOpen(false)} + onConfirm={() => onSave()} + /> + )} ); } diff --git a/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx b/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx index a1fdb1c..49aa082 100644 --- a/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx +++ b/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx @@ -61,6 +61,7 @@ export default function MultiviewSettingsConfig({ } handleUpdateMultiview({ ...multiviewLayouts[0], + _id: multiviewLayouts[0]._id?.toString(), for_pipeline_idx: 0 }); } @@ -80,7 +81,11 @@ export default function MultiviewSettingsConfig({ output: multiview?.output || selected.output }; setSelectedMultiviewLayout(updatedMultiview); - handleUpdateMultiview({ ...updatedMultiview, for_pipeline_idx: 0 }); + handleUpdateMultiview({ + ...updatedMultiview, + _id: updatedMultiview._id?.toString(), + for_pipeline_idx: 0 + }); }; const getNumber = (val: string, prev: number) => { diff --git a/src/components/startProduction/StartProductionButton.tsx b/src/components/startProduction/StartProductionButton.tsx index 61b4384..3f7944c 100644 --- a/src/components/startProduction/StartProductionButton.tsx +++ b/src/components/startProduction/StartProductionButton.tsx @@ -106,7 +106,13 @@ export function StartProductionButton({ ), { ...pipelineToUpdateMultiview, - multiviews: [{ ...multiviewLayouts[0], for_pipeline_idx: 0 }] + multiviews: [ + { + ...multiviewLayouts[0], + for_pipeline_idx: 0, + _id: multiviewLayouts[0]._id?.toString() + } + ] } ] } diff --git a/src/hooks/workflow.ts b/src/hooks/workflow.ts index e4e797b..5c8b64e 100644 --- a/src/hooks/workflow.ts +++ b/src/hooks/workflow.ts @@ -8,8 +8,8 @@ import { import { CallbackHook } from './types'; import { Result } from '../interfaces/result'; import { API_SECRET_KEY } from '../utils/constants'; -import { TeardownOptions } from '../api/manager/teardown'; import { MultiviewSettings } from '../interfaces/multiview'; +import { TeardownOptions } from '../api/manager/teardown'; export function useStopProduction(): CallbackHook< (production: Production) => Promise> @@ -58,6 +58,86 @@ export function useStartProduction(): CallbackHook< return [startProduction, loading]; } +export function useAddMultiviewersOnRunningProduction(): CallbackHook< + (production: Production, additions: MultiviewSettings[]) => void +> { + const [loading, setLoading] = useState(false); + + const addMultiviewersOnRunningProduction = useCallback( + async (production: Production, additions: MultiviewSettings[]) => { + setLoading(true); + + fetch('/api/manager/multiviewersOnRunningProduction', { + method: 'POST', + headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], + body: JSON.stringify({ production, additions }) + }).finally(() => setLoading(false)); + }, + [] + ); + + return [addMultiviewersOnRunningProduction, loading]; +} + +export function useUpdateMultiviewersOnRunningProduction(): CallbackHook< + (production: Production, updates: MultiviewSettings[]) => void +> { + const [loading, setLoading] = useState(false); + + const updateMultiviewersOnRunningProduction = useCallback( + async (production: Production, updates: MultiviewSettings[]) => { + setLoading(true); + + updates.forEach(async (multiview) => { + try { + return await fetch( + `/api/manager/multiviewersOnRunningProduction/${multiview.multiview_id}`, + { + method: 'PUT', + headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], + body: JSON.stringify({ production, updates }) + } + ); + } finally { + setLoading(false); + } + }); + }, + [] + ); + + return [updateMultiviewersOnRunningProduction, loading]; +} + +export function useRemoveMultiviewersOnRunningProduction(): CallbackHook< + (production: Production, removals: MultiviewSettings[]) => void +> { + const [loading, setLoading] = useState(false); + + const removeMultiviewersOnRunningProduction = useCallback( + async (production: Production, removals: MultiviewSettings[]) => { + setLoading(true); + removals.forEach(async (multiview) => { + try { + return await fetch( + `/api/manager/multiviewersOnRunningProduction/${multiview.multiview_id}`, + { + method: 'DELETE', + headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], + body: JSON.stringify({ production, removals }) + } + ); + } finally { + setLoading(false); + } + }); + }, + [] + ); + + return [removeMultiviewersOnRunningProduction, loading]; +} + export function useTeardown(): CallbackHook< (option: TeardownOptions) => Promise> > { @@ -79,68 +159,3 @@ export function useTeardown(): CallbackHook< return [teardown, loading]; } - -export function useUpdateMultiviewersOnRunningProduction(): CallbackHook< - ( - production: Production, - additions: MultiviewSettings[], - updates: MultiviewSettings[], - removals: MultiviewSettings[] - ) => void -> { - const [loading, setLoading] = useState(false); - - const updateMultiviewersOnRunningProduction = useCallback( - async ( - production: Production, - additions: MultiviewSettings[], - updates: MultiviewSettings[], - removals: MultiviewSettings[] - ) => { - setLoading(true); - switch (true) { - case additions.length > 0: - return fetch('/api/manager/multiviewersOnRunningProduction', { - method: 'POST', - headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], - body: JSON.stringify({ production, additions }) - }).finally(() => setLoading(false)); - case updates.length > 0: - updates.forEach(async (multiview) => { - try { - return await fetch( - `/api/manager/multiviewersOnRunningProduction/${multiview.multiview_id}`, - { - method: 'PUT', - headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], - body: JSON.stringify({ production, updates }) - } - ); - } finally { - setLoading(false); - } - }); - break; - case removals.length > 0: - removals.forEach(async (multiview) => { - try { - return await fetch( - `/api/manager/multiviewersOnRunningProduction/${multiview.multiview_id}`, - { - method: 'DELETE', - headers: [['x-api-key', `Bearer ${API_SECRET_KEY}`]], - body: JSON.stringify({ production, removals }) - } - ); - } finally { - setLoading(false); - } - }); - break; - } - }, - [] - ); - - return [updateMultiviewersOnRunningProduction, loading]; -} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 7de30c3..cefc750 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -673,7 +673,10 @@ export const en = { layout_already_exist: 'Layout {{layoutNameAlreadyExist}} will be replaced on save', remove_multiview: 'Remove multiview', - add_another_multiview: 'Add another multiview' + add_another_multiview: 'Add another multiview', + confirm_update_multiviewers: + 'Are you sure you want to update multiviewers for the running production?', + confirm_update: 'Update multiviewers' }, error: { missing_sources_in_db: 'Missing sources, please restart production.', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index 15579b4..1cd73d7 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -677,7 +677,10 @@ export const sv = { layout_already_exist: 'Konfigurationen {{layoutNameAlreadyExist}} skrivs över om du sparar', remove_multiview: 'Ta bort multiview', - add_another_multiview: 'Lägg till ny multiview' + add_another_multiview: 'Lägg till ny multiview', + confirm_update_multiviewers: + 'Är du säker på att du vill uppdatera multiview för pågående produktion?', + confirm_update: 'Uppdatera multiviewers' }, error: { missing_sources_in_db: 'Källor saknas, var god starta om produktionen.', diff --git a/src/interfaces/preset.ts b/src/interfaces/preset.ts index 8bf4809..6e791cf 100644 --- a/src/interfaces/preset.ts +++ b/src/interfaces/preset.ts @@ -19,7 +19,7 @@ export interface PresetReference { } export interface MultiviewPreset { - _id?: ObjectId; + _id?: ObjectId | string; name: string; layout: MultiviewStructureLayout; output: MultiviewOutputSettings;