From 5251717961cae9f3fee4756f916a83e5d24e4d4a Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 30 Oct 2026 13:53:13 +0100 Subject: [PATCH 1/2] Support the extended capture agent API (2.x) Read parsed capabilities and vendor info from agents, show them in the recording details, and let users pick stream configuration, positions and content when scheduling, without inputs for agents offering content. This commit was AI generated --- .../EventDetailsSchedulingTab.tsx | 112 +++++++++++------- .../ModalTabsAndPages/NewSourcePage.tsx | 49 +++++--- .../scheduling/SchedulingCaptureOptions.tsx | 55 +++++++++ .../wizards/scheduling/SchedulingInputs.tsx | 5 +- .../wizards/CapabilitiesDetailsTab.tsx | 45 +++++++ .../partials/wizards/GeneralDetailsTab.tsx | 18 +++ src/configs/modalConfig.ts | 6 + .../adminui/languages/lang-en_US.json | 30 ++++- src/slices/eventDetailsSlice.ts | 50 ++++++-- src/slices/eventSlice.ts | 15 +++ src/slices/recordingDetailsSlice.ts | 28 ++++- src/slices/recordingSlice.ts | 76 +++++++++++- 12 files changed, 411 insertions(+), 78 deletions(-) create mode 100644 src/components/events/partials/wizards/scheduling/SchedulingCaptureOptions.tsx diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx index bfba0c5ef0..318dfcaf0e 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx @@ -36,6 +36,7 @@ import { NOTIFICATION_CONTEXT } from "../../../../configs/modalConfig"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { checkConflicts, + getSelectedCaptureOptions, saveSchedulingInfo, SchedulingInfo, } from "../../../../slices/eventDetailsSlice"; @@ -43,18 +44,25 @@ import { removeNotificationWizardForm, addNotification, } from "../../../../slices/notificationSlice"; -import { Recording } from "../../../../slices/recordingSlice"; +import { + CaptureOptionSelection, + captureOptions, + contentOverridesInputs, + Recording, +} from "../../../../slices/recordingSlice"; import { useTranslation } from "react-i18next"; import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons"; import SchedulingTime from "../wizards/scheduling/SchedulingTime"; import SchedulingLocation from "../wizards/scheduling/SchedulingLocation"; import SchedulingInputs from "../wizards/scheduling/SchedulingInputs"; +import SchedulingCaptureOptions from "../wizards/scheduling/SchedulingCaptureOptions"; + import SchedulingConflicts from "../wizards/scheduling/SchedulingConflicts"; import { ParseKeys } from "i18next"; import ModalContentTable from "../../../shared/modals/ModalContentTable"; import i18n from "../../../../i18n/i18n"; -export type InitialValues = { +export type InitialValues = CaptureOptionSelection & { scheduleStartDate: string; scheduleStartHour: string; scheduleStartMinute: string; @@ -143,10 +151,24 @@ const EventDetailsSchedulingTab = ({ return value ? t(value as ParseKeys) : ""; }; - // changes the inputs in the formik + // finds the agent, whose options are to be displayed in the formik + const getAgent = (deviceId: Recording["id"]) => { + if (deviceId === source.device.id) { + return source.device; + } + return filterDevicesForAccess(user, captureAgents).find(agent => agent.id === deviceId); + }; + + // changes the inputs and other options in the formik const changeInputs = (deviceId: Recording["id"], setFieldValue: FormikHelpers["setFieldValue"]) => { + const agent = getAgent(deviceId); setFieldValue("captureAgent", deviceId); - setFieldValue("inputs", []); + // The inputs of an agent offering contents cannot be selected, so all of them are kept + setFieldValue("inputs", contentOverridesInputs(agent) ? getInputs(deviceId).map(input => input.id) : []); + // Like in the wizard, all options of the agent are selected + for (const { field, list } of captureOptions) { + setFieldValue(field, agent?.[list]?.map(option => option.id) ?? []); + } }; const filterCaptureAgents = (agent: Recording) => { return agent.id === source.agentId || hasDeviceAccess(user, agent.id); @@ -161,19 +183,15 @@ const EventDetailsSchedulingTab = ({ accessAllowed(formik.values.captureAgent) && !(conflicts.length > 0) ) { - // check if user provided values differ from initial ones - if (!_.isEqual(formik.values, formik.initialValues)) { - if (!_.isEqual(formik.values.inputs, formik.initialValues.inputs)) { - return !_.isEqual( - [...formik.values.inputs].sort(), - [...formik.initialValues.inputs].sort(), - ); - } else { - return true; - } - } else { - return false; - } + // check if user provided values differ from initial ones, the order of selected options does not matter + const sortSelected = (values: InitialValues) => ({ + ...values, + inputs: [...values.inputs].sort(), + streamConfiguration: [...values.streamConfiguration].sort(), + devicePosition: [...values.devicePosition].sort(), + deviceContent: [...values.deviceContent].sort(), + }); + return !_.isEqual(sortSelected(formik.values), sortSelected(formik.initialValues)); } else { return false; } @@ -231,6 +249,7 @@ const EventDetailsSchedulingTab = ({ scheduleEndMinute: source.end.minute != null ? makeTwoDigits(source.end.minute) : "", captureAgent: source.device.name, inputs: inputs.filter(input => input !== ""), + ...getSelectedCaptureOptions(source.agentConfiguration), }; }; @@ -491,31 +510,40 @@ const EventDetailsSchedulingTab = ({ } - {/* inputs */} - - - {t( - "EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.INPUTS", - )} - - - {!!formik.values.captureAgent && - !!getInputs(formik.values.captureAgent) && - getInputs(formik.values.captureAgent).length > - 0 && - (hasAccessRole && - accessAllowed(formik.values.captureAgent) - ? - : formik.values.inputs.map((input, key) => ( - - {getInputForAgent(formik.values.captureAgent, input)} -
-
- )))} - - + {/* inputs, which are not offered if the content configuration determines them */} + {!contentOverridesInputs(getAgent(formik.values.captureAgent)) && ( + + + {t( + "EVENTS.EVENTS.DETAILS.SOURCE.PLACEHOLDER.INPUTS", + )} + + + {!!formik.values.captureAgent && + !!getInputs(formik.values.captureAgent) && + getInputs(formik.values.captureAgent).length > + 0 && + (hasAccessRole && + accessAllowed(formik.values.captureAgent) + ? + : formik.values.inputs.map((input, key) => ( + + {getInputForAgent(formik.values.captureAgent, input)} +
+
+ )))} + + + )} + + {/* other options the agent offers */} + diff --git a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx index fd495a2abe..aa94d18a34 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx @@ -36,7 +36,13 @@ import { changeStartMinuteMultiple, } from "../../../../utils/dateUtils"; import { useAppDispatch, useAppSelector } from "../../../../store"; -import { Recording, fetchRecordings } from "../../../../slices/recordingSlice"; +import { + CaptureOptionSelection, + Recording, + captureOptions, + contentOverridesInputs, + fetchRecordings, +} from "../../../../slices/recordingSlice"; import { addNotification, removeNotificationWizardForm } from "../../../../slices/notificationSlice"; import { parseISO } from "date-fns"; import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons"; @@ -46,6 +52,7 @@ import ButtonLikeAnchor from "../../../shared/ButtonLikeAnchor"; import SchedulingTime from "../wizards/scheduling/SchedulingTime"; import SchedulingLocation from "../wizards/scheduling/SchedulingLocation"; import SchedulingInputs from "../wizards/scheduling/SchedulingInputs"; +import SchedulingCaptureOptions from "../wizards/scheduling/SchedulingCaptureOptions"; import SchedulingConflicts from "../wizards/scheduling/SchedulingConflicts"; import { ParseKeys } from "i18next"; import { LuCircleX } from "react-icons/lu"; @@ -54,7 +61,7 @@ import i18n from "../../../../i18n/i18n"; /** * This component renders the source page for new events in the new event wizard. */ -interface RequiredFormProps { +interface RequiredFormProps extends CaptureOptionSelection { scheduleStartDate: string, sourceMode: string, // Schedule @@ -393,7 +400,7 @@ const Upload = ({ /* * Renders fields for providing information for schedule of event */ -const Schedule = name === formik.values.location); + const renderInputDeviceOptions = () => { if (formik.values.location) { - const inputDevice = inputDevices.find( - ({ name }) => name === formik.values.location, - ); - if (!inputDevice) { + if (!selectedDevice) { return <>; } return ( ); } @@ -759,17 +765,30 @@ const Schedule = input.id)); } + // Select all options of the location + for (const { field, list } of captureOptions) { + await formik.setFieldValue(field, inputDevice ? inputDevice[list].map(option => option.id) : []); + } // Set location await formik.setFieldValue("location", value); }} /> - - {t("EVENTS.EVENTS.NEW.SOURCE.PLACEHOLDER.INPUTS")} - - {/* Render checkbox for each input option of the selected input device*/} - {renderInputDeviceOptions()} - - + {/* The inputs are not offered, if the content configuration determines them */} + {!contentOverridesInputs(selectedDevice) && ( + + {t("EVENTS.EVENTS.NEW.SOURCE.PLACEHOLDER.INPUTS")} + + {/* Render checkbox for each input option of the selected input device*/} + {renderInputDeviceOptions()} + + + )} + {/* Other options the selected location offers */} + diff --git a/src/components/events/partials/wizards/scheduling/SchedulingCaptureOptions.tsx b/src/components/events/partials/wizards/scheduling/SchedulingCaptureOptions.tsx new file mode 100644 index 0000000000..bceec339c7 --- /dev/null +++ b/src/components/events/partials/wizards/scheduling/SchedulingCaptureOptions.tsx @@ -0,0 +1,55 @@ +import { useTranslation } from "react-i18next"; +import SchedulingInputs from "./SchedulingInputs"; +import { + CaptureOptionLists, + CaptureOptionSelection, + captureOptions, +} from "../../../../../slices/recordingSlice"; + +/** + * Renders a row for every option of a capture agent that can be chosen for an event, in the same way as the inputs + */ +const SchedulingCaptureOptions = ({ + agent, + selection, + disabled, +}: { + agent: CaptureOptionLists | undefined, + selection: CaptureOptionSelection, + // Only show what is selected + disabled: boolean, +}) => { + const { t } = useTranslation(); + + return ( + <> + {captureOptions.map(({ field, list, label }) => { + const options = agent?.[list] ?? []; + if (options.length === 0) { + return null; + } + + return ( + + {t(label)} + + {field === "deviceContent" && ( +

{t("CAPTURE_AGENT.OPTIONS.CONTENT_NOTICE")}

+ )} + {disabled + ? options.filter(option => selection[field].includes(option.id)).map(option => ( + + {t(option.value, option.id)} +
+
+ )) + : } + + + ); + })} + + ); +}; + +export default SchedulingCaptureOptions; diff --git a/src/components/events/partials/wizards/scheduling/SchedulingInputs.tsx b/src/components/events/partials/wizards/scheduling/SchedulingInputs.tsx index 25e49ba54c..30ad422c5c 100644 --- a/src/components/events/partials/wizards/scheduling/SchedulingInputs.tsx +++ b/src/components/events/partials/wizards/scheduling/SchedulingInputs.tsx @@ -2,8 +2,11 @@ import { useTranslation } from "react-i18next"; import { Field } from "formik"; const SchedulingInputs = ({ + name = "inputs", inputs, }: { + // Name of the form field, that holds the ids of the selected inputs + name?: string, inputs: { id: string, value: string @@ -17,7 +20,7 @@ const SchedulingInputs = ({ (input, key) => (