From 9a83fc3d98bb085965b123533aadba82a84683ec Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 10 Sep 2024 15:50:10 +0200 Subject: [PATCH 01/56] feat: add management lock --- src/app/inventory/page.tsx | 21 +-------- src/app/page.tsx | 18 +++----- src/app/production/[id]/page.tsx | 29 +++++++++--- src/components/button/Button.tsx | 7 ++- src/components/button/MonitoringButton.tsx | 10 ++++- .../createProduction/CreateProduction.tsx | 29 +++++++++--- .../headerNavigation/HeaderNavigation.tsx | 2 +- .../homePageContent/HomePageContent.tsx | 28 ++++++++++++ src/components/inventory/Inventory.tsx | 7 ++- .../inventory/InventoryHeaderContent.tsx | 32 ++++++++++++++ .../inventory/InventoryPageContent.tsx | 32 ++++++++++++++ .../editView/AudioChannels/AudioChannels.tsx | 4 +- .../editView/AudioChannels/NumberInput.tsx | 2 +- .../editView/AudioChannels/Outputs.tsx | 10 +++-- .../inventory/editView/EditView.tsx | 23 ++++++---- .../inventory/editView/GeneralSettings.tsx | 19 ++++++-- .../inventory/editView/SelectOptions.tsx | 27 ++++++++---- .../inventory/editView/UpdateButtons.tsx | 44 ++++++++++++++----- src/components/lockButton/LockButton.tsx | 23 ++++++++++ .../DeleteProductionButton.tsx | 12 ++--- .../productionsList/ProductionsList.tsx | 12 +++-- .../productionsList/ProductionsListItem.tsx | 24 +++++++--- src/components/sourceCard/SourceCard.tsx | 12 ++++- src/components/sourceCards/SourceCards.tsx | 6 ++- .../startProduction/ConfigureOutputButton.tsx | 7 ++- .../startProduction/StartProductionButton.tsx | 13 +++++- 26 files changed, 340 insertions(+), 113 deletions(-) create mode 100644 src/components/homePageContent/HomePageContent.tsx create mode 100644 src/components/inventory/InventoryHeaderContent.tsx create mode 100644 src/components/inventory/InventoryPageContent.tsx create mode 100644 src/components/lockButton/LockButton.tsx diff --git a/src/app/inventory/page.tsx b/src/app/inventory/page.tsx index 0b851641..a762e9da 100644 --- a/src/app/inventory/page.tsx +++ b/src/app/inventory/page.tsx @@ -1,22 +1,5 @@ -import { Suspense } from 'react'; -import HeaderNavigation from '../../components/headerNavigation/HeaderNavigation'; -import { useTranslate } from '../../i18n/useTranslate'; -import { LoadingCover } from '../../components/loader/LoadingCover'; -import Inventory from '../../components/inventory/Inventory'; +import { InventoryPageContent } from '../../components/inventory/InventoryPageContent'; export default function Page() { - const t = useTranslate(); - - return ( - <> - -

- {t('inventory')} -

-
- }> - - - - ); + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 30467a92..f6ad0174 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,15 @@ -import React, { Suspense } from 'react'; -import ProductionsList from '../components/productionsList/ProductionsList'; -import { CreateProduction } from '../components/createProduction/CreateProduction'; -import { LoadingCover } from '../components/loader/LoadingCover'; +import React from 'react'; import Link from 'next/link'; import { Button } from '../components/button/Button'; import { useTranslate } from '../i18n/useTranslate'; +import { getProductions } from '../api/manager/productions'; +import { HomePageContent } from '../components/homePageContent/HomePageContent'; export const dynamic = 'force-dynamic'; -function Home() { +async function Home() { const t = useTranslate(); + const productions = await getProductions(); return ( <>
@@ -22,13 +22,7 @@ function Home() {
- - -
- }> - {/* @ts-expect-error Async Server Component: https://github.com/vercel/next.js/issues/42292 */} - - +
); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index be3112c7..e56eb63e 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -42,6 +42,7 @@ import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { ISource } from '../../../hooks/useDragableItems'; import { useMultiviews } from '../../../hooks/multiviews'; +import { LockButton } from '../../../components/lockButton/LockButton'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -88,6 +89,8 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + const [isLocked, setIsLocked] = useState(true); + useEffect(() => { refreshPipelines(); refreshControlPanels(); @@ -617,14 +620,21 @@ export default function ProductionConfiguration({ params }: PageProps) { } }} onBlur={() => updateConfigName(configurationName)} + disabled={isLocked} />
+ setIsLocked(!isLocked)} + />
@@ -735,6 +745,7 @@ export default function ProductionConfiguration({ params }: PageProps) { }); } }} + isLocked={isLocked} /> {removeSourceModal && selectedSourceRef && ( { setInventoryVisible(true); @@ -764,7 +776,7 @@ export default function ProductionConfiguration({ params }: PageProps) { (pipeline, i) => { return ( ({ @@ -780,7 +792,7 @@ export default function ProductionConfiguration({ params }: PageProps) { )} {productionSetup?.production_settings && ( ({ option: controlPanel.name, available: controlPanel.outgoing_connections?.length === 0 @@ -799,7 +811,10 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup && productionSetup.isActive && (
- +
)} diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 673a0ca1..489c9035 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -1,5 +1,6 @@ import { PropsWithChildren } from 'react'; import { twMerge } from 'tailwind-merge'; + type ButtonProps = { type?: 'submit' | 'button'; className?: string; @@ -26,16 +27,14 @@ export function Button({ hoverMessage, icon }: PropsWithChildren) { - const css = disabled - ? 'bg-gray-400 text-gray-600 cursor-default hover:bg-gray-400' - : states[state]; + const css = !disabled && states[state]; return ( @@ -92,7 +105,11 @@ export function CreateProduction() {
- + ); +}; diff --git a/src/components/productionsList/DeleteProductionButton.tsx b/src/components/productionsList/DeleteProductionButton.tsx index a0dfe898..f4cada82 100644 --- a/src/components/productionsList/DeleteProductionButton.tsx +++ b/src/components/productionsList/DeleteProductionButton.tsx @@ -11,12 +11,14 @@ type DeleteProductionButtonProps = { id: string; name: string; isActive: boolean; + isLocked: boolean; }; export function DeleteProductionButton({ id, name, - isActive + isActive, + isLocked }: DeleteProductionButtonProps) { const router = useRouter(); const deleteProduction = useDeleteProduction(); @@ -38,12 +40,12 @@ export function DeleteProductionButton({ <> {preset && ( From e210ab6c5a1b9ceed8b1277d30535cc30b430413 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 10 Sep 2024 16:20:23 +0200 Subject: [PATCH 02/56] fix: build error --- src/app/production/[id]/page.tsx | 1 + src/components/inventory/Inventory.tsx | 1 + src/components/sourceListItem/SourceListItem.tsx | 7 +++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index e56eb63e..65375959 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -374,6 +374,7 @@ export default function ProductionConfiguration({ params }: PageProps) { key={`${source.ingest_source_name}-${index}`} source={source} disabled={selectedProductionItems?.includes(source._id.toString())} + isLocked={isLocked} action={(source: SourceWithId) => { if (productionSetup && productionSetup.isActive) { setSelectedSource(source); diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index ac209898..b2d5d846 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -56,6 +56,7 @@ export default function Inventory({ isLocked }: InventoryProps) { action={(source) => { editSource(source); }} + isLocked={isLocked} /> ); } diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf7..02be92d6 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -14,9 +14,10 @@ import capitalize from '../../utils/capitalize'; type SourceListItemProps = { source: SourceWithId; - action: (source: SourceWithId) => void; edit?: boolean; disabled: unknown; + isLocked: boolean; + action: (source: SourceWithId) => void; }; const getIcon = (source: Source) => { @@ -51,7 +52,8 @@ function InventoryListItem({ source, action, disabled, - edit = false + edit = false, + isLocked }: SourceListItemProps) { const t = useTranslate(); const [previewVisible, setPreviewVisible] = useState(false); @@ -169,6 +171,7 @@ function InventoryListItem({ outputRows={outputRows} rowIndex={rowIndex} max={channelsInArray[channelsInArray.length - 1]} + isLocked={isLocked} />
))} From d67930e74474275b485f66309f436b033240847c Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 10 Sep 2024 16:21:23 +0200 Subject: [PATCH 03/56] fix: remove left-over file, has been renamed --- .../inventory/InventoryHeaderContent.tsx | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/components/inventory/InventoryHeaderContent.tsx diff --git a/src/components/inventory/InventoryHeaderContent.tsx b/src/components/inventory/InventoryHeaderContent.tsx deleted file mode 100644 index a7b93e12..00000000 --- a/src/components/inventory/InventoryHeaderContent.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; -import { useState, Suspense } from 'react'; -import { LockButton } from '../lockButton/LockButton'; -import { useTranslate } from '../../i18n/useTranslate'; -import HeaderNavigation from '../headerNavigation/HeaderNavigation'; -import Inventory from './Inventory'; - -export const InventoryHeaderContent = () => { - const [isLocked, setIsLocked] = useState(true); - const t = useTranslate(); - - return ( - <> - -
-
-

- {t('inventory')} -

- setIsLocked(!isLocked)} - /> -
-
-
- - - - - ); -}; From fa006bf4ee19704e3ce0b36dcac09ef55db29153 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 09:24:02 +0200 Subject: [PATCH 04/56] feat: support media and html sources in prod and web sockets --- .env.sample | 3 + package-lock.json | 32 +++- package.json | 6 +- src/api/ateliereLive/websocket.ts | 45 ++++++ src/api/manager/productions.ts | 1 + src/api/manager/sources.ts | 51 ++++--- src/api/manager/workflow.ts | 45 +++++- src/app/html_input/page.tsx | 10 ++ src/app/production/[id]/page.tsx | 106 ++++++++++--- src/components/addInput/AddInput.tsx | 30 ++++ src/components/addSource/AddSource.tsx | 24 --- src/components/dragElement/DragItem.tsx | 51 ++++--- src/components/filter/FilterDropdown.tsx | 5 +- src/components/filter/FilterOptions.tsx | 8 + src/components/modal/AddSourceModal.tsx | 2 +- src/components/select/Select.tsx | 27 ++++ src/components/sourceCard/SourceCard.tsx | 121 ++++++++++----- src/components/sourceCard/SourceThumbnail.tsx | 58 +++++--- src/components/sourceCards/SourceCards.tsx | 139 +++++++----------- .../sourceListItem/SourceListItem.tsx | 4 +- src/hooks/items/addSetupItem.ts | 3 +- src/hooks/productions.ts | 3 +- src/hooks/useDragableItems.ts | 83 +++++++---- src/i18n/locales/en.ts | 10 +- src/i18n/locales/sv.ts | 10 +- src/interfaces/Source.ts | 4 +- src/middleware.ts | 2 +- 27 files changed, 602 insertions(+), 281 deletions(-) create mode 100644 src/api/ateliereLive/websocket.ts create mode 100644 src/app/html_input/page.tsx create mode 100644 src/components/addInput/AddInput.tsx delete mode 100644 src/components/addSource/AddSource.tsx create mode 100644 src/components/select/Select.tsx diff --git a/.env.sample b/.env.sample index 8c3990fc..da9f7260 100644 --- a/.env.sample +++ b/.env.sample @@ -14,3 +14,6 @@ BCRYPT_SALT_ROUNDS=${BCRYPT_SALT_ROUNDS:-10} # i18n UI_LANG=${UI_LANG:-en} + +# Mediaplayer - path on the system controller +MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 73c7afff..26159b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -35,7 +36,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", @@ -2512,6 +2514,14 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -11233,6 +11243,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3d341165..7b3597c1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "pretty:format": "prettier --write .", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "next lint", - "dev": "./update_gui_version.sh && next dev", + "dev": "next dev", "build": "next build", "start": "next start", "version:rc": "npm version prerelease --preid=rc", @@ -32,6 +32,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -48,7 +49,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts new file mode 100644 index 00000000..6f51466f --- /dev/null +++ b/src/api/ateliereLive/websocket.ts @@ -0,0 +1,45 @@ +import WebSocket from 'ws'; + +function createWebSocket(): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://${process.env.AGILE_WEBSOCKET}`); + ws.on('error', reject); + ws.on('open', () => { + // const send = ws.send.bind(ws); + // ws.send = (message) => { + // console.debug(`[websocket] sending message: ${message}`); + // send(message); + // }; + resolve(ws); + }); + }); +} + +export async function createControlPanelWebSocket() { + const ws = await createWebSocket(); + return { + createHtml: (input: number) => { + ws.send('html reset'); + ws.send(`html create ${input} 1920 1080`); + setTimeout(() => { + ws.send( + `html load ${input} ${process.env.NEXTAUTH_URL}/html_input?input=${input}` + ); + }, 1000); + }, + createMediaplayer: (input: number) => { + ws.send('media reset'); + ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`); + ws.send(`media play ${input}`); + }, + closeHtml: (input: number) => { + ws.send(`html close ${input}`); + }, + closeMediaplayer: (input: number) => { + ws.send(`media close ${input}`); + }, + close: () => { + ws.close(); + } + }; +} diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e68524ad..714d822d 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -2,6 +2,7 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Production, ProductionWithId } from '../../interfaces/production'; import { Log } from '../logger'; +import { SourceReference, Type } from '../../interfaces/Source'; export async function getProductions(): Promise { const db = await getDatabase(); diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 8bb83e80..a54fb38a 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -1,6 +1,6 @@ import inventory from './mocks/inventory.json'; import { Source } from '../../interfaces/Source'; -import { ObjectId } from 'mongodb'; +import { ObjectId, OptionalId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -9,37 +9,44 @@ export function getMockedSources() { export async function postSource(data: Source): Promise { const db = await getDatabase(); - return (await db.collection('inventory').insertOne(data)) - .insertedId as ObjectId; + const insertData: OptionalId> & { _id?: ObjectId } = { + ...data, + _id: typeof data._id === 'string' ? new ObjectId(data._id) : data._id + }; + const result = await db.collection('inventory').insertOne(insertData); + return result.insertedId as ObjectId; } export async function getSources() { const db = await getDatabase(); return await db.collection('inventory').find().toArray(); } - export async function getSourcesByIds(_ids: string[]) { const db = await getDatabase().catch(() => { - throw "Can't connect to Database"; - }); - const objectIds = _ids.map((id: string) => { - return new ObjectId(id); + throw new Error("Can't connect to Database"); }); - return ( - await db - .collection('inventory') - .find({ - _id: { - $in: objectIds - } - }) - .toArray() - ).sort( - (a, b) => - _ids.findIndex((id) => a._id.equals(id)) - - _ids.findIndex((id) => b._id.equals(id)) - ); + const objectIds = _ids.map((id: string) => new ObjectId(id)); + + const sources = await db + .collection('inventory') + .find({ + _id: { + $in: objectIds + } + }) + .toArray(); + + return sources.sort((a, b) => { + const findIndex = (id: ObjectId | string) => + _ids.findIndex((originalId) => + id instanceof ObjectId + ? id.equals(new ObjectId(originalId)) + : id === originalId + ); + + return findIndex(a._id) - findIndex(b._id); + }); } export async function updateSource(source: any) { diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 7538be78..8edc5a9e 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -49,6 +49,8 @@ import { Result } from '../../interfaces/result'; import { Monitoring } from '../../interfaces/monitoring'; import { getDatabase } from '../mongoClient/dbClient'; import { updatedMonitoringForProduction } from './job/syncMonitoring'; +import { createControlPanelWebSocket } from '../ateliereLive/websocket'; +import { ObjectId } from 'mongodb'; const isUsed = (pipeline: ResourcesPipelineResponse) => { const hasStreams = pipeline.streams.length > 0; @@ -89,7 +91,7 @@ async function connectIngestSources( source.ingest_source_name, false ); - const audioSettings = await getAudioMapping(source._id); + const audioSettings = await getAudioMapping(new ObjectId(source._id)); const newAudioMapping = audioSettings?.audio_stream?.audio_mapping; const audioMapping = newAudioMapping?.length ? newAudioMapping : [[0, 1]]; @@ -308,6 +310,14 @@ export async function stopProduction( (p) => p.pipeline_id ); + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + for (const source of production.sources) { for (const stream_uuid of source.stream_uuids || []) { await deleteStreamByUuid(stream_uuid).catch((error) => { @@ -316,6 +326,11 @@ export async function stopProduction( } } + htmlSources.map((source) => controlPanelWS.closeHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.closeMediaplayer(source.input_slot) + ); + for (const id of pipelineIds) { Log().info(`Stopping pipeline '${id}'`); if (!id) continue; @@ -449,10 +464,30 @@ export async function startProduction( // Try to setup streams from ingest(s) to pipeline(s) start try { // Get sources from the DB + // Skapa en createHtmlWebSocket, spara + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + + htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.createMediaplayer(source.input_slot) + ); + + controlPanelWS.close(); + + // Nedan behöver göras efter att vi har skapat en produktion + // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( - production.sources.map((source) => { - return source._id.toString(); - }) + production.sources + .filter((source) => source._id !== undefined) + .map((source) => { + return source._id!.toString(); + }) ).catch((error) => { if (error === "Can't connect to Database") { throw "Can't connect to Database"; @@ -720,7 +755,7 @@ export async function startProduction( ...production, sources: production.sources.map((source) => { const streamsForSource = streams?.filter( - (stream) => stream.source_id === source._id.toString() + (stream) => stream.source_id === source._id?.toString() ); return { ...source, diff --git a/src/app/html_input/page.tsx b/src/app/html_input/page.tsx new file mode 100644 index 00000000..81cfaa52 --- /dev/null +++ b/src/app/html_input/page.tsx @@ -0,0 +1,10 @@ +import { PageProps } from '../../../.next/types/app/html_input/page'; + +export default function HtmlInput({ searchParams: { input } }: PageProps) { + return ( +
+

HTML INPUT

+

{input}

+
+ ); +} diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index be3112c7..0b4259df 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -3,14 +3,15 @@ import React, { useEffect, useState, KeyboardEvent } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; import SourceListItem from '../../../components/sourceListItem/SourceListItem'; import FilterOptions from '../../../components/filter/FilterOptions'; -import { AddSource } from '../../../components/addSource/AddSource'; +import { AddInput } from '../../../components/addInput/AddInput'; import { IconX } from '@tabler/icons-react'; import { useSources } from '../../../hooks/sources/useSources'; import { AddSourceStatus, DeleteSourceStatus, SourceReference, - SourceWithId + SourceWithId, + Type } from '../../../interfaces/Source'; import { useGetProduction, usePutProduction } from '../../../hooks/productions'; import { Production } from '../../../interfaces/production'; @@ -40,8 +41,9 @@ import { RemoveSourceModal } from '../../../components/modal/RemoveSourceModal'; import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; -import { ISource } from '../../../hooks/useDragableItems'; import { useMultiviews } from '../../../hooks/multiviews'; +import { v4 as uuidv4 } from 'uuid'; +import { Select } from '../../../components/select/Select'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -51,6 +53,9 @@ export default function ProductionConfiguration({ params }: PageProps) { const [filteredSources, setFilteredSources] = useState( new Map() ); + const [selectedValue, setSelectedValue] = useState( + t('production.add_other_source_type') + ); const [addSourceModal, setAddSourceModal] = useState(false); const [removeSourceModal, setRemoveSourceModal] = useState(false); const [selectedSource, setSelectedSource] = useState< @@ -59,6 +64,8 @@ export default function ProductionConfiguration({ params }: PageProps) { const [selectedSourceRef, setSelectedSourceRef] = useState< SourceReference | undefined >(); + const [sourceReferenceToAdd, setSourceReferenceToAdd] = + useState(); const [createStream, loadingCreateStream] = useCreateStream(); const [deleteStream, loadingDeleteStream] = useDeleteStream(); //PRODUCTION @@ -88,11 +95,39 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + const isAddButtonDisabled = + selectedValue !== 'HTML' && selectedValue !== 'Media Player'; + useEffect(() => { refreshPipelines(); refreshControlPanels(); }, [productionSetup?.isActive]); + // TODO: Väldigt lik den för ingest_source --> ändra?? + const addSourceToProduction = (type: Type) => { + const newSource: SourceReference = { + _id: uuidv4(), + type: type, + label: type === 'html' ? 'HTML Input' : 'Media Player Source', + input_slot: getFirstEmptySlot() + }; + + setSourceReferenceToAdd(newSource); + + if (productionSetup) { + const updatedSetup = addSetupItem(newSource, productionSetup); + + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + refreshProduction(); + setAddSourceModal(false); + setSourceReferenceToAdd(undefined); + }); + setAddSourceStatus(undefined); + } + }; + const setSelectedControlPanel = (controlPanel: string[]) => { setProductionSetup((prevState) => { if (!prevState) return; @@ -219,6 +254,12 @@ export default function ProductionConfiguration({ params }: PageProps) { setFilteredSources(sources); }, [sources]); + useEffect(() => { + if (selectedValue === t('production.source')) { + setInventoryVisible(true); + } + }, [selectedValue]); + const updatePreset = (preset: Preset) => { if (!productionSetup?._id) return; putProduction(productionSetup?._id.toString(), { @@ -379,6 +420,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const updatedSetup = addSetupItem( { _id: source._id.toString(), + type: 'ingest_source', label: source.ingest_source_name, input_slot: getFirstEmptySlot() }, @@ -456,8 +498,9 @@ export default function ProductionConfiguration({ params }: PageProps) { } if (result.ok) { if (result.value.success) { - const sourceToAdd = { + const sourceToAdd: SourceReference = { _id: result.value.streams[0].source_id, + type: 'ingest_source', label: selectedSource.name, stream_uuids: result.value.streams.map((r) => r.stream_uuid), input_slot: getFirstEmptySlot() @@ -601,6 +644,7 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(undefined); setDeleteSourceStatus(undefined); }; + return ( <> @@ -700,15 +744,12 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); }} - onSourceUpdate={( - source: SourceReference, - sourceItem: ISource - ) => { - sourceItem.label = source.label; + onSourceUpdate={(source: SourceReference) => { updateSource(source, productionSetup); }} onSourceRemoval={(source: SourceReference) => { @@ -719,6 +760,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const updatedSetup = removeSetupItem( { _id: source._id, + type: source.type, label: source.label, input_slot: source.input_slot }, @@ -748,15 +790,43 @@ export default function ProductionConfiguration({ params }: PageProps) { )} )} - { - setInventoryVisible(true); - }} - /> +
+ setInventoryVisible(true)} + disabled={ + productionSetup?.production_settings === undefined || + productionSetup.production_settings === null + } + /> +
+ + {options.map((value) => ( + + ))} + + ); +}; diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 6d1ecd85..246e7c22 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -2,20 +2,22 @@ import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; import { IconTrash } from '@tabler/icons-react'; -import { SourceReference } from '../../interfaces/Source'; +import { SourceReference, Type } from '../../interfaces/Source'; import { SourceThumbnail } from './SourceThumbnail'; import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source: ISource; + source?: ISource; label: string; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src: string; + src?: string; + sourceRef?: SourceReference; + type: Type; }; export default function SourceCard({ @@ -26,9 +28,13 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + sourceRef, + type }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); + const [sourceLabel, setSourceLabel] = useState( + sourceRef?.label || source?.name + ); const t = useTranslate(); @@ -37,20 +43,29 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - // if (source.name === label) { - // return; - // } - if (sourceLabel.length === 0) { - setSourceLabel(source.name); + if (sourceLabel?.length === 0) { + if (source) { + setSourceLabel(source.name); + } else if (sourceRef) { + setSourceLabel(sourceRef.label); + } } - onSourceUpdate( - { + + if (source) { + onSourceUpdate({ _id: source._id.toString(), - label: sourceLabel, + type: 'ingest_source', + label: sourceLabel || source.name, input_slot: source.input_slot - }, - source - ); + }); + } else if (sourceRef) { + onSourceUpdate({ + _id: sourceRef._id, + type: sourceRef.type, + label: sourceLabel || sourceRef.label, + input_slot: sourceRef.input_slot + }); + } }; const handleKeyDown = (event: KeyboardEvent) => { @@ -77,25 +92,59 @@ export default function SourceCard({ onBlur={saveText} />
- -

- {t('source.ingest', { - ingest: source.ingest_name - })} -

- + {source && source.src && ( + + )} + {!source && sourceRef && } + {(sourceRef || source) && ( +

+ {t('source.input_slot', { + input_slot: + sourceRef?.input_slot?.toString() || + source?.input_slot?.toString() || + '' + })} +

+ )} + + {source && ( +

+ {t('source.ingest', { + ingest: source.ingest_name + })} +

+ )} + {(source || sourceRef) && ( + + )}
); } diff --git a/src/components/sourceCard/SourceThumbnail.tsx b/src/components/sourceCard/SourceThumbnail.tsx index 5aa7114f..b5e5bcbe 100644 --- a/src/components/sourceCard/SourceThumbnail.tsx +++ b/src/components/sourceCard/SourceThumbnail.tsx @@ -2,18 +2,19 @@ import Image from 'next/image'; import { useState } from 'react'; -import { Source } from '../../interfaces/Source'; +import { Source, Type } from '../../interfaces/Source'; import { IconExclamationCircle } from '@tabler/icons-react'; type SourceThumbnailProps = { - source: Source; - src: string; + source?: Source; + src?: string; + type?: Type; }; -export function SourceThumbnail({ source, src }: SourceThumbnailProps) { +export function SourceThumbnail({ source, src, type }: SourceThumbnailProps) { const [loaded, setLoaded] = useState(false); - if (source.status === 'gone') { + if (source && source.status === 'gone') { return (
@@ -22,20 +23,37 @@ export function SourceThumbnail({ source, src }: SourceThumbnailProps) { } return ( - Preview Thumbnail setLoaded(true)} - onError={() => setLoaded(true)} - placeholder="empty" - width={0} - height={0} - sizes="20vh" - style={{ - width: 'auto', - height: '100%' - }} - /> + <> + {(type === 'ingest_source' || !type) && src && ( + Preview Thumbnail setLoaded(true)} + onError={() => setLoaded(true)} + placeholder="empty" + width={0} + height={0} + sizes="20vh" + style={{ + width: 'auto', + height: '100%' + }} + /> + )} + {(type === 'html' || type === 'mediaplayer') && ( + +

+ {type === 'html' ? 'HTML' : 'Media Player'} +

+
+ )} + ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9666bccc..d8615784 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -5,108 +5,73 @@ import { SourceReference } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; -import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; - export default function SourceCards({ productionSetup, + sourceRef, updateProduction, onSourceUpdate, onSourceRemoval }: { productionSetup: Production; + sourceRef?: SourceReference; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); - const currentOrder: SourceReference[] = items.map((source) => { - return { - _id: source._id.toString(), - label: source.label, - input_slot: source.input_slot, - stream_uuids: source.stream_uuids - }; - }); - const gridItems: React.JSX.Element[] = []; - let tempItems = [...items]; - let firstEmptySlot = items.length + 1; + if (loading || !items) return null; + + // Filter SourceReference and ISource objects correctly + const sourceReferences = items.filter( + (item): item is SourceReference => item.type !== 'ingest_source' + ); - if (!items || items.length === 0) return null; - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - if (!items.some((source) => source.input_slot === i + 1)) { - firstEmptySlot = i + 1; - break; - } - } - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - // console.log(`On input slot: ${i + 1}`); - // console.log(`Checking sources:`); - // console.log(tempItems); - tempItems.every((source) => { - if (source.input_slot === i + 1) { - // console.log(`Found source on input slot: ${i + 1}`); - // console.log(`Removing source "${source.name}" from sources list`); - tempItems = tempItems.filter((i) => i._id !== source._id); - // console.log(`Adding source "${source.name}" to grid`); - if (!productionSetup.isActive) { - gridItems.push( - - - setSelectingText(isSelecting) - } - /> - - ); - } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); - } - return false; - } else { - // console.log(`No source found on input slot: ${i + 1}`); - // console.log(`Adding empty slot to grid`); - if (productionSetup.isActive) { - gridItems.push( - - ); - } + const isISource = (source: SourceReference | ISource): source is ISource => { + // Use properties unique to ISource to check the type + return 'src' in source; + }; + + const gridItems = items.map((source) => { + const isSource = isISource(source); + + return ( + + {isSource ? ( + setSelectingText(isSelecting)} + type={'ingest_source'} + /> + ) : ( + setSelectingText(isSelecting)} + type={source.type} + /> + )} + + ); + }); - return false; - } - }); - } return <>{gridItems}; } diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf7..c8b724e5 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceWithId } from '../../interfaces/Source'; +import { Source, SourceReference, SourceWithId } from '../../interfaces/Source'; import { PreviewThumbnail } from './PreviewThumbnail'; import { getSourceThumbnail } from '../../utils/source'; import videoSettings from '../../utils/videoSettings'; @@ -95,7 +95,7 @@ function InventoryListItem({ : [] ); } - }, [source.audio_stream.audio_mapping]); + }, [source?.audio_stream.audio_mapping]); return (
  • a.input_slot - b.input_slot) }; - return { ...updatedSetup, sources: [ ...productionSetup.sources, { _id: source._id, + type: source.type, label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index e3164fe9..cdaf4612 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -11,7 +11,8 @@ export function usePostProduction() { isActive: false, name, sources: [], - selectedPresetRef: undefined + html: [], + mediaplayers: [] }) }); if (response.ok) { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 427ffbf1..4cc3a03b 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -9,57 +9,82 @@ export interface ISource extends SourceWithId { stream_uuids?: string[]; src: string; } + export function useDragableItems( sources: SourceReference[] -): [ISource[], (originId: string, destinationId: string) => void, boolean] { +): [ + (SourceReference | ISource)[], + (originId: string, destinationId: string) => void, + boolean +] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState( + const [items, setItems] = useState<(SourceReference | ISource)[]>( sources.flatMap((ref) => { const source = inventorySources.get(ref._id); if (!source) return []; return { ...source, + _id: ref._id, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) + src: getSourceThumbnail(source), + ingest_source_name: source.ingest_source_name, + ingest_name: source.ingest_name, + video_stream: source.video_stream, + audio_stream: source.audio_stream, + status: source.status, + type: source.type, + tags: source.tags, + name: source.name }; }) ); useEffect(() => { - setItems( - sources.flatMap((ref) => { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) - }; - }) - ); + const updatedItems = sources.map((ref) => { + const source = inventorySources.get(ref._id); + if (!source) return { ...ref }; + return { + ...ref, + _id: ref._id, + status: source.status, + name: source.name, + type: source.type, + tags: source.tags, + ingest_name: source.ingest_name, + ingest_source_name: source.ingest_source_name, + ingest_type: source.ingest_type, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + video_stream: source.video_stream, + audio_stream: source.audio_stream, + lastConnected: source.lastConnected + }; + }); + setItems(updatedItems); }, [sources, inventorySources]); const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((i) => i._id.toString() === originId); + const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => item._id.toString() === destinationId ); + if (!originSource || !destinationSource) return; - const originInputSlot = originSource.input_slot; - const destinationInputSlot = destinationSource.input_slot; - originSource.input_slot = destinationInputSlot; - destinationSource.input_slot = originInputSlot; - const updatedItems = [ - ...items.filter( - (i) => i._id !== originSource._id && i._id !== destinationSource._id - ), - originSource, - destinationSource - ].sort((a, b) => a.input_slot - b.input_slot); + + const updatedItems = items + .map((item) => { + if (item._id === originSource._id) + return { ...item, input_slot: destinationSource.input_slot }; + if (item._id === destinationSource._id) + return { ...item, input_slot: originSource.input_slot }; + return item; + }) + .sort((a, b) => a.input_slot - b.input_slot); + setItems(updatedItems); }; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e57..596e25c8 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -46,7 +46,8 @@ export const en = { orig: 'Original Name: {{name}}', metadata: 'Source Metadata', location_unknown: 'Unknown', - last_connected: 'Last connection' + last_connected: 'Last connection', + input_slot: 'Input slot: {{input_slot}}' }, delete_source_status: { delete_stream: 'Delete stream', @@ -63,14 +64,17 @@ export const en = { }, production_configuration: 'Production Configuration', production: { - add_source: 'Add Source', + add_source: 'Add ingest', select_preset: 'Select Preset', clear_selection: 'Clear Selection', started: 'Production started: {{name}}', failed: 'Production start failed: {{name}}', stopped: 'Production stopped: {{name}}', stop_failed: 'Production stop failed: {{name}}', - missing_multiview: 'Missing multiview reference in selected preset' + missing_multiview: 'Missing multiview reference in selected preset', + source: 'Source', + add: 'Add', + add_other_source_type: 'Add other source type' }, create_new: 'Create New', default_prod_placeholder: 'My New Configuration', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index b33112df..bf9f42c8 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -48,7 +48,8 @@ export const sv = { orig: 'Enhetsnamn: {{name}}', metadata: 'Käll-metadata', location_unknown: 'Okänd', - last_connected: 'Senast uppkoppling' + last_connected: 'Senast uppkoppling', + input_slot: 'Ingång: {{input_slot}}' }, delete_source_status: { delete_stream: 'Radera ström', @@ -65,14 +66,17 @@ export const sv = { }, production_configuration: 'Produktionskonfiguration', production: { - add_source: 'Lägg till källa', + add_source: 'Lägg till ingång', select_preset: 'Välj produktionsmall', clear_selection: 'Rensa val', started: 'Produktion startad: {{name}}', failed: 'Start av produktion misslyckades: {{name}}', stopped: 'Produktion stoppad: {{name}}', stop_failed: 'Stopp av produktion misslyckades: {{name}}', - missing_multiview: 'Saknar referens till en multiview i valt preset' + missing_multiview: 'Saknar referens till en multiview i valt preset', + source: 'Källa', + add: 'Lägg till', + add_other_source_type: 'Lägg till annan källtyp' }, create_new: 'Skapa ny', default_prod_placeholder: 'Min Nya Konfiguration', diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index e59afa4a..1aec774f 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,6 +1,7 @@ import { ObjectId, WithId } from 'mongodb'; export type SourceType = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; +export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { height?: number; width?: number; @@ -16,7 +17,7 @@ export type AudioStream = { export type Numbers = number | number[]; export interface Source { - _id?: ObjectId; + _id?: ObjectId | string; status: SourceStatus; name: string; type: SourceType; @@ -34,6 +35,7 @@ export interface Source { export interface SourceReference { _id: string; + type: Type; label: string; stream_uuids?: string[]; input_slot: number; diff --git a/src/middleware.ts b/src/middleware.ts index 7724e3b7..cac08478 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -41,4 +41,4 @@ export default withAuth(function middleware(req) { } }); -export const config = { matcher: ['/', '/((?!api|images).*)/'] }; +export const config = { matcher: ['/', '/((?!api|images|html_input).*)/'] }; From b9f4c4c9d06c5a79ed64eda703213921806a606b Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 05/56] WIP! feat: add media & html websocket connections --- src/api/manager/workflow.ts | 16 ++++++++++++++++ src/interfaces/production.ts | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 8edc5a9e..f7a3da04 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -77,6 +77,10 @@ async function connectIngestSources( let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; + console.log('connectIngestSources - productionSettings', productionSettings); + console.log('connectIngestSources - sources', sources); + console.log('connectIngestSources - usedPorts', usedPorts); + for (const source of sources) { input_slot = input_slot + 1; const ingestUuid = await getUuidFromIngestName( @@ -460,6 +464,8 @@ export async function startProduction( await initDedicatedPorts(); + console.log('startProduction - production', production); + let streams: SourceToPipelineStream[] = []; // Try to setup streams from ingest(s) to pipeline(s) start try { @@ -479,6 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); + console.log('startProduction - production', production); // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket @@ -496,11 +503,13 @@ export async function startProduction( throw "Can't get source!"; }); + console.log('startProduction - production', production); // Lookup pipeline UUIDs from pipeline names and insert to production_settings await insertPipelineUuid(production_settings).catch((error) => { throw error; }); + console.log('startProduction - production', production); // Fetch expanded pipeline objects from Ateliere Live const pipelinesToUsePromises = production_settings.pipelines.map( (pipeline) => { @@ -508,11 +517,13 @@ export async function startProduction( } ); const pipelinesToUse = await Promise.all(pipelinesToUsePromises); + console.log('startProduction - pipelinesToUse', pipelinesToUse); // Check if pipelines are already in use by another production const hasAlreadyUsedPipeline = pipelinesToUse.filter((pipeline) => isUsed(pipeline) ); + console.log('startProduction - production', production); if (hasAlreadyUsedPipeline.length > 0) { Log().error( @@ -524,6 +535,7 @@ export async function startProduction( (p) => p.name )}`; } + console.log('startProduction - hasAlreadyUsedPipeline', hasAlreadyUsedPipeline); const resetPipelinePromises = production_settings.pipelines.map( (pipeline) => { @@ -533,6 +545,7 @@ export async function startProduction( await Promise.all(resetPipelinePromises).catch((error) => { throw `Failed to reset pipelines: ${error}`; }); + console.log('startProduction - resetPipelinePromises', resetPipelinePromises); // Fetch all control panels from Ateliere Live const allControlPanels = await getControlPanels(); @@ -546,6 +559,7 @@ export async function startProduction( const hasAlreadyUsedControlPanel = controlPanelsToUse.filter( (controlPanel) => controlPanel.outgoing_connections.length > 0 ); + console.log('startProduction - hasAlreadyUsedControlPanel', hasAlreadyUsedControlPanel); if (hasAlreadyUsedControlPanel.length > 0) { Log().error( @@ -572,6 +586,7 @@ export async function startProduction( return pipeline.uuid; }) ); + console.log('startProduction - stopPipelines', production_settings.pipelines); streams = await connectIngestSources( production_settings, @@ -596,6 +611,7 @@ export async function startProduction( error: 'Could not setup streams: Unexpected error occured' }; } + console.log('startProduction - streams', streams); return { ok: false, value: [ diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d9655..b979a5ca 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From d1f6cbfa49077c8b17528ed69674122a8dd5203f Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 3 Sep 2024 16:57:15 +0200 Subject: [PATCH 06/56] feat: support to add media player and html sources --- src/api/ateliereLive/websocket.ts | 2 +- src/components/sourceCards/SourceCards.tsx | 1 + src/interfaces/production.ts | 14 -------------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466f..4e26e8dd 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - }; + } } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index d8615784..0b94651b 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -20,6 +20,7 @@ export default function SourceCards({ onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const referenceItems = productionSetup.sources; const [selectingText, setSelectingText] = useState(false); if (loading || !items) return null; diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5ca..eb4d9655 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 8b9d4aee85899ad3cf7715e43aa7d4fc793b98c4 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 4 Sep 2024 21:12:14 +0200 Subject: [PATCH 07/56] fix: fix drag and drop and lint --- src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/workflow.ts | 3 +-- src/components/sourceCards/SourceCards.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 4e26e8dd..6f51466f 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - } + }; } diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index f7a3da04..fa1764aa 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -485,8 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); - console.log('startProduction - production', production); - + // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 0b94651b..6cdb2919 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,7 +19,7 @@ export default function SourceCards({ onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const [items, moveItems] = useDragableItems(productionSetup.sources); const referenceItems = productionSetup.sources; const [selectingText, setSelectingText] = useState(false); From 03b6a7db4d58b98da755c90a92139e33dfca44f7 Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 08/56] WIP! feat: add media & html websocket connections --- src/interfaces/production.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d9655..b979a5ca 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From ca1f8e692eb89d95c23e31e94dc3b032b448ad11 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 3 Sep 2024 16:57:15 +0200 Subject: [PATCH 09/56] feat: support to add media player and html sources --- src/api/ateliereLive/websocket.ts | 2 +- src/app/production/[id]/page.tsx | 2 + src/components/sourceCard/SourceCard.tsx | 2 +- src/hooks/useDragableItems.ts | 75 +++++++++++++++++++++++- src/interfaces/production.ts | 14 ----- 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466f..4e26e8dd 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - }; + } } diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 0b4259df..26b8d130 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -521,11 +521,13 @@ export default function ProductionConfiguration({ params }: PageProps) { } }; + // TODO: HTML och MEDIA PLAYER KÄLLOR TAS INTE BORT const handleRemoveSource = async () => { if ( productionSetup && productionSetup.isActive && selectedSourceRef && + // Gör det här att sourcen inte tas bort ordentligt? selectedSourceRef.stream_uuids ) { const multiviews = diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 246e7c22..f2b94466 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -83,7 +83,7 @@ export default function SourceCard({
    { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 4cc3a03b..c213f189 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -41,6 +41,31 @@ export function useDragableItems( }) ); + const [refItems, setRefItems] = useState( + sources.flatMap((ref) => { + return {... ref} + }) + ); + + const [allItems, setAllItems] = useState<(SourceReference | ISource)[]>( + sources.flatMap((ref) => { + if (ref.type === 'ingest_source') { + const source = inventorySources.get(ref._id); + if (!source) return []; + return { + ...source, + _id: ref._id, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + }; + } else { + return {...ref} + } + }) + ) + useEffect(() => { const updatedItems = sources.map((ref) => { const source = inventorySources.get(ref._id); @@ -67,6 +92,52 @@ export function useDragableItems( setItems(updatedItems); }, [sources, inventorySources]); + useEffect(() => { + setAllItems( + sources.flatMap((ref) => { + if (ref.type === 'ingest_source') { + const source = inventorySources.get(ref._id); + if (!source) return []; + return { + ...source, + _id: ref._id, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + }; + } else { + return {...ref} + } + }) + ) + }) + + // useEffect(() => { + // setIngestSources( + // sources.flatMap((ref) => { + // const source = inventorySources.get(ref._id); + // if (!source) return []; + // return { + // ...source, + // _id: ref._id, + // label: ref.label, + // input_slot: ref.input_slot, + // stream_uuids: ref.stream_uuids, + // src: getSourceThumbnail(source), + // }; + // }) + // ); + // }, [sources, inventorySources]); + + // useEffect(() => { + // setRefSources(sources.filter((ref) => ref.type !== 'ingest_source')); + // }, [sources]) + + // useEffect(() => { + // setAllSources([...refSources, ...ingestSources]); + // }, [refSources, ingestSources]); + const moveItem = (originId: string, destinationId: string) => { const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( @@ -88,5 +159,5 @@ export function useDragableItems( setItems(updatedItems); }; - return [items, moveItem, loading]; -} + return [allItems, moveItems, loading]; +} \ No newline at end of file diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5ca..eb4d9655 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From ca856ef95394804823e2811b530b2ac24b4747b4 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 4 Sep 2024 21:12:14 +0200 Subject: [PATCH 10/56] fix: fix drag and drop and lint --- src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/workflow.ts | 2 +- src/components/sourceCards/SourceCards.tsx | 3 +- src/hooks/useDragableItems.ts | 73 +--------------------- 4 files changed, 4 insertions(+), 76 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 4e26e8dd..6f51466f 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - } + }; } diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index fa1764aa..6dc8ba0b 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -485,7 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); - + // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 6cdb2919..d8615784 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,8 +19,7 @@ export default function SourceCards({ onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItems] = useDragableItems(productionSetup.sources); - const referenceItems = productionSetup.sources; + const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); if (loading || !items) return null; diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index c213f189..5cf6d5a9 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -41,31 +41,6 @@ export function useDragableItems( }) ); - const [refItems, setRefItems] = useState( - sources.flatMap((ref) => { - return {... ref} - }) - ); - - const [allItems, setAllItems] = useState<(SourceReference | ISource)[]>( - sources.flatMap((ref) => { - if (ref.type === 'ingest_source') { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - _id: ref._id, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - }; - } else { - return {...ref} - } - }) - ) - useEffect(() => { const updatedItems = sources.map((ref) => { const source = inventorySources.get(ref._id); @@ -92,52 +67,6 @@ export function useDragableItems( setItems(updatedItems); }, [sources, inventorySources]); - useEffect(() => { - setAllItems( - sources.flatMap((ref) => { - if (ref.type === 'ingest_source') { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - _id: ref._id, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - }; - } else { - return {...ref} - } - }) - ) - }) - - // useEffect(() => { - // setIngestSources( - // sources.flatMap((ref) => { - // const source = inventorySources.get(ref._id); - // if (!source) return []; - // return { - // ...source, - // _id: ref._id, - // label: ref.label, - // input_slot: ref.input_slot, - // stream_uuids: ref.stream_uuids, - // src: getSourceThumbnail(source), - // }; - // }) - // ); - // }, [sources, inventorySources]); - - // useEffect(() => { - // setRefSources(sources.filter((ref) => ref.type !== 'ingest_source')); - // }, [sources]) - - // useEffect(() => { - // setAllSources([...refSources, ...ingestSources]); - // }, [refSources, ingestSources]); - const moveItem = (originId: string, destinationId: string) => { const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( @@ -160,4 +89,4 @@ export function useDragableItems( }; return [allItems, moveItems, loading]; -} \ No newline at end of file +} From 6b5ee1196bdbc1517bb1b9e815db67ca0e4adb27 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 09:24:02 +0200 Subject: [PATCH 11/56] feat: add additional filtering --- src/api/ateliereLive/ingest.ts | 19 ++++++ .../manager/sources/resources/[id]/route.ts | 21 +++++++ .../api/manager/sources/resources/route.ts | 18 ++++++ src/components/filter/FilterOptions.tsx | 1 + src/hooks/sources/useResources.tsx | 59 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 src/app/api/manager/sources/resources/[id]/route.ts create mode 100644 src/app/api/manager/sources/resources/route.ts create mode 100644 src/hooks/sources/useResources.tsx diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index 0c7b5218..c02fa28a 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -5,6 +5,7 @@ import { } from '../../../types/ateliere-live'; import { LIVE_BASE_API_PATH } from '../../constants'; import { getAuthorizationHeader } from './utils/authheader'; +import { ResourcesSourceResponse } from '../../../types/agile-live'; // TODO: create proper cache... const INGEST_UUID_CACHE: Map = new Map(); @@ -74,6 +75,24 @@ export async function getIngests(): Promise { throw await response.json(); } +export async function getCompleteIngests(): Promise { + const response = await fetch( + new URL(LIVE_BASE_API_PATH + `/ingests?expand=true`, process.env.LIVE_URL), + { + headers: { + authorization: getAuthorizationHeader() + }, + next: { + revalidate: 0 + } + } + ); + if (response.ok) { + return response.json(); + } + throw await response.json(); +} + export async function getIngest( uuid: string ): Promise { diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts new file mode 100644 index 00000000..da6c2357 --- /dev/null +++ b/src/app/api/manager/sources/resources/[id]/route.ts @@ -0,0 +1,21 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getIngestSources } from "../../../../../../api/agileLive/ingest"; +import { isAuthenticated } from "../../../../../../api/manager/auth"; + +type Params = { + id: string; +}; + +export async function GET(request: NextRequest, { params }: { params: Params }): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + return NextResponse.json(await getIngestSources(params.id)); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }); + } +} diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts new file mode 100644 index 00000000..5a942676 --- /dev/null +++ b/src/app/api/manager/sources/resources/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { isAuthenticated } from '../../../../../api/manager/auth'; +import { getCompleteIngests } from '../../../../../api/agileLive/ingest'; + + +export async function GET(): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + return NextResponse.json(await getCompleteIngests()); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }) + } +} diff --git a/src/components/filter/FilterOptions.tsx b/src/components/filter/FilterOptions.tsx index 67798fcb..8752d44e 100644 --- a/src/components/filter/FilterOptions.tsx +++ b/src/components/filter/FilterOptions.tsx @@ -5,6 +5,7 @@ import FilterDropdown from './FilterDropdown'; import { ClickAwayListener } from '@mui/base'; import { SourceWithId } from '../../interfaces/Source'; import { FilterContext } from '../inventory/FilterContext'; +import { useResources } from '../../hooks/sources/useResources'; type FilterOptionsProps = { onFilteredSources: (sources: Map) => void; diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx new file mode 100644 index 00000000..d6b37b71 --- /dev/null +++ b/src/hooks/sources/useResources.tsx @@ -0,0 +1,59 @@ +import { + ResourcesIngestResponse, + ResourcesSourceResponse +} from '../../../types/agile-live'; +import { useState, useEffect } from 'react'; + +export function useResources() { + const [ingests, setIngests] = useState([]); + const [resources, setResources] = useState([]); + + useEffect(() => { + let isMounted = true; + + const fetchSources = async () => { + try { + const response = await fetch(`/api/manager/sources/resourceSource`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }); + + if (!response.ok) { + throw new Error('Error'); + } + + const ing = await response.json(); + if (isMounted) { + setIngests(ing); + } + } catch (e) { + console.log('ERROR'); + } + }; + fetchSources(); + + return () => { + isMounted = false; + }; + }, []); + + useEffect(() => { + if (ingests) { + for (let i = 0; i < ingests.length; i++) { + const id = ingests[i].uuid; + if (id) { + fetch(`/api/manager/resources/${id}`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }).then(async (response) => { + console.log('RESPONSE: ', response); + const sources = await response.json(); + setResources(sources); + }); + } + } + } + }, [ingests]); + + return [resources]; +} From 474d203b2952f5bf16cc23bd05a8974a8785dd00 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 11:00:58 +0200 Subject: [PATCH 12/56] fix: handle active + source filter, reduce code --- src/api/ateliereLive/ingest.ts | 1 - .../api/manager/sources/resources/route.ts | 4 +-- src/hooks/sources/useResources.tsx | 33 +++++++------------ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index c02fa28a..c9420831 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -5,7 +5,6 @@ import { } from '../../../types/ateliere-live'; import { LIVE_BASE_API_PATH } from '../../constants'; import { getAuthorizationHeader } from './utils/authheader'; -import { ResourcesSourceResponse } from '../../../types/agile-live'; // TODO: create proper cache... const INGEST_UUID_CACHE: Map = new Map(); diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts index 5a942676..8932a0df 100644 --- a/src/app/api/manager/sources/resources/route.ts +++ b/src/app/api/manager/sources/resources/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { isAuthenticated } from '../../../../../api/manager/auth'; -import { getCompleteIngests } from '../../../../../api/agileLive/ingest'; +import { getIngests } from '../../../../../api/agileLive/ingest'; export async function GET(): Promise { @@ -11,7 +11,7 @@ export async function GET(): Promise { } try { - return NextResponse.json(await getCompleteIngests()); + return NextResponse.json(await getIngests()); } catch (e) { return new NextResponse(e?.toString(), { status: 404 }) } diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx index d6b37b71..607c34a4 100644 --- a/src/hooks/sources/useResources.tsx +++ b/src/hooks/sources/useResources.tsx @@ -1,37 +1,27 @@ import { - ResourcesIngestResponse, - ResourcesSourceResponse + ResourcesSourceResponse, + ResourcesCompactIngestResponse } from '../../../types/agile-live'; import { useState, useEffect } from 'react'; export function useResources() { - const [ingests, setIngests] = useState([]); + const [ingests, setIngests] = useState([]); const [resources, setResources] = useState([]); useEffect(() => { let isMounted = true; - const fetchSources = async () => { - try { - const response = await fetch(`/api/manager/sources/resourceSource`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }); - - if (!response.ok) { - throw new Error('Error'); - } - + const getIngests = async () => + await fetch(`/api/manager/sources/resources`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }).then(async (response) => { const ing = await response.json(); if (isMounted) { setIngests(ing); } - } catch (e) { - console.log('ERROR'); - } - }; - fetchSources(); - + }); + getIngests(); return () => { isMounted = false; }; @@ -42,11 +32,10 @@ export function useResources() { for (let i = 0; i < ingests.length; i++) { const id = ingests[i].uuid; if (id) { - fetch(`/api/manager/resources/${id}`, { + fetch(`/api/manager/sources/resources/${id}`, { method: 'GET', headers: [['x-api-key', `Bearer apisecretkey`]] }).then(async (response) => { - console.log('RESPONSE: ', response); const sources = await response.json(); setResources(sources); }); From 77ba4e1abd704aeaff7b9d9b65f3456f0a3e74c7 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 11:39:23 +0200 Subject: [PATCH 13/56] fix: linting --- .../manager/sources/resources/[id]/route.ts | 21 +++++++++++-------- .../api/manager/sources/resources/route.ts | 19 ++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts index da6c2357..e1b68b78 100644 --- a/src/app/api/manager/sources/resources/[id]/route.ts +++ b/src/app/api/manager/sources/resources/[id]/route.ts @@ -1,18 +1,21 @@ -import { NextResponse, NextRequest } from "next/server"; -import { getIngestSources } from "../../../../../../api/agileLive/ingest"; -import { isAuthenticated } from "../../../../../../api/manager/auth"; +import { NextResponse, NextRequest } from 'next/server'; +import { getIngestSources } from '../../../../../../api/agileLive/ingest'; +import { isAuthenticated } from '../../../../../../api/manager/auth'; type Params = { id: string; }; -export async function GET(request: NextRequest, { params }: { params: Params }): Promise { +export async function GET( + request: NextRequest, + { params }: { params: Params } +): Promise { if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + try { return NextResponse.json(await getIngestSources(params.id)); } catch (e) { diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts index 8932a0df..64200693 100644 --- a/src/app/api/manager/sources/resources/route.ts +++ b/src/app/api/manager/sources/resources/route.ts @@ -2,17 +2,16 @@ import { NextResponse } from 'next/server'; import { isAuthenticated } from '../../../../../api/manager/auth'; import { getIngests } from '../../../../../api/agileLive/ingest'; - export async function GET(): Promise { if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + try { - return NextResponse.json(await getIngests()); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }) - } + return NextResponse.json(await getIngests()); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }); + } } From aaf3069e8d6db5e3276b690245fb112b8526b55d Mon Sep 17 00:00:00 2001 From: Saelmala Date: Mon, 26 Aug 2024 10:40:02 +0200 Subject: [PATCH 14/56] fix: add ingest type to database --- src/api/manager/job/syncInventory.ts | 3 +- .../manager/sources/resources/[id]/route.ts | 24 ---------- .../api/manager/sources/resources/route.ts | 17 ------- src/components/filter/FilterOptions.tsx | 1 - src/hooks/sources/useResources.tsx | 48 ------------------- 5 files changed, 2 insertions(+), 91 deletions(-) delete mode 100644 src/app/api/manager/sources/resources/[id]/route.ts delete mode 100644 src/app/api/manager/sources/resources/route.ts delete mode 100644 src/hooks/sources/useResources.tsx diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 2e813d0a..383e5c11 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -86,7 +86,8 @@ export async function runSyncInventory() { const apiSource = apiSources.find((source) => { return ( source.ingest_name === inventorySource.ingest_name && - source.ingest_source_name === inventorySource.ingest_source_name + source.ingest_source_name === inventorySource.ingest_source_name && + source.ingest_type === inventorySource.type ); }); if (!apiSource) { diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts deleted file mode 100644 index e1b68b78..00000000 --- a/src/app/api/manager/sources/resources/[id]/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextResponse, NextRequest } from 'next/server'; -import { getIngestSources } from '../../../../../../api/agileLive/ingest'; -import { isAuthenticated } from '../../../../../../api/manager/auth'; - -type Params = { - id: string; -}; - -export async function GET( - request: NextRequest, - { params }: { params: Params } -): Promise { - if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - - try { - return NextResponse.json(await getIngestSources(params.id)); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }); - } -} diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts deleted file mode 100644 index 64200693..00000000 --- a/src/app/api/manager/sources/resources/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from 'next/server'; -import { isAuthenticated } from '../../../../../api/manager/auth'; -import { getIngests } from '../../../../../api/agileLive/ingest'; - -export async function GET(): Promise { - if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - - try { - return NextResponse.json(await getIngests()); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }); - } -} diff --git a/src/components/filter/FilterOptions.tsx b/src/components/filter/FilterOptions.tsx index 8752d44e..67798fcb 100644 --- a/src/components/filter/FilterOptions.tsx +++ b/src/components/filter/FilterOptions.tsx @@ -5,7 +5,6 @@ import FilterDropdown from './FilterDropdown'; import { ClickAwayListener } from '@mui/base'; import { SourceWithId } from '../../interfaces/Source'; import { FilterContext } from '../inventory/FilterContext'; -import { useResources } from '../../hooks/sources/useResources'; type FilterOptionsProps = { onFilteredSources: (sources: Map) => void; diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx deleted file mode 100644 index 607c34a4..00000000 --- a/src/hooks/sources/useResources.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - ResourcesSourceResponse, - ResourcesCompactIngestResponse -} from '../../../types/agile-live'; -import { useState, useEffect } from 'react'; - -export function useResources() { - const [ingests, setIngests] = useState([]); - const [resources, setResources] = useState([]); - - useEffect(() => { - let isMounted = true; - - const getIngests = async () => - await fetch(`/api/manager/sources/resources`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }).then(async (response) => { - const ing = await response.json(); - if (isMounted) { - setIngests(ing); - } - }); - getIngests(); - return () => { - isMounted = false; - }; - }, []); - - useEffect(() => { - if (ingests) { - for (let i = 0; i < ingests.length; i++) { - const id = ingests[i].uuid; - if (id) { - fetch(`/api/manager/sources/resources/${id}`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }).then(async (response) => { - const sources = await response.json(); - setResources(sources); - }); - } - } - } - }, [ingests]); - - return [resources]; -} From 93f9048815193464733eebbbf944251d6738ef86 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 28 Aug 2024 17:14:15 +0200 Subject: [PATCH 15/56] feat: first commit of source-delete, with basic styling and fetches --- .../inventory/editView/EditView.tsx | 4 ++- src/hooks/sources/useDeleteSource.tsx | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/hooks/sources/useDeleteSource.tsx diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 80d498a3..df237b7d 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,12 +1,13 @@ import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; import { IconExclamationCircle } from '@tabler/icons-react'; +import { useDeleteSource } from '../../../hooks/sources/useDeleteSource'; export default function EditView({ source, @@ -20,6 +21,7 @@ export default function EditView({ removeInventorySource: (source: SourceWithId) => void; }) { const [loaded, setLoaded] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); const src = useMemo(() => getSourceThumbnail(source), [source]); return ( diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx new file mode 100644 index 00000000..e7a33f23 --- /dev/null +++ b/src/hooks/sources/useDeleteSource.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { SourceWithId } from '../../interfaces/Source'; + +export function useDeleteSource(source: SourceWithId | null) { + const [loading, setLoading] = useState(true); + const [deleteComplete, setDeleteComplete] = useState(false); + + useEffect(() => { + if (source && source.status === 'gone') { + setLoading(true); + setDeleteComplete(false); + // Source to be deleted: + console.log('source._id', source); + fetch(`/api/manager/inventory/${source._id}`, { + method: 'DELETE', + // TODO: Implement api key + headers: [['x-api-key', `Bearer apisecretkey`]] + }) + .then((response) => { + if (response.ok) { + setLoading(false); + setDeleteComplete(true); + } + }) + .catch((e) => { + console.log(`Failed to delete source-item: ${e}`); + }); + } else { + setLoading(false); + setDeleteComplete(false); + } + }, [source]); + return [loading, deleteComplete]; +} From 0acd37ae027b1ab2adbff0c470964f774fc954ba Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:39:22 +0200 Subject: [PATCH 16/56] feat: added creation-date and function to set correct status when getting sources from api --- src/api/manager/job/syncInventory.ts | 3 ++- src/interfaces/Source.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 383e5c11..774c9d7c 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -41,7 +41,8 @@ async function getSourcesFromAPI(): Promise { audio_stream: { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate - } + }, + createdAt: new Date() } satisfies SourceWithoutLastConnected) ); } diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 1aec774f..11020302 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -30,6 +30,7 @@ export interface Source { ingest_type: string; video_stream: VideoStream; audio_stream: AudioStream; + createdAt: Date; lastConnected: Date; } From 9d19b685e645aa497c13935c47e389d37f9ddbd4 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:41:14 +0200 Subject: [PATCH 17/56] feat: updated server-fetch to be put, not possible to delete --- src/hooks/sources/useDeleteSource.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx index e7a33f23..d67e9ae7 100644 --- a/src/hooks/sources/useDeleteSource.tsx +++ b/src/hooks/sources/useDeleteSource.tsx @@ -9,18 +9,22 @@ export function useDeleteSource(source: SourceWithId | null) { if (source && source.status === 'gone') { setLoading(true); setDeleteComplete(false); - // Source to be deleted: - console.log('source._id', source); + fetch(`/api/manager/inventory/${source._id}`, { - method: 'DELETE', + method: 'PUT', // TODO: Implement api key headers: [['x-api-key', `Bearer apisecretkey`]] }) .then((response) => { - if (response.ok) { + if (!response.ok) { setLoading(false); setDeleteComplete(true); + return response.text().then((message) => { + throw new Error(`Error ${response.status}: ${message}`); + }); } + setLoading(false); + setDeleteComplete(true); }) .catch((e) => { console.log(`Failed to delete source-item: ${e}`); From ae74ea99d8aa3a94d65740e4dcfbd3a3121eb6d3 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:45:20 +0200 Subject: [PATCH 18/56] feat: moved logic out of component to inventory-page and added purge-flag --- src/components/inventory/Inventory.tsx | 3 ++- src/components/inventory/editView/EditView.tsx | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index dacb2623..280a0990 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -8,6 +8,7 @@ import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; import FilterContext from './FilterContext'; +import { useDeleteSource } from '../../hooks/sources/useDeleteSource'; import styles from './Inventory.module.scss'; export default function Inventory() { @@ -82,7 +83,7 @@ export default function Inventory() {
      - {getSourcesToDisplay(filteredSources)} + {loading ? '' : getSourcesToDisplay(filteredSources)}
  • diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index df237b7d..80d498a3 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,13 +1,12 @@ import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; import { IconExclamationCircle } from '@tabler/icons-react'; -import { useDeleteSource } from '../../../hooks/sources/useDeleteSource'; export default function EditView({ source, @@ -21,7 +20,6 @@ export default function EditView({ removeInventorySource: (source: SourceWithId) => void; }) { const [loaded, setLoaded] = useState(false); - const [itemToDelete, setItemToDelete] = useState(null); const src = useMemo(() => getSourceThumbnail(source), [source]); return ( From ebb2d201c6d934b696dd0fbda6a1868370dcf389 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 17:07:12 +0200 Subject: [PATCH 19/56] fix: simplified the delete-source-hook --- src/hooks/sources/useDeleteSource.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx index d67e9ae7..29c4bb14 100644 --- a/src/hooks/sources/useDeleteSource.tsx +++ b/src/hooks/sources/useDeleteSource.tsx @@ -1,13 +1,14 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; +import { CallbackHook } from '../types'; -export function useDeleteSource(source: SourceWithId | null) { - const [loading, setLoading] = useState(true); +export function useDeleteSource(): CallbackHook< + (source: SourceWithId) => void +> { const [deleteComplete, setDeleteComplete] = useState(false); - useEffect(() => { + const setSourceToPurge = (source: SourceWithId) => { if (source && source.status === 'gone') { - setLoading(true); setDeleteComplete(false); fetch(`/api/manager/inventory/${source._id}`, { @@ -17,22 +18,19 @@ export function useDeleteSource(source: SourceWithId | null) { }) .then((response) => { if (!response.ok) { - setLoading(false); setDeleteComplete(true); return response.text().then((message) => { throw new Error(`Error ${response.status}: ${message}`); }); } - setLoading(false); setDeleteComplete(true); }) .catch((e) => { console.log(`Failed to delete source-item: ${e}`); }); } else { - setLoading(false); setDeleteComplete(false); } - }, [source]); - return [loading, deleteComplete]; + }; + return [setSourceToPurge, deleteComplete]; } From 89d6ec88ffd21ad209da1bcf20a2cebb9f2182b2 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 07:20:20 +0200 Subject: [PATCH 20/56] fix: rebase to main --- src/api/manager/job/syncInventory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 774c9d7c..cdfc0cba 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -42,7 +42,7 @@ async function getSourcesFromAPI(): Promise { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate }, - createdAt: new Date() + createdAt: new Date() } satisfies SourceWithoutLastConnected) ); } From f3ecb17a3b7dcc15c3ec2cbb264712773f1bf336 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 12:35:40 +0200 Subject: [PATCH 21/56] fix: removed created-at and used last-connected instead --- src/api/manager/job/syncInventory.ts | 6 ++---- src/interfaces/Source.ts | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index cdfc0cba..2e813d0a 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -41,8 +41,7 @@ async function getSourcesFromAPI(): Promise { audio_stream: { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate - }, - createdAt: new Date() + } } satisfies SourceWithoutLastConnected) ); } @@ -87,8 +86,7 @@ export async function runSyncInventory() { const apiSource = apiSources.find((source) => { return ( source.ingest_name === inventorySource.ingest_name && - source.ingest_source_name === inventorySource.ingest_source_name && - source.ingest_type === inventorySource.type + source.ingest_source_name === inventorySource.ingest_source_name ); }); if (!apiSource) { diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 11020302..1aec774f 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -30,7 +30,6 @@ export interface Source { ingest_type: string; video_stream: VideoStream; audio_stream: AudioStream; - createdAt: Date; lastConnected: Date; } From 88ff05183b6b53a870cf9e7f9ea8e4ccd6f41f54 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 12:47:29 +0200 Subject: [PATCH 22/56] fix: added better error-handling and made more logical naming of code --- src/components/inventory/Inventory.tsx | 1 - src/hooks/sources/useDeleteSource.tsx | 36 ------------------- src/hooks/sources/useSetSourceToPerge.tsx | 44 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 37 deletions(-) delete mode 100644 src/hooks/sources/useDeleteSource.tsx create mode 100644 src/hooks/sources/useSetSourceToPerge.tsx diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index 280a0990..c7c76709 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -8,7 +8,6 @@ import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; import FilterContext from './FilterContext'; -import { useDeleteSource } from '../../hooks/sources/useDeleteSource'; import styles from './Inventory.module.scss'; export default function Inventory() { diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx deleted file mode 100644 index 29c4bb14..00000000 --- a/src/hooks/sources/useDeleteSource.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; -import { CallbackHook } from '../types'; - -export function useDeleteSource(): CallbackHook< - (source: SourceWithId) => void -> { - const [deleteComplete, setDeleteComplete] = useState(false); - - const setSourceToPurge = (source: SourceWithId) => { - if (source && source.status === 'gone') { - setDeleteComplete(false); - - fetch(`/api/manager/inventory/${source._id}`, { - method: 'PUT', - // TODO: Implement api key - headers: [['x-api-key', `Bearer apisecretkey`]] - }) - .then((response) => { - if (!response.ok) { - setDeleteComplete(true); - return response.text().then((message) => { - throw new Error(`Error ${response.status}: ${message}`); - }); - } - setDeleteComplete(true); - }) - .catch((e) => { - console.log(`Failed to delete source-item: ${e}`); - }); - } else { - setDeleteComplete(false); - } - }; - return [setSourceToPurge, deleteComplete]; -} diff --git a/src/hooks/sources/useSetSourceToPerge.tsx b/src/hooks/sources/useSetSourceToPerge.tsx new file mode 100644 index 00000000..37ce4712 --- /dev/null +++ b/src/hooks/sources/useSetSourceToPerge.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { SourceWithId } from '../../interfaces/Source'; +import { CallbackHook } from '../types'; +import { Log } from '../../api/logger'; + +export function useSetSourceToPerge(): CallbackHook< + (source: SourceWithId) => void +> { + const [reloadList, setReloadList] = useState(false); + + const removeInventorySource = (source: SourceWithId) => { + if (source && source.status === 'gone') { + setReloadList(false); + + fetch(`/api/manager/inventory/${source._id}`, { + method: 'PUT', + // TODO: Implement api key + headers: [['x-api-key', `Bearer apisecretkey`]] + }) + .then((response) => { + if (!response.ok) { + setReloadList(true); + Log().error( + `Failed to set ${source.name} with id: ${source._id} to purge` + ); + } else { + console.log( + `${source.name} with id: ${source._id} is set to purge` + ); + } + setReloadList(true); + }) + .catch((e) => { + Log().error( + `Failed to set ${source.name} with id: ${source._id} to purge: ${e}` + ); + throw `Failed to set ${source.name} with id: ${source._id} to purge: ${e}`; + }); + } else { + setReloadList(false); + } + }; + return [removeInventorySource, reloadList]; +} From 824d700c20c1b3134ad2befd3325958cecef6b50 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 13:59:59 +0200 Subject: [PATCH 23/56] fix: corrected misspelling --- src/hooks/sources/useSetSourceToPerge.tsx | 44 ----------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/hooks/sources/useSetSourceToPerge.tsx diff --git a/src/hooks/sources/useSetSourceToPerge.tsx b/src/hooks/sources/useSetSourceToPerge.tsx deleted file mode 100644 index 37ce4712..00000000 --- a/src/hooks/sources/useSetSourceToPerge.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; -import { CallbackHook } from '../types'; -import { Log } from '../../api/logger'; - -export function useSetSourceToPerge(): CallbackHook< - (source: SourceWithId) => void -> { - const [reloadList, setReloadList] = useState(false); - - const removeInventorySource = (source: SourceWithId) => { - if (source && source.status === 'gone') { - setReloadList(false); - - fetch(`/api/manager/inventory/${source._id}`, { - method: 'PUT', - // TODO: Implement api key - headers: [['x-api-key', `Bearer apisecretkey`]] - }) - .then((response) => { - if (!response.ok) { - setReloadList(true); - Log().error( - `Failed to set ${source.name} with id: ${source._id} to purge` - ); - } else { - console.log( - `${source.name} with id: ${source._id} is set to purge` - ); - } - setReloadList(true); - }) - .catch((e) => { - Log().error( - `Failed to set ${source.name} with id: ${source._id} to purge: ${e}` - ); - throw `Failed to set ${source.name} with id: ${source._id} to purge: ${e}`; - }); - } else { - setReloadList(false); - } - }; - return [removeInventorySource, reloadList]; -} From 0389ac5b01b7abdab9d38e56c3857a9d350c4473 Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 24/56] WIP! feat: add media & html websocket connections --- src/interfaces/production.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d9655..b979a5ca 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 1f579c58bf77c00cb01d0db2b3fff1091d829ef4 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 15:14:06 +0200 Subject: [PATCH 25/56] fix: able to start prod w wevsocket sources --- .env.sample | 1 + src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/productions.ts | 28 ++++++- src/api/manager/sources.ts | 6 +- src/api/manager/workflow.ts | 29 +++---- src/app/production/[id]/page.tsx | 92 ++++++++-------------- src/components/modal/AddSourceModal.tsx | 1 - src/components/sourceCards/SourceCards.tsx | 3 + src/hooks/productions.ts | 4 +- src/hooks/sources/useAddSource.tsx | 39 +++++++++ src/hooks/sources/useSources.tsx | 6 +- src/hooks/useGetFirstEmptySlot.ts | 37 +++++++++ src/interfaces/Source.ts | 2 +- 13 files changed, 158 insertions(+), 92 deletions(-) create mode 100644 src/hooks/sources/useAddSource.tsx create mode 100644 src/hooks/useGetFirstEmptySlot.ts diff --git a/.env.sample b/.env.sample index da9f7260..3ff9058c 100644 --- a/.env.sample +++ b/.env.sample @@ -4,6 +4,7 @@ MONGODB_URI=${MONGODB_URI:-mongodb://api:password@localhost:27017/live-gui} # Ateliere Live System Controlleer LIVE_URL=${LIVE_URL:-https://localhost:8080} LIVE_CREDENTIALS=${LIVE_CREDENTIALS:-admin:admin} +CONTROL_PANEL_WS==${} # This ENV variable disables SSL Verification, use if the above LIVE_URL doesn't have a proper certificate NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-1} diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466f..df8b47af 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -2,7 +2,7 @@ import WebSocket from 'ws'; function createWebSocket(): Promise { return new Promise((resolve, reject) => { - const ws = new WebSocket(`ws://${process.env.AGILE_WEBSOCKET}`); + const ws = new WebSocket(`ws://${process.env.CONTROL_PANEL_WS}`); ws.on('error', reject); ws.on('open', () => { // const send = ws.send.bind(ws); diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index 714d822d..e3647767 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -2,7 +2,6 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Production, ProductionWithId } from '../../interfaces/production'; import { Log } from '../logger'; -import { SourceReference, Type } from '../../interfaces/Source'; export async function getProductions(): Promise { const db = await getDatabase(); @@ -29,14 +28,29 @@ export async function setProductionsIsActiveFalse(): Promise< export async function putProduction( id: string, production: Production -): Promise { +): Promise { const db = await getDatabase(); + const newSourceId = new ObjectId().toString(); + + const sources = production.sources + ? production.sources.flatMap((singleSource) => { + return singleSource._id + ? singleSource + : { + _id: newSourceId, + type: singleSource.type, + label: singleSource.label, + input_slot: singleSource.input_slot + }; + }) + : []; + await db.collection('productions').findOneAndReplace( { _id: new ObjectId(id) }, { name: production.name, isActive: production.isActive, - sources: production.sources, + sources: sources, production_settings: production.production_settings } ); @@ -44,6 +58,14 @@ export async function putProduction( if (!production.isActive) { deleteMonitoring(db, id); } + + return { + _id: new ObjectId(id).toString(), + name: production.name, + isActive: production.isActive, + sources: sources, + production_settings: production.production_settings + }; } export async function postProduction(data: Production): Promise { diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index a54fb38a..853f43ce 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -1,6 +1,6 @@ import inventory from './mocks/inventory.json'; import { Source } from '../../interfaces/Source'; -import { ObjectId, OptionalId } from 'mongodb'; +import { ObjectId, OptionalId, WithId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -21,7 +21,9 @@ export async function getSources() { const db = await getDatabase(); return await db.collection('inventory').find().toArray(); } -export async function getSourcesByIds(_ids: string[]) { +export async function getSourcesByIds( + _ids: string[] +): Promise[]> { const db = await getDatabase().catch(() => { throw new Error("Can't connect to Database"); }); diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 6dc8ba0b..89808acb 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -1,3 +1,4 @@ +import { SourceWithId } from './../../interfaces/Source'; import { Production, ProductionSettings, @@ -35,7 +36,7 @@ import { ResourcesSenderNetworkEndpoint } from '../../../types/ateliere-live'; import { getSourcesByIds } from './sources'; -import { SourceWithId, SourceToPipelineStream } from '../../interfaces/Source'; +import { SourceToPipelineStream } from '../../interfaces/Source'; import { getAvailablePortsForIngest, getCurrentlyUsedPorts, @@ -77,10 +78,6 @@ async function connectIngestSources( let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; - console.log('connectIngestSources - productionSettings', productionSettings); - console.log('connectIngestSources - sources', sources); - console.log('connectIngestSources - usedPorts', usedPorts); - for (const source of sources) { input_slot = input_slot + 1; const ingestUuid = await getUuidFromIngestName( @@ -464,8 +461,6 @@ export async function startProduction( await initDedicatedPorts(); - console.log('startProduction - production', production); - let streams: SourceToPipelineStream[] = []; // Try to setup streams from ingest(s) to pipeline(s) start try { @@ -478,7 +473,6 @@ export async function startProduction( const mediaPlayerSources = production.sources.filter( (source) => source.type === 'mediaplayer' ); - htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); mediaPlayerSources.map((source) => controlPanelWS.createMediaplayer(source.input_slot) @@ -490,7 +484,10 @@ export async function startProduction( // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( production.sources - .filter((source) => source._id !== undefined) + .filter( + (source) => + source._id !== undefined && source.type === 'ingest_source' + ) .map((source) => { return source._id!.toString(); }) @@ -502,13 +499,11 @@ export async function startProduction( throw "Can't get source!"; }); - console.log('startProduction - production', production); // Lookup pipeline UUIDs from pipeline names and insert to production_settings await insertPipelineUuid(production_settings).catch((error) => { throw error; }); - console.log('startProduction - production', production); // Fetch expanded pipeline objects from Ateliere Live const pipelinesToUsePromises = production_settings.pipelines.map( (pipeline) => { @@ -516,13 +511,11 @@ export async function startProduction( } ); const pipelinesToUse = await Promise.all(pipelinesToUsePromises); - console.log('startProduction - pipelinesToUse', pipelinesToUse); // Check if pipelines are already in use by another production const hasAlreadyUsedPipeline = pipelinesToUse.filter((pipeline) => isUsed(pipeline) ); - console.log('startProduction - production', production); if (hasAlreadyUsedPipeline.length > 0) { Log().error( @@ -534,7 +527,6 @@ export async function startProduction( (p) => p.name )}`; } - console.log('startProduction - hasAlreadyUsedPipeline', hasAlreadyUsedPipeline); const resetPipelinePromises = production_settings.pipelines.map( (pipeline) => { @@ -544,7 +536,6 @@ export async function startProduction( await Promise.all(resetPipelinePromises).catch((error) => { throw `Failed to reset pipelines: ${error}`; }); - console.log('startProduction - resetPipelinePromises', resetPipelinePromises); // Fetch all control panels from Ateliere Live const allControlPanels = await getControlPanels(); @@ -558,7 +549,6 @@ export async function startProduction( const hasAlreadyUsedControlPanel = controlPanelsToUse.filter( (controlPanel) => controlPanel.outgoing_connections.length > 0 ); - console.log('startProduction - hasAlreadyUsedControlPanel', hasAlreadyUsedControlPanel); if (hasAlreadyUsedControlPanel.length > 0) { Log().error( @@ -585,7 +575,6 @@ export async function startProduction( return pipeline.uuid; }) ); - console.log('startProduction - stopPipelines', production_settings.pipelines); streams = await connectIngestSources( production_settings, @@ -610,7 +599,6 @@ export async function startProduction( error: 'Could not setup streams: Unexpected error occured' }; } - console.log('startProduction - streams', streams); return { ok: false, value: [ @@ -774,8 +762,9 @@ export async function startProduction( ); return { ...source, - stream_uuids: streamsForSource?.map((s) => s.stream_uuid), - input_slot: streamsForSource[0].input_slot + stream_uuids: + streamsForSource?.map((s) => s.stream_uuid) || undefined, + input_slot: source.input_slot }; }), isActive: true diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 26b8d130..a96d3cb6 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,4 +1,5 @@ 'use client'; + import React, { useEffect, useState, KeyboardEvent } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; import SourceListItem from '../../../components/sourceListItem/SourceListItem'; @@ -42,7 +43,8 @@ import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { useMultiviews } from '../../../hooks/multiviews'; -import { v4 as uuidv4 } from 'uuid'; +import { useAddSource } from '../../../hooks/sources/useAddSource'; +import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; import { Select } from '../../../components/select/Select'; export default function ProductionConfiguration({ params }: PageProps) { @@ -95,6 +97,10 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + // Create source + const [firstEmptySlot] = useGetFirstEmptySlot(); + const [addSource] = useAddSource(); + const isAddButtonDisabled = selectedValue !== 'HTML' && selectedValue !== 'Media Player'; @@ -103,29 +109,24 @@ export default function ProductionConfiguration({ params }: PageProps) { refreshControlPanels(); }, [productionSetup?.isActive]); - // TODO: Väldigt lik den för ingest_source --> ändra?? const addSourceToProduction = (type: Type) => { - const newSource: SourceReference = { - _id: uuidv4(), + const input: SourceReference = { type: type, label: type === 'html' ? 'HTML Input' : 'Media Player Source', - input_slot: getFirstEmptySlot() + input_slot: firstEmptySlot(productionSetup) }; - setSourceReferenceToAdd(newSource); - - if (productionSetup) { - const updatedSetup = addSetupItem(newSource, productionSetup); + if (!productionSetup) return; + addSource(input, productionSetup).then((updatedSetup) => { if (!updatedSetup) return; + setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - refreshProduction(); - setAddSourceModal(false); - setSourceReferenceToAdd(undefined); - }); - setAddSourceStatus(undefined); - } + refreshProduction(); + setAddSourceModal(false); + setSelectedSource(undefined); + }); + setAddSourceStatus(undefined); }; const setSelectedControlPanel = (controlPanel: string[]) => { @@ -403,6 +404,8 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } + + // Adding source to a production, both in setup-mode and in live-mode function getSourcesToDisplay( filteredSources: Map ): React.ReactNode[] { @@ -417,23 +420,18 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(source); setAddSourceModal(true); } else if (productionSetup) { - const updatedSetup = addSetupItem( - { - _id: source._id.toString(), - type: 'ingest_source', - label: source.ingest_source_name, - input_slot: getFirstEmptySlot() - }, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); + const input: SourceReference = { + _id: source._id.toString(), + type: 'ingest_source', + label: source.ingest_source_name, + input_slot: firstEmptySlot(productionSetup) + }; + addSource(input, productionSetup).then((updatedSetup) => { + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + setAddSourceModal(false); + setSelectedSource(undefined); + }); } }} /> @@ -441,28 +439,6 @@ export default function ProductionConfiguration({ params }: PageProps) { }); } - const getFirstEmptySlot = () => { - if (!productionSetup) throw 'no_production'; - let firstEmptySlot = productionSetup.sources.length + 1; - if (productionSetup.sources.length === 0) { - return firstEmptySlot; - } - for ( - let i = 0; - i < - productionSetup.sources[productionSetup.sources.length - 1].input_slot; - i++ - ) { - if ( - !productionSetup.sources.some((source) => source.input_slot === i + 1) - ) { - firstEmptySlot = i + 1; - break; - } - } - return firstEmptySlot; - }; - const handleAddSource = async () => { setAddSourceStatus(undefined); if ( @@ -477,11 +453,10 @@ export default function ProductionConfiguration({ params }: PageProps) { ) : false) ) { - const firstEmptySlot = getFirstEmptySlot(); const result = await createStream( selectedSource, productionSetup, - firstEmptySlot ? firstEmptySlot : productionSetup.sources.length + 1 + firstEmptySlot(productionSetup) ); if (!result.ok) { if (!result.value) { @@ -503,7 +478,7 @@ export default function ProductionConfiguration({ params }: PageProps) { type: 'ingest_source', label: selectedSource.name, stream_uuids: result.value.streams.map((r) => r.stream_uuid), - input_slot: getFirstEmptySlot() + input_slot: firstEmptySlot(productionSetup) }; const updatedSetup = addSetupItem(sourceToAdd, productionSetup); if (!updatedSetup) return; @@ -521,7 +496,6 @@ export default function ProductionConfiguration({ params }: PageProps) { } }; - // TODO: HTML och MEDIA PLAYER KÄLLOR TAS INTE BORT const handleRemoveSource = async () => { if ( productionSetup && diff --git a/src/components/modal/AddSourceModal.tsx b/src/components/modal/AddSourceModal.tsx index d81c1714..236b3cb3 100644 --- a/src/components/modal/AddSourceModal.tsx +++ b/src/components/modal/AddSourceModal.tsx @@ -26,7 +26,6 @@ export function AddSourceModal({ return (
    -

    HEJ

    {t('workflow.add_source_modal', { name })}

    {status && }
    diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index d8615784..9af6ec4e 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -6,6 +6,7 @@ import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; + export default function SourceCards({ productionSetup, sourceRef, @@ -37,6 +38,8 @@ export default function SourceCards({ const gridItems = items.map((source) => { const isSource = isISource(source); + if (!source._id) return; + return ( => { + return async (id: string, production: Production): Promise => { const response = await fetch(`/api/manager/productions/${id}`, { method: 'PUT', // TODO: Implement api key @@ -45,7 +45,7 @@ export function usePutProduction() { body: JSON.stringify(production) }); if (response.ok) { - return; + return response.json(); } throw await response.text(); }; diff --git a/src/hooks/sources/useAddSource.tsx b/src/hooks/sources/useAddSource.tsx new file mode 100644 index 00000000..c04c1bb3 --- /dev/null +++ b/src/hooks/sources/useAddSource.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { addSetupItem } from '../items/addSetupItem'; +import { CallbackHook } from '../types'; +import { Production } from '../../interfaces/production'; +import { usePutProduction } from '../productions'; +import { SourceReference } from '../../interfaces/Source'; + +export function useAddSource(): CallbackHook< + ( + input: SourceReference, + productionSetup: Production + ) => Promise +> { + const [loading, setLoading] = useState(true); + const putProduction = usePutProduction(); + + const addSource = async ( + input: SourceReference, + productionSetup: Production + ) => { + const updatedSetup = addSetupItem( + { + _id: input._id ? input._id : undefined, + type: input.type, + label: input.label, + input_slot: input.input_slot + }, + productionSetup + ); + + if (!updatedSetup) return; + + const res = await putProduction(updatedSetup._id.toString(), updatedSetup); + console.log('res', res); + return res; + }; + + return [addSource, loading]; +} diff --git a/src/hooks/sources/useSources.tsx b/src/hooks/sources/useSources.tsx index 1b58418a..8b44ea29 100644 --- a/src/hooks/sources/useSources.tsx +++ b/src/hooks/sources/useSources.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; export function useSources( - deleteComplete?: boolean, + reloadList?: boolean, updatedSource?: SourceWithId ): [Map, boolean] { const [sources, setSources] = useState>( @@ -11,7 +11,7 @@ export function useSources( const [loading, setLoading] = useState(true); useEffect(() => { - if (!updatedSource || deleteComplete) { + if (!updatedSource || reloadList) { fetch('/api/manager/sources?mocked=false', { method: 'GET', // TODO: Implement api key @@ -34,6 +34,6 @@ export function useSources( } sources.set(updatedSource._id.toString(), updatedSource); setSources(new Map(sources)); - }, [updatedSource, deleteComplete]); + }, [updatedSource, reloadList]); return [sources, loading]; } diff --git a/src/hooks/useGetFirstEmptySlot.ts b/src/hooks/useGetFirstEmptySlot.ts new file mode 100644 index 00000000..8cda1821 --- /dev/null +++ b/src/hooks/useGetFirstEmptySlot.ts @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import { Production } from '../interfaces/production'; +import { CallbackHook } from './types'; + +export function useGetFirstEmptySlot(): CallbackHook< + (productionSetup?: Production | undefined) => number +> { + const [loading, setLoading] = useState(true); + + const findFirstEmptySlot = (productionSetup: Production | undefined) => { + if (!productionSetup) throw 'no_production'; + + if (productionSetup) { + let firstEmptySlotTemp = productionSetup.sources.length + 1; + if (productionSetup.sources.length === 0) { + return firstEmptySlotTemp; + } + for ( + let i = 0; + i < + productionSetup.sources[productionSetup.sources.length - 1].input_slot; + i++ + ) { + if ( + !productionSetup.sources.some((source) => source.input_slot === i + 1) + ) { + firstEmptySlotTemp = i + 1; + break; + } + } + return firstEmptySlotTemp; + } else { + return 0; + } + }; + return [findFirstEmptySlot, loading]; +} diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 1aec774f..2e9935b6 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -34,7 +34,7 @@ export interface Source { } export interface SourceReference { - _id: string; + _id?: string; type: Type; label: string; stream_uuids?: string[]; From 0f2d040cdb8c782107f60dd77f25a24252f21e0d Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 19:58:43 +0200 Subject: [PATCH 26/56] fix: solved errors and conflicts --- .../pipelines/multiviews/multiviews.ts | 7 +- src/api/manager/productions.ts | 2 + src/api/manager/sources.ts | 1 - src/app/production/[id]/page.tsx | 3 +- src/components/inventory/Inventory.tsx | 2 +- src/components/sourceCard/SourceCard.tsx | 123 +++++---------- src/components/sourceCards/SourceCards.tsx | 143 +++++++++++------- src/hooks/useDragableItems.ts | 89 ++++------- 8 files changed, 166 insertions(+), 204 deletions(-) diff --git a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts index 5f24a869..ac76f6c3 100644 --- a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts +++ b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts @@ -64,13 +64,12 @@ export async function createMultiviewForPipeline( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion productionSettings.pipelines[multiviewIndex].pipeline_id!; const sources = await getSourcesByIds( - sourceRefs.map((ref) => ref._id.toString()) + sourceRefs.map((ref) => (ref._id ? ref._id.toString() : '')) ); const sourceRefsWithLabels = sourceRefs.map((ref) => { + const refId = ref._id ? ref._id.toString() : ''; if (!ref.label) { - const source = sources.find( - (source) => source._id.toString() === ref._id.toString() - ); + const source = sources.find((source) => source._id.toString() === refId); ref.label = source?.name || ''; } return ref; diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e3647767..12bec1a1 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -64,6 +64,8 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, + html: [], + mediaplayers: [], production_settings: production.production_settings }; } diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 853f43ce..4e77f393 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -27,7 +27,6 @@ export async function getSourcesByIds( const db = await getDatabase().catch(() => { throw new Error("Can't connect to Database"); }); - const objectIds = _ids.map((id: string) => new ObjectId(id)); const sources = await db diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index a96d3cb6..7355155f 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -118,7 +118,6 @@ export default function ProductionConfiguration({ params }: PageProps) { if (!productionSetup) return; addSource(input, productionSetup).then((updatedSetup) => { - if (!updatedSetup) return; setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); @@ -720,7 +719,7 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index c7c76709..dacb2623 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -82,7 +82,7 @@ export default function Inventory() {
      - {loading ? '' : getSourcesToDisplay(filteredSources)} + {getSourcesToDisplay(filteredSources)}
    diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index f2b94466..6eed4e23 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -8,16 +8,14 @@ import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source?: ISource; + source: ISource; label: string; - onSourceUpdate: (source: SourceReference) => void; + onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src?: string; - sourceRef?: SourceReference; - type: Type; + src: string; }; export default function SourceCard({ @@ -28,13 +26,9 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style, - sourceRef, - type + style }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState( - sourceRef?.label || source?.name - ); + const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); const t = useTranslate(); @@ -43,29 +37,21 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - if (sourceLabel?.length === 0) { - if (source) { - setSourceLabel(source.name); - } else if (sourceRef) { - setSourceLabel(sourceRef.label); - } + // if (source.name === label) { + // return; + // } + if (sourceLabel.length === 0) { + setSourceLabel(source.name); } - - if (source) { - onSourceUpdate({ + onSourceUpdate( + { _id: source._id.toString(), - type: 'ingest_source', - label: sourceLabel || source.name, + label: sourceLabel, + type: source.ingest_type as Type, input_slot: source.input_slot - }); - } else if (sourceRef) { - onSourceUpdate({ - _id: sourceRef._id, - type: sourceRef.type, - label: sourceLabel || sourceRef.label, - input_slot: sourceRef.input_slot - }); - } + }, + source + ); }; const handleKeyDown = (event: KeyboardEvent) => { @@ -83,7 +69,7 @@ export default function SourceCard({
    { @@ -92,59 +78,26 @@ export default function SourceCard({ onBlur={saveText} />
    - {source && source.src && ( - - )} - {!source && sourceRef && } - {(sourceRef || source) && ( -

    - {t('source.input_slot', { - input_slot: - sourceRef?.input_slot?.toString() || - source?.input_slot?.toString() || - '' - })} -

    - )} - - {source && ( -

    - {t('source.ingest', { - ingest: source.ingest_name - })} -

    - )} - {(source || sourceRef) && ( - - )} + +

    + {t('source.ingest', { + ingest: source.ingest_name + })} +

    + ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9af6ec4e..04101d06 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -1,80 +1,113 @@ 'use client'; import React, { useState } from 'react'; -import { SourceReference } from '../../interfaces/Source'; +import { SourceReference, Type } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; +import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; export default function SourceCards({ productionSetup, - sourceRef, updateProduction, onSourceUpdate, onSourceRemoval }: { productionSetup: Production; - sourceRef?: SourceReference; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference) => void; + onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); + const currentOrder: SourceReference[] = items.map((source) => { + return { + _id: source._id.toString(), + label: source.label, + type: source.ingest_type as Type, + input_slot: source.input_slot, + stream_uuids: source.stream_uuids + }; + }); - if (loading || !items) return null; - - // Filter SourceReference and ISource objects correctly - const sourceReferences = items.filter( - (item): item is SourceReference => item.type !== 'ingest_source' - ); - - const isISource = (source: SourceReference | ISource): source is ISource => { - // Use properties unique to ISource to check the type - return 'src' in source; - }; - - const gridItems = items.map((source) => { - const isSource = isISource(source); - - if (!source._id) return; + const gridItems: React.JSX.Element[] = []; + let tempItems = [...items]; + let firstEmptySlot = items.length + 1; - return ( - - {isSource ? ( - setSelectingText(isSelecting)} - type={'ingest_source'} - /> - ) : ( - setSelectingText(isSelecting)} - type={source.type} - /> - )} - - ); - }); + if (!items || items.length === 0) return null; + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + if (!items.some((source) => source.input_slot === i + 1)) { + firstEmptySlot = i + 1; + break; + } + } + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + // console.log(`On input slot: ${i + 1}`); + // console.log(`Checking sources:`); + // console.log(tempItems); + tempItems.every((source) => { + if (source.input_slot === i + 1) { + // console.log(`Found source on input slot: ${i + 1}`); + // console.log(`Removing source "${source.name}" from sources list`); + tempItems = tempItems.filter((i) => i._id !== source._id); + // console.log(`Adding source "${source.name}" to grid`); + if (!productionSetup.isActive) { + gridItems.push( + + + setSelectingText(isSelecting) + } + /> + + ); + } else { + gridItems.push( + + setSelectingText(isSelecting) + } + /> + ); + } + return false; + } else { + // console.log(`No source found on input slot: ${i + 1}`); + // console.log(`Adding empty slot to grid`); + if (productionSetup.isActive) { + gridItems.push( + + ); + } + return false; + } + }); + } return <>{gridItems}; } diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 5cf6d5a9..95601dc4 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -9,84 +9,61 @@ export interface ISource extends SourceWithId { stream_uuids?: string[]; src: string; } - export function useDragableItems( sources: SourceReference[] -): [ - (SourceReference | ISource)[], - (originId: string, destinationId: string) => void, - boolean -] { +): [ISource[], (originId: string, destinationId: string) => void, boolean] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState<(SourceReference | ISource)[]>( + const [items, setItems] = useState( sources.flatMap((ref) => { - const source = inventorySources.get(ref._id); + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); if (!source) return []; return { ...source, - _id: ref._id, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - ingest_source_name: source.ingest_source_name, - ingest_name: source.ingest_name, - video_stream: source.video_stream, - audio_stream: source.audio_stream, - status: source.status, - type: source.type, - tags: source.tags, - name: source.name + src: getSourceThumbnail(source) }; }) ); useEffect(() => { - const updatedItems = sources.map((ref) => { - const source = inventorySources.get(ref._id); - if (!source) return { ...ref }; - return { - ...ref, - _id: ref._id, - status: source.status, - name: source.name, - type: source.type, - tags: source.tags, - ingest_name: source.ingest_name, - ingest_source_name: source.ingest_source_name, - ingest_type: source.ingest_type, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - video_stream: source.video_stream, - audio_stream: source.audio_stream, - lastConnected: source.lastConnected - }; - }); - setItems(updatedItems); + setItems( + sources.flatMap((ref) => { + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); + if (!source) return []; + return { + ...source, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source) + }; + }) + ); }, [sources, inventorySources]); const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((item) => item._id.toString() === originId); + const originSource = items.find((i) => i._id.toString() === originId); const destinationSource = items.find( - (item) => item._id.toString() === destinationId + (i) => i._id.toString() === destinationId ); - if (!originSource || !destinationSource) return; - - const updatedItems = items - .map((item) => { - if (item._id === originSource._id) - return { ...item, input_slot: destinationSource.input_slot }; - if (item._id === destinationSource._id) - return { ...item, input_slot: originSource.input_slot }; - return item; - }) - .sort((a, b) => a.input_slot - b.input_slot); - + const originInputSlot = originSource.input_slot; + const destinationInputSlot = destinationSource.input_slot; + originSource.input_slot = destinationInputSlot; + destinationSource.input_slot = originInputSlot; + const updatedItems = [ + ...items.filter( + (i) => i._id !== originSource._id && i._id !== destinationSource._id + ), + originSource, + destinationSource + ].sort((a, b) => a.input_slot - b.input_slot); setItems(updatedItems); }; - return [allItems, moveItems, loading]; + return [items, moveItem, loading]; } From 13772e4d7de50037dfc72a737c0f513471491686 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 20:12:07 +0200 Subject: [PATCH 27/56] fix: added missing types --- src/hooks/items/addSetupItem.ts | 6 +++--- src/interfaces/production.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index b341c44e..e330a71a 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,5 +1,5 @@ -import { SourceReference } from '../../interfaces/Source'; -import { Production } from '../../interfaces/production'; +import { SourceReference, Type } from '../../interfaces/Source'; +import { HtmlReference, MediaplayerReference, Production } from '../../interfaces/production'; export function addSetupItem( source: SourceReference, @@ -15,7 +15,7 @@ export function addSetupItem( { _id: source._id, type: source.type, - label: source.label, + label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot } diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5ca..9db620d6 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,13 +3,13 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { +export interface HtmlReference { _id: string; input_slot: number; label: string; } -interface MediaplayerReference { +export interface MediaplayerReference { _id: string; input_slot: number; label: string; From bcdb35bcda66c0c92e20e91c340a4e7c284191a2 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 20:14:59 +0200 Subject: [PATCH 28/56] fix: lint error --- src/hooks/items/addSetupItem.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index e330a71a..a8e046f6 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,5 +1,9 @@ import { SourceReference, Type } from '../../interfaces/Source'; -import { HtmlReference, MediaplayerReference, Production } from '../../interfaces/production'; +import { + HtmlReference, + MediaplayerReference, + Production +} from '../../interfaces/production'; export function addSetupItem( source: SourceReference, @@ -15,7 +19,7 @@ export function addSetupItem( { _id: source._id, type: source.type, - label: source.label, + label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot } From c60a4786e1cd5b5fb9f9088dff7cde6d0a5e695f Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 14:38:29 +0200 Subject: [PATCH 29/56] fix: solved conflicts and problems with 'media-html-inputs'-branch --- src/api/ateliereLive/ingest.ts | 18 --- src/api/manager/productions.ts | 2 - src/app/production/[id]/page.tsx | 4 - src/components/dragElement/DragItem.tsx | 5 +- src/components/sourceCard/SourceCard.tsx | 123 ++++++++++++------ src/components/sourceCards/SourceCards.tsx | 140 +++++++-------------- src/hooks/items/addSetupItem.ts | 8 +- src/hooks/productions.ts | 4 +- src/hooks/sources/useAddSource.tsx | 1 - src/hooks/useDragableItems.ts | 87 ++++++++----- src/interfaces/production.ts | 14 --- 11 files changed, 187 insertions(+), 219 deletions(-) diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index c9420831..0c7b5218 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -74,24 +74,6 @@ export async function getIngests(): Promise { throw await response.json(); } -export async function getCompleteIngests(): Promise { - const response = await fetch( - new URL(LIVE_BASE_API_PATH + `/ingests?expand=true`, process.env.LIVE_URL), - { - headers: { - authorization: getAuthorizationHeader() - }, - next: { - revalidate: 0 - } - } - ); - if (response.ok) { - return response.json(); - } - throw await response.json(); -} - export async function getIngest( uuid: string ): Promise { diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index 12bec1a1..e3647767 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -64,8 +64,6 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, - html: [], - mediaplayers: [], production_settings: production.production_settings }; } diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 7355155f..0f54b14b 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -66,8 +66,6 @@ export default function ProductionConfiguration({ params }: PageProps) { const [selectedSourceRef, setSelectedSourceRef] = useState< SourceReference | undefined >(); - const [sourceReferenceToAdd, setSourceReferenceToAdd] = - useState(); const [createStream, loadingCreateStream] = useCreateStream(); const [deleteStream, loadingDeleteStream] = useDeleteStream(); //PRODUCTION @@ -119,7 +117,6 @@ export default function ProductionConfiguration({ params }: PageProps) { if (!productionSetup) return; addSource(input, productionSetup).then((updatedSetup) => { if (!updatedSetup) return; - setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); refreshProduction(); setAddSourceModal(false); @@ -719,7 +716,6 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index 4888298f..7f7364f8 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -1,9 +1,8 @@ -import React, { ReactElement, memo, useEffect, useRef } from 'react'; +import React, { ReactElement, memo, useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; -import { v4 as uuidv4 } from 'uuid'; interface IDrag { id: ObjectId | string; @@ -59,7 +58,7 @@ const DragItem: React.FC = memo( ...productionSetup, sources: currentOrder.map((source) => ({ ...source, - _id: source._id || uuidv4() // Ensure ID consistency + _id: source._id || undefined })) }; diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 6eed4e23..9645e9cf 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -1,5 +1,4 @@ 'use client'; - import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; import { IconTrash } from '@tabler/icons-react'; import { SourceReference, Type } from '../../interfaces/Source'; @@ -8,14 +7,16 @@ import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source: ISource; + source?: ISource; label: string; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src: string; + src?: string; + sourceRef?: SourceReference; + type: Type; }; export default function SourceCard({ @@ -26,10 +27,13 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + sourceRef, + type }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); - + const [sourceLabel, setSourceLabel] = useState( + sourceRef?.label || source?.name + ); const t = useTranslate(); const updateText = (event: ChangeEvent) => { @@ -37,29 +41,34 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - // if (source.name === label) { - // return; - // } - if (sourceLabel.length === 0) { - setSourceLabel(source.name); + if (sourceLabel?.length === 0) { + if (source) { + setSourceLabel(source.name); + } else if (sourceRef) { + setSourceLabel(sourceRef.label); + } } - onSourceUpdate( - { + if (source) { + onSourceUpdate({ _id: source._id.toString(), - label: sourceLabel, - type: source.ingest_type as Type, + type: 'ingest_source', + label: sourceLabel || source.name, input_slot: source.input_slot - }, - source - ); + }); + } else if (sourceRef) { + onSourceUpdate({ + _id: sourceRef._id, + type: sourceRef.type, + label: sourceLabel || sourceRef.label, + input_slot: sourceRef.input_slot + }); + } }; - const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { event.currentTarget.blur(); } }; - return (
    - -

    - {t('source.ingest', { - ingest: source.ingest_name - })} -

    - + {source && source.src && ( + + )} + {!source && sourceRef && } + {(sourceRef || source) && ( +

    + {t('source.input_slot', { + input_slot: + sourceRef?.input_slot?.toString() || + source?.input_slot?.toString() || + '' + })} +

    + )} + {source && ( +

    + {t('source.ingest', { + ingest: source.ingest_name + })} +

    + )} + {(source || sourceRef) && ( + + )} ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 04101d06..7255b70e 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -1,13 +1,10 @@ 'use client'; - import React, { useState } from 'react'; -import { SourceReference, Type } from '../../interfaces/Source'; +import { SourceReference } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; -import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; - export default function SourceCards({ productionSetup, updateProduction, @@ -16,98 +13,55 @@ export default function SourceCards({ }: { productionSetup: Production; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); - const currentOrder: SourceReference[] = items.map((source) => { - return { - _id: source._id.toString(), - label: source.label, - type: source.ingest_type as Type, - input_slot: source.input_slot, - stream_uuids: source.stream_uuids - }; - }); + if (!items) return null; + const sourceReferences = items.filter( + (item): item is SourceReference => item.type !== 'ingest_source' + ); + const isISource = (source: SourceReference | ISource): source is ISource => { + return 'src' in source; + }; - const gridItems: React.JSX.Element[] = []; - let tempItems = [...items]; - let firstEmptySlot = items.length + 1; - - if (!items || items.length === 0) return null; - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - if (!items.some((source) => source.input_slot === i + 1)) { - firstEmptySlot = i + 1; - break; - } - } - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - // console.log(`On input slot: ${i + 1}`); - // console.log(`Checking sources:`); - // console.log(tempItems); - tempItems.every((source) => { - if (source.input_slot === i + 1) { - // console.log(`Found source on input slot: ${i + 1}`); - // console.log(`Removing source "${source.name}" from sources list`); - tempItems = tempItems.filter((i) => i._id !== source._id); - // console.log(`Adding source "${source.name}" to grid`); - if (!productionSetup.isActive) { - gridItems.push( - - - setSelectingText(isSelecting) - } - /> - - ); - } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); - } - return false; - } else { - // console.log(`No source found on input slot: ${i + 1}`); - // console.log(`Adding empty slot to grid`); - if (productionSetup.isActive) { - gridItems.push( - - ); - } - - return false; - } - }); - } + const gridItems = items.map((source) => { + const id = source._id ? source._id : ''; + const isSource = isISource(source); + return ( + + {isSource ? ( + setSelectingText(isSelecting)} + type={'ingest_source'} + /> + ) : ( + setSelectingText(isSelecting)} + type={source.type} + /> + )} + + ); + }); return <>{gridItems}; } diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index a8e046f6..b341c44e 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,9 +1,5 @@ -import { SourceReference, Type } from '../../interfaces/Source'; -import { - HtmlReference, - MediaplayerReference, - Production -} from '../../interfaces/production'; +import { SourceReference } from '../../interfaces/Source'; +import { Production } from '../../interfaces/production'; export function addSetupItem( source: SourceReference, diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index 8e29cd18..cdfe918e 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -10,9 +10,7 @@ export function usePostProduction() { body: JSON.stringify({ isActive: false, name, - sources: [], - html: [], - mediaplayers: [] + sources: [] }) }); if (response.ok) { diff --git a/src/hooks/sources/useAddSource.tsx b/src/hooks/sources/useAddSource.tsx index c04c1bb3..cdd66d61 100644 --- a/src/hooks/sources/useAddSource.tsx +++ b/src/hooks/sources/useAddSource.tsx @@ -31,7 +31,6 @@ export function useAddSource(): CallbackHook< if (!updatedSetup) return; const res = await putProduction(updatedSetup._id.toString(), updatedSetup); - console.log('res', res); return res; }; diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 95601dc4..a31a09ea 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { SourceReference, SourceWithId } from '../interfaces/Source'; import { useSources } from './sources/useSources'; import { getSourceThumbnail } from '../utils/source'; - export interface ISource extends SourceWithId { label: string; input_slot: number; @@ -11,59 +10,79 @@ export interface ISource extends SourceWithId { } export function useDragableItems( sources: SourceReference[] -): [ISource[], (originId: string, destinationId: string) => void, boolean] { +): [ + (SourceReference | ISource)[], + (originId: string, destinationId: string) => void, + boolean +] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState( + const [items, setItems] = useState<(SourceReference | ISource)[]>( sources.flatMap((ref) => { const refId = ref._id ? ref._id : ''; const source = inventorySources.get(refId); if (!source) return []; return { ...source, + _id: refId, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) + src: getSourceThumbnail(source), + ingest_source_name: source.ingest_source_name, + ingest_name: source.ingest_name, + video_stream: source.video_stream, + audio_stream: source.audio_stream, + status: source.status, + type: source.type, + tags: source.tags, + name: source.name }; }) ); - useEffect(() => { - setItems( - sources.flatMap((ref) => { - const refId = ref._id ? ref._id : ''; - const source = inventorySources.get(refId); - if (!source) return []; - return { - ...source, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) - }; - }) - ); + const updatedItems = sources.map((ref) => { + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); + if (!source) return { ...ref }; + return { + ...ref, + _id: refId, + status: source.status, + name: source.name, + type: source.type, + tags: source.tags, + ingest_name: source.ingest_name, + ingest_source_name: source.ingest_source_name, + ingest_type: source.ingest_type, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + video_stream: source.video_stream, + audio_stream: source.audio_stream, + lastConnected: source.lastConnected + }; + }); + setItems(updatedItems); }, [sources, inventorySources]); - const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((i) => i._id.toString() === originId); + const originSource = items.find( + (item) => (item._id ? item._id.toString() : '') === originId + ); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => (item._id ? item._id.toString() : '') === destinationId ); if (!originSource || !destinationSource) return; - const originInputSlot = originSource.input_slot; - const destinationInputSlot = destinationSource.input_slot; - originSource.input_slot = destinationInputSlot; - destinationSource.input_slot = originInputSlot; - const updatedItems = [ - ...items.filter( - (i) => i._id !== originSource._id && i._id !== destinationSource._id - ), - originSource, - destinationSource - ].sort((a, b) => a.input_slot - b.input_slot); + const updatedItems = items + .map((item) => { + if (item._id === originSource._id) + return { ...item, input_slot: destinationSource.input_slot }; + if (item._id === destinationSource._id) + return { ...item, input_slot: originSource.input_slot }; + return item; + }) + .sort((a, b) => a.input_slot - b.input_slot); setItems(updatedItems); }; - return [items, moveItem, loading]; } diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index 9db620d6..eb4d9655 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -export interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -export interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 230837f7a46f3dfb42f3cbaeba77b57a67d397e9 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 16:00:30 +0200 Subject: [PATCH 30/56] fix: updated code, with dnd not working --- src/app/production/[id]/page.tsx | 1 + src/components/dragElement/DragItem.tsx | 1 + src/components/sourceCards/SourceCards.tsx | 10 +++++++--- .../startProduction/StartProductionButton.tsx | 5 +++++ src/hooks/pipelines.ts | 1 + src/hooks/useDragableItems.ts | 4 ++-- src/interfaces/Source.ts | 4 ++-- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 0f54b14b..20c9a638 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -661,6 +661,7 @@ export default function ProductionConfiguration({ params }: PageProps) { diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index 7f7364f8..ba68cd07 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -3,6 +3,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; +import { ISource } from '../../hooks/useDragableItems'; interface IDrag { id: ObjectId | string; diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 7255b70e..cc1e1496 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,13 +19,17 @@ export default function SourceCards({ const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); if (!items) return null; - const sourceReferences = items.filter( - (item): item is SourceReference => item.type !== 'ingest_source' - ); const isISource = (source: SourceReference | ISource): source is ISource => { return 'src' in source; }; + const sourceReferences = items.filter( + // (item): item is SourceReference => item.type !== 'ingest_source' + (item) => + (item as SourceReference).type === 'html' || + (item as SourceReference).type === 'mediaplayer' + ); + const gridItems = items.map((source) => { const id = source._id ? source._id : ''; const isSource = isISource(source); diff --git a/src/components/startProduction/StartProductionButton.tsx b/src/components/startProduction/StartProductionButton.tsx index aca65a4c..81d2e84d 100644 --- a/src/components/startProduction/StartProductionButton.tsx +++ b/src/components/startProduction/StartProductionButton.tsx @@ -17,15 +17,18 @@ import { usePutProduction } from '../../hooks/productions'; import toast from 'react-hot-toast'; import { useDeleteMonitoring } from '../../hooks/monitoring'; import { useMultiviewPresets } from '../../hooks/multiviewPreset'; +import { SourceWithId } from '../../interfaces/Source'; type StartProductionButtonProps = { production: Production | undefined; + sources: Map; disabled: boolean; refreshProduction: () => void; }; export function StartProductionButton({ production, + sources, disabled, refreshProduction }: StartProductionButtonProps) { @@ -45,6 +48,8 @@ export function StartProductionButton({ const onClick = () => { if (!production) return; + console.log('sources', sources); + console.log('production', production); const hasUndefinedPipeline = production.production_settings.pipelines.some( (p) => !p.pipeline_name ); diff --git a/src/hooks/pipelines.ts b/src/hooks/pipelines.ts index f9ef473a..baa52ffd 100644 --- a/src/hooks/pipelines.ts +++ b/src/hooks/pipelines.ts @@ -33,6 +33,7 @@ export function usePipeline( setLoading(true); getPipeline(id) .then((pipeline) => { + console.log('pipeline', pipeline); setPipeline(pipeline); }) .catch((error) => { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index a31a09ea..a9147996 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -33,7 +33,7 @@ export function useDragableItems( video_stream: source.video_stream, audio_stream: source.audio_stream, status: source.status, - type: source.type, + media_element: source.media_element, tags: source.tags, name: source.name }; @@ -49,7 +49,7 @@ export function useDragableItems( _id: refId, status: source.status, name: source.name, - type: source.type, + media_element: source.media_element, tags: source.tags, ingest_name: source.ingest_name, ingest_source_name: source.ingest_source_name, diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 2e9935b6..5df0e6f7 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,5 +1,5 @@ import { ObjectId, WithId } from 'mongodb'; -export type SourceType = 'camera' | 'graphics' | 'microphone'; +export type MediaElement = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { @@ -20,7 +20,7 @@ export interface Source { _id?: ObjectId | string; status: SourceStatus; name: string; - type: SourceType; + media_element: MediaElement; tags: { location: string; [key: string]: string | undefined; From 975bebbdc7890eb54a3bcfccc86b633008f577dd Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 16:57:01 +0200 Subject: [PATCH 31/56] fix: orders dnd correct when starting production --- src/components/dragElement/DragItem.tsx | 1 - src/components/sourceCards/SourceCards.tsx | 9 +-------- src/hooks/useDragableItems.ts | 4 ++-- src/interfaces/Source.ts | 4 ++-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index ba68cd07..7f7364f8 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -3,7 +3,6 @@ import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; -import { ISource } from '../../hooks/useDragableItems'; interface IDrag { id: ObjectId | string; diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index cc1e1496..9f360d77 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -23,13 +23,6 @@ export default function SourceCards({ return 'src' in source; }; - const sourceReferences = items.filter( - // (item): item is SourceReference => item.type !== 'ingest_source' - (item) => - (item as SourceReference).type === 'html' || - (item as SourceReference).type === 'mediaplayer' - ); - const gridItems = items.map((source) => { const id = source._id ? source._id : ''; const isSource = isISource(source); @@ -39,7 +32,7 @@ export default function SourceCards({ id={id} onMoveItem={moveItem} previousOrder={productionSetup.sources} - currentOrder={sourceReferences} + currentOrder={items as SourceReference[]} productionSetup={productionSetup} updateProduction={updateProduction} selectingText={selectingText} diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index a9147996..a31a09ea 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -33,7 +33,7 @@ export function useDragableItems( video_stream: source.video_stream, audio_stream: source.audio_stream, status: source.status, - media_element: source.media_element, + type: source.type, tags: source.tags, name: source.name }; @@ -49,7 +49,7 @@ export function useDragableItems( _id: refId, status: source.status, name: source.name, - media_element: source.media_element, + type: source.type, tags: source.tags, ingest_name: source.ingest_name, ingest_source_name: source.ingest_source_name, diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 5df0e6f7..2e9935b6 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,5 +1,5 @@ import { ObjectId, WithId } from 'mongodb'; -export type MediaElement = 'camera' | 'graphics' | 'microphone'; +export type SourceType = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { @@ -20,7 +20,7 @@ export interface Source { _id?: ObjectId | string; status: SourceStatus; name: string; - media_element: MediaElement; + type: SourceType; tags: { location: string; [key: string]: string | undefined; From f9fc4af4a04ad489a1336f1a19ef3054d210ad7b Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 13 Sep 2024 09:13:05 +0200 Subject: [PATCH 32/56] fix: add back previous functionality and empty slot card --- src/components/sourceCards/SourceCards.tsx | 137 +++++++++++++++------ 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9f360d77..c20b8c8a 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -5,6 +5,7 @@ import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; +import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; export default function SourceCards({ productionSetup, updateProduction, @@ -23,42 +24,104 @@ export default function SourceCards({ return 'src' in source; }; - const gridItems = items.map((source) => { - const id = source._id ? source._id : ''; - const isSource = isISource(source); - return ( - - {isSource ? ( - setSelectingText(isSelecting)} - type={'ingest_source'} - /> - ) : ( - setSelectingText(isSelecting)} - type={source.type} - /> - )} - - ); - }); + const gridItems: React.JSX.Element[] = []; + let tempItems = [...items]; + let firstEmptySlot = items.length + 1; + + if (!items || items.length === 0) return null; + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + if (!items.some((source) => source.input_slot === i + 1)) { + firstEmptySlot = i + 1; + break; + } + } + + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + tempItems.every((source) => { + const id = source._id ? source._id : ''; + const isSource = isISource(source); + if (source.input_slot === i + 1) { + tempItems = tempItems.filter((i) => i._id !== source._id); + if (!productionSetup.isActive) { + gridItems.push( + + {isSource ? ( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) : ( + + setSelectingText(isSelecting) + } + type={source.type} + /> + )} + + ); + } else { + isSource + ? gridItems.push( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) + : gridItems.push( + + setSelectingText(isSelecting) + } + type={source.type} + /> + ); + } + return false; + } else { + if (productionSetup.isActive) { + gridItems.push( + + ); + } + return false; + } + }); + } return <>{gridItems}; } From fd63506b23ce0029f9c027a989d80db7ee7fc306 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 13 Sep 2024 11:00:33 +0200 Subject: [PATCH 33/56] fix: can remove html/mediaplayer during production --- src/app/production/[id]/page.tsx | 114 +++++++++++++++---------------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 20c9a638..87371c90 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -493,13 +493,7 @@ export default function ProductionConfiguration({ params }: PageProps) { }; const handleRemoveSource = async () => { - if ( - productionSetup && - productionSetup.isActive && - selectedSourceRef && - // Gör det här att sourcen inte tas bort ordentligt? - selectedSourceRef.stream_uuids - ) { + if (productionSetup && productionSetup.isActive && selectedSourceRef) { const multiviews = productionSetup.production_settings.pipelines[0].multiviews; @@ -511,9 +505,60 @@ export default function ProductionConfiguration({ params }: PageProps) { ) ); - if (!viewToUpdate) { - if (!productionSetup.production_settings.pipelines[0].pipeline_id) + if (selectedSourceRef.stream_uuids) { + if (!viewToUpdate) { + if (!productionSetup.production_settings.pipelines[0].pipeline_id) + return; + + const result = await deleteStream( + selectedSourceRef.stream_uuids, + productionSetup, + selectedSourceRef.input_slot + ); + + if (!result.ok) { + if (!result.value) { + setDeleteSourceStatus({ + success: false, + steps: [{ step: 'unexpected', success: false }] + }); + } else { + setDeleteSourceStatus({ success: false, steps: result.value }); + const didDeleteStream = result.value.some( + (step) => step.step === 'delete_stream' && step.success + ); + if (didDeleteStream) { + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then( + () => { + setSelectedSourceRef(undefined); + } + ); + return; + } + } + return; + } + + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + + if (!updatedSetup) return; + + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + }); return; + } const result = await deleteStream( selectedSourceRef.stream_uuids, @@ -539,61 +584,12 @@ export default function ProductionConfiguration({ params }: PageProps) { ); if (!updatedSetup) return; setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setSelectedSourceRef(undefined); - } - ); + putProduction(updatedSetup._id.toString(), updatedSetup); return; } } return; } - - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - - if (!updatedSetup) return; - - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - setRemoveSourceModal(false); - setSelectedSourceRef(undefined); - }); - return; - } - - const result = await deleteStream( - selectedSourceRef.stream_uuids, - productionSetup, - selectedSourceRef.input_slot - ); - - if (!result.ok) { - if (!result.value) { - setDeleteSourceStatus({ - success: false, - steps: [{ step: 'unexpected', success: false }] - }); - } else { - setDeleteSourceStatus({ success: false, steps: result.value }); - const didDeleteStream = result.value.some( - (step) => step.step === 'delete_stream' && step.success - ); - if (didDeleteStream) { - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup); - return; - } - } - return; } const updatedSetup = removeSetupItem(selectedSourceRef, productionSetup); if (!updatedSetup) return; From e373e58c40ea6792dcdf44a5c456b1a992bdcafc Mon Sep 17 00:00:00 2001 From: Lucas Maupin Date: Mon, 16 Sep 2024 09:50:31 +0200 Subject: [PATCH 34/56] Feat/thumbnail fetch (#14) * feat: add additional filtering * fix: handle active + source filter, reduce code * fix: allow multiple source type selection * fix: linting * fixup! * feat: sorting ui * fix: add ingest type to database * feat: add sort based on lastConnected * fix: remove duplicate handleSorting call * fixup! * fix: add date to source card * fixup! * fix: add status-not-gone check * fix: linting error * fix: remove unused function * feat: updated to add additional multiviewers in the production-output-modal * feat: updated the functions on the production-page to handle a multiview-array * fix: changed the interface to have multiview as array * feat: updated to handle multiview-array when fetching server and api * fix: updated to stream-handling, not comfortable with the structure of this part * fix: updated to handle multiview-arr * feat: not possible to add output to same port, will cause visible error * fix: changed find to filter and maped through the results * fix: removed dev-logs * fix: correct filtering * fix: lint test fail * fix: changed to a some-check instead of a map * fix: multiview-arr does individual fetch for all mvs and creates unique ids for each * fix: updated ts-error * fix: adding and removing source from active production is working again * fix: added duplicate-check whenever multiviews-arr update and minor fixes * fix: updated multiview-arr-interface to be called multiviews instead of multiview, and minor fixes * fix: bug solve for incorrect gone assignment * fix: some cleanups * style: redesign SourceListItem Thumbnails * feat: imageComponent and sourceListing cleanup * feat: editView always shows same image as listing thumbnail * feat: general sourceList component and GlobalContext * fix: bad main merge * fix: lint * chore: add refresh images to translate and make modal z-index 50 * fix: use translate for refresh thumbnails and delete duplicated code from merge * fix: remove another duplicated line --------- Co-authored-by: Saelmala Co-authored-by: malmen237 Co-authored-by: Linda Malm <109201562+malmen237@users.noreply.github.com> --- src/api/ateliereLive/ingest.ts | 4 +- .../[source_name]/thumbnail/route.ts | 1 - src/app/layout.tsx | 5 +- src/app/production/[id]/page.tsx | 116 +++++++----------- src/components/filter/FilterOptions.tsx | 3 +- .../headerNavigation/HeaderNavigation.tsx | 20 ++- src/components/image/ImageComponent.tsx | 94 ++++++++++++++ src/components/inventory/Inventory.tsx | 91 +++----------- .../inventory/editView/EditView.tsx | 33 +---- .../inventory/editView/GeneralSettings.tsx | 2 +- src/components/modal/Modal.tsx | 2 +- src/components/sourceCard/SourceCard.tsx | 10 +- .../SourceList.module.scss} | 0 src/components/sourceList/SourceList.tsx | 89 ++++++++++++++ .../sourceListItem/PreviewThumbnail.tsx | 31 ----- .../sourceListItem/SourceListItem.tsx | 55 ++------- .../SourceListItemThumbnail.tsx | 50 ++++++++ .../inventory => contexts}/FilterContext.tsx | 2 +- src/contexts/GlobalContext.tsx | 50 ++++++++ src/i18n/locales/en.ts | 1 + src/i18n/locales/sv.ts | 1 + 21 files changed, 400 insertions(+), 260 deletions(-) create mode 100644 src/components/image/ImageComponent.tsx rename src/components/{inventory/Inventory.module.scss => sourceList/SourceList.module.scss} (100%) create mode 100644 src/components/sourceList/SourceList.tsx delete mode 100644 src/components/sourceListItem/PreviewThumbnail.tsx create mode 100644 src/components/sourceListItem/SourceListItemThumbnail.tsx rename src/{components/inventory => contexts}/FilterContext.tsx (95%) create mode 100644 src/contexts/GlobalContext.tsx diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index 0c7b5218..396ef5a1 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -106,6 +106,7 @@ export async function getSourceThumbnail( process.env.LIVE_URL ), { + next: { tags: ['image'] }, method: 'POST', body: JSON.stringify({ encoder: 'auto', @@ -114,7 +115,8 @@ export async function getSourceThumbnail( width }), headers: { - authorization: getAuthorizationHeader() + authorization: getAuthorizationHeader(), + cache: 'no-store' } } ); diff --git a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts index 926d3e33..8d69748f 100644 --- a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts +++ b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts @@ -20,7 +20,6 @@ export async function GET( status: 403 }); } - try { const ingestUuid = await getUuidFromIngestName(params.ingest_name); const sourceId = await getSourceIdFromSourceName( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eab1456f..622ac7a0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import { Toaster } from 'react-hot-toast'; import DefaultLayout from '../components/layout/DefaultLayout'; import './globals.css'; +import GlobalContextProvider from '../contexts/GlobalContext'; export default async function RootLayout({ children @@ -21,7 +22,9 @@ export default async function RootLayout({ } }} /> - {children} + + {children} + ); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index be3112c7..530c2ec6 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -19,7 +19,7 @@ import { removeSetupItem } from '../../../hooks/items/removeSetupItem'; import { addSetupItem } from '../../../hooks/items/addSetupItem'; import HeaderNavigation from '../../../components/headerNavigation/HeaderNavigation'; import styles from './page.module.scss'; -import FilterProvider from '../../../components/inventory/FilterContext'; +import FilterProvider from '../../../contexts/FilterContext'; import { useGetPresets } from '../../../hooks/presets'; import { Preset } from '../../../interfaces/preset'; import SourceCards from '../../../components/sourceCards/SourceCards'; @@ -42,6 +42,7 @@ import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { ISource } from '../../../hooks/useDragableItems'; import { useMultiviews } from '../../../hooks/multiviews'; +import SourceList from '../../../components/sourceList/SourceList'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -362,42 +363,32 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode[] { - return Array.from(filteredSources.values()).map((source, index) => { - return ( - { - if (productionSetup && productionSetup.isActive) { - setSelectedSource(source); - setAddSourceModal(true); - } else if (productionSetup) { - const updatedSetup = addSetupItem( - { - _id: source._id.toString(), - label: source.ingest_source_name, - input_slot: getFirstEmptySlot() - }, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); - } - }} - /> + + const addSourceAction = (source: SourceWithId) => { + if (productionSetup && productionSetup.isActive) { + setSelectedSource(source); + setAddSourceModal(true); + } else if (productionSetup) { + const updatedSetup = addSetupItem( + { + _id: source._id.toString(), + label: source.ingest_source_name, + input_slot: getFirstEmptySlot() + }, + productionSetup ); - }); - } + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + setAddSourceModal(false); + setSelectedSource(undefined); + }); + } + }; + + const isDisabledFunction = (source: SourceWithId): boolean => { + return selectedProductionItems?.includes(source._id.toString()); + }; const getFirstEmptySlot = () => { if (!productionSetup) throw 'no_production'; @@ -655,42 +646,23 @@ export default function ProductionConfiguration({ params }: PageProps) { inventoryVisible ? 'min-w-[35%] ml-2 mt-2 max-h-[89vh]' : '' }`} > -
    -
    - -
    -
    - - ) => { - setFilteredSources(new Map(filtered)); - }} - /> - -
    -
      - {getSourcesToDisplay(filteredSources)} - {addSourceModal && selectedSource && ( - - )} -
    -
    + setInventoryVisible(false)} + isDisabledFunc={isDisabledFunction} + /> + {addSourceModal && selectedSource && ( + + )}
    ) => void; @@ -93,7 +93,6 @@ function FilterOptions({ onFilteredSources }: FilterOptionsProps) { } } }; - const filterSources = (tempSet: Map) => { const isFilteringByType = showNdiType || showBmdType || showSrtType || showMediaSourceGeneratorType; diff --git a/src/components/headerNavigation/HeaderNavigation.tsx b/src/components/headerNavigation/HeaderNavigation.tsx index 68500db6..e2867212 100644 --- a/src/components/headerNavigation/HeaderNavigation.tsx +++ b/src/components/headerNavigation/HeaderNavigation.tsx @@ -1,5 +1,10 @@ +'use client'; + import Link from 'next/link'; import { useTranslate } from '../../i18n/useTranslate'; +import { useContext } from 'react'; +import { GlobalContext } from '../../contexts/GlobalContext'; +import { IconRefresh } from '@tabler/icons-react'; export default function HeaderNavigation({ children @@ -7,15 +12,26 @@ export default function HeaderNavigation({ children: React.ReactNode; }) { const t = useTranslate(); + const { incrementImageRefetchIndex } = useContext(GlobalContext); return ( -
    -
    +
    +
    {t('homepage')} +
    {children}
    diff --git a/src/components/image/ImageComponent.tsx b/src/components/image/ImageComponent.tsx new file mode 100644 index 00000000..cdb20ee8 --- /dev/null +++ b/src/components/image/ImageComponent.tsx @@ -0,0 +1,94 @@ +import { + PropsWithChildren, + SyntheticEvent, + useContext, + useEffect, + useRef, + useState +} from 'react'; +import Image from 'next/image'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { Loader } from '../loader/Loader'; +import { GlobalContext } from '../../contexts/GlobalContext'; + +interface ImageComponentProps extends PropsWithChildren { + src: string; + alt?: string; +} + +const ImageComponent: React.FC = (props) => { + const { src, alt = 'Image', children } = props; + const { imageRefetchIndex } = useContext(GlobalContext); + const [imgSrc, setImgSrc] = useState(); + const [loaded, setLoaded] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState>(); + const timeout = useRef>(); + + const refetchImage = () => { + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + setError(undefined); + setLoading(true); + clearTimeout(timeout.current); + timeout.current = setTimeout(() => setLoading(false), 500); + }; + + useEffect(() => { + refetchImage(); + }, [imageRefetchIndex]); + + useEffect(() => { + setError(undefined); + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + }, [src]); + + useEffect(() => { + return () => { + clearTimeout(timeout.current); + }; + }, []); + + return ( +
    + {((!imgSrc || error) && ( + + )) || ( + <> + {alt} { + setError(undefined); + setLoaded(false); + }} + onLoadingComplete={() => { + setLoaded(true); + }} + onError={(err) => { + setError(err); + }} + placeholder="empty" + width={0} + height={0} + sizes="20vh" + style={{ + width: 'auto', + height: '100%' + }} + /> + + + )} + {children} +
    + ); +}; + +export default ImageComponent; diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index dacb2623..b69da077 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -3,12 +3,10 @@ import { useEffect, useState } from 'react'; import { useSources } from '../../hooks/sources/useSources'; import { useSetSourceToPurge } from '../../hooks/sources/useSetSourceToPurge'; -import FilterOptions from '../../components/filter/FilterOptions'; -import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; -import FilterContext from './FilterContext'; -import styles from './Inventory.module.scss'; +import SourceList from '../sourceList/SourceList'; +import { useTranslate } from '../../i18n/useTranslate'; export default function Inventory() { const [removeInventorySource, reloadList] = useSetSourceToPurge(); @@ -17,10 +15,7 @@ export default function Inventory() { >(); const [sources] = useSources(reloadList, updatedSource); const [currentSource, setCurrentSource] = useState(); - const [filteredSources, setFilteredSources] = - useState>(sources); - - const inventoryVisible = true; + const t = useTranslate(); useEffect(() => { if (updatedSource && typeof updatedSource !== 'boolean') { @@ -35,71 +30,25 @@ export default function Inventory() { }, [reloadList]); const editSource = (source: SourceWithId) => { - setCurrentSource(() => source); + setCurrentSource(source); }; - - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode { - return Array.from(filteredSources.values()).map((source, index) => { - if (source.status !== 'purge') { - return ( - { - editSource(source); - }} - /> - ); - } - }); - } - return ( - -
    -
    -
    -
    - ) => - setFilteredSources(new Map(filtered)) - } - /> -
    -
      - {getSourcesToDisplay(filteredSources)} -
    -
    +
    + + {currentSource ? ( +
    + setUpdatedSource(source)} + close={() => setCurrentSource(null)} + removeInventorySource={removeInventorySource} + />
    - - {currentSource ? ( -
    - setUpdatedSource(source)} - close={() => setCurrentSource(null)} - removeInventorySource={(source) => removeInventorySource(source)} - /> -
    - ) : null} -
    - + ) : null} +
    ); } diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 80d498a3..e8e8ae72 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,12 +1,10 @@ -import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; -import { IconExclamationCircle } from '@tabler/icons-react'; +import ImageComponent from '../../image/ImageComponent'; export default function EditView({ source, @@ -19,33 +17,12 @@ export default function EditView({ close: () => void; removeInventorySource: (source: SourceWithId) => void; }) { - const [loaded, setLoaded] = useState(false); - const src = useMemo(() => getSourceThumbnail(source), [source]); - return ( -
    - {source.status === 'gone' ? ( -
    - -
    - ) : ( - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={300} - height={0} - style={{ - objectFit: 'contain' - }} - /> - )} - +
    +
    + +
    diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index ae2c4c66..2c5c6ec0 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { EditViewContext, IInput } from '../EditViewContext'; -import { FilterContext } from '../FilterContext'; +import { FilterContext } from '../../../contexts/FilterContext'; import { useTranslate } from '../../../i18n/useTranslate'; import SelectOptions from './SelectOptions'; import { getHertz } from '../../../utils/stream'; diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx index 8b3122d3..061b15ac 100644 --- a/src/components/modal/Modal.tsx +++ b/src/components/modal/Modal.tsx @@ -29,7 +29,7 @@ export function Modal({ open, children, outsideClick }: ModalProps) { return (
    - -

    + +

    {t('source.ingest', { ingest: source.ingest_name })}

    + )} +

    +
      + {getSourcesToDisplay(filteredSources)} +
    +
    +
    +
    + + ); +}; + +export default SourceList; diff --git a/src/components/sourceListItem/PreviewThumbnail.tsx b/src/components/sourceListItem/PreviewThumbnail.tsx deleted file mode 100644 index dcda882d..00000000 --- a/src/components/sourceListItem/PreviewThumbnail.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from 'next/image'; -import { useState } from 'react'; - -type PreviewProps = { src: string }; - -export const PreviewThumbnail = ({ src }: PreviewProps) => { - const [loaded, setLoaded] = useState(false); - - return ( -
    - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={0} - height={0} - sizes="20vh" - style={{ - width: 'auto', - height: '100%' - }} - /> -
    - ); -}; diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf7..db814f46 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceWithId } from '../../interfaces/Source'; -import { PreviewThumbnail } from './PreviewThumbnail'; -import { getSourceThumbnail } from '../../utils/source'; +import { SourceWithId } from '../../interfaces/Source'; import videoSettings from '../../utils/videoSettings'; import { getHertz } from '../../utils/stream'; import { useTranslate } from '../../i18n/useTranslate'; @@ -11,53 +9,27 @@ import Outputs from '../inventory/editView/AudioChannels/Outputs'; import { mapAudio } from '../../utils/audioMapping'; import { oneBased } from '../inventory/editView/AudioChannels/utils'; import capitalize from '../../utils/capitalize'; +import { SourceListItemThumbnail } from './SourceListItemThumbnail'; type SourceListItemProps = { source: SourceWithId; - action: (source: SourceWithId) => void; - edit?: boolean; + action?: (source: SourceWithId) => void; + actionText?: string; disabled: unknown; }; -const getIcon = (source: Source) => { - const isGone = source.status === 'gone'; - const className = isGone ? 'text-error' : 'text-brand'; - - const types = { - camera: ( - - ), - microphone: ( - - ), - graphics: ( - - ) - }; - - return types[source.type]; -}; - -function InventoryListItem({ +function SourceListItem({ source, action, disabled, - edit = false + actionText }: SourceListItemProps) { const t = useTranslate(); const [previewVisible, setPreviewVisible] = useState(false); const [outputRows, setOutputRows] = useState< { id: string; value: string }[][] >([]); + const timeoutRef = useRef(); const { video_stream: videoStream, audio_stream: audioStream } = source; @@ -102,15 +74,10 @@ function InventoryListItem({ className={`relative w-full items-center border-b border-gray-600 ${ disabled ? 'bg-unclickable-bg' : 'hover:bg-zinc-700' }`} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} > - {source.status !== 'gone' && - source.type === 'camera' && - previewVisible && }
    -
    {getIcon(source)}
    +
    (disabled ? '' : action(source))} + onClick={() => (disabled || !action ? '' : action(source))} >
    - {edit ? t('inventory_list.edit') : t('inventory_list.add')} + {actionText}
    { + const { source } = props; + + const getIcon = (source: Source) => { + const isGone = source.status === 'gone'; + const className = isGone ? 'text-error' : 'text-brand'; + + const types = { + camera: ( + + ), + microphone: ( + + ), + graphics: ( + + ) + }; + + return types[source.type]; + }; + + return ( +
    + {/* TODO perhaps add alts to translations */} + +
    {getIcon(source)}
    +
    +
    + ); +}; diff --git a/src/components/inventory/FilterContext.tsx b/src/contexts/FilterContext.tsx similarity index 95% rename from src/components/inventory/FilterContext.tsx rename to src/contexts/FilterContext.tsx index 5bd67896..4121694a 100644 --- a/src/components/inventory/FilterContext.tsx +++ b/src/contexts/FilterContext.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, useState, useEffect, ReactNode } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; +import { SourceWithId } from '../interfaces/Source'; interface IContext { locations: string[]; diff --git a/src/contexts/GlobalContext.tsx b/src/contexts/GlobalContext.tsx new file mode 100644 index 00000000..65cc63f1 --- /dev/null +++ b/src/contexts/GlobalContext.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { createContext, useState, PropsWithChildren } from 'react'; + +interface IGlobalContext { + locked: boolean; + imageRefetchIndex: number; + incrementImageRefetchIndex: () => void; + toggleLocked: () => void; +} + +export const GlobalContext = createContext({ + locked: false, + imageRefetchIndex: 0, + incrementImageRefetchIndex: () => { + // outsmarting lint + }, + toggleLocked: () => { + // outsmarting lint + } +}); + +const GlobalContextProvider = (props: PropsWithChildren) => { + const { children } = props; + const [locked, setLocked] = useState(false); + const [imageRefetchIndex, setImageRefetchIndex] = useState(0); + + const incrementImageRefetchIndex = () => { + setImageRefetchIndex(imageRefetchIndex + 1); + }; + + const toggleLocked = () => { + setLocked(!locked); + }; + + return ( + + {children} + + ); +}; + +export default GlobalContextProvider; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e57..5843374c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -584,6 +584,7 @@ export const en = { }, online: 'ONLINE', offline: 'OFFLINE', + refresh_images: 'Refresh Thumbnails', server_error: 'No connection with {{string}}', system_controller: 'System controller', database: 'Database', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index b33112df..b1275066 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -587,6 +587,7 @@ export const sv = { }, online: 'ONLINE', offline: 'OFFLINE', + refresh_images: 'Uppdatera Tumnaglar', server_error: '{{string}}:n inte ansluten', system_controller: 'Systemkontroller', database: 'Databas', From 1a57886ad4e80eef604c3bf83b81183672877c86 Mon Sep 17 00:00:00 2001 From: Lucas Maupin Date: Mon, 16 Sep 2024 09:50:31 +0200 Subject: [PATCH 35/56] Feat/thumbnail fetch (#14) * feat: add additional filtering * fix: handle active + source filter, reduce code * fix: allow multiple source type selection * fix: linting * fixup! * feat: sorting ui * fix: add ingest type to database * feat: add sort based on lastConnected * fix: remove duplicate handleSorting call * fixup! * fix: add date to source card * fixup! * fix: add status-not-gone check * fix: linting error * fix: remove unused function * feat: updated to add additional multiviewers in the production-output-modal * feat: updated the functions on the production-page to handle a multiview-array * fix: changed the interface to have multiview as array * feat: updated to handle multiview-array when fetching server and api * fix: updated to stream-handling, not comfortable with the structure of this part * fix: updated to handle multiview-arr * feat: not possible to add output to same port, will cause visible error * fix: changed find to filter and maped through the results * fix: removed dev-logs * fix: correct filtering * fix: lint test fail * fix: changed to a some-check instead of a map * fix: multiview-arr does individual fetch for all mvs and creates unique ids for each * fix: updated ts-error * fix: adding and removing source from active production is working again * fix: added duplicate-check whenever multiviews-arr update and minor fixes * fix: updated multiview-arr-interface to be called multiviews instead of multiview, and minor fixes * fix: bug solve for incorrect gone assignment * fix: some cleanups * style: redesign SourceListItem Thumbnails * feat: imageComponent and sourceListing cleanup * feat: editView always shows same image as listing thumbnail * feat: general sourceList component and GlobalContext * fix: bad main merge * fix: lint * chore: add refresh images to translate and make modal z-index 50 * fix: use translate for refresh thumbnails and delete duplicated code from merge * fix: remove another duplicated line --------- Co-authored-by: Saelmala Co-authored-by: malmen237 Co-authored-by: Linda Malm <109201562+malmen237@users.noreply.github.com> --- src/api/ateliereLive/ingest.ts | 4 +- .../[source_name]/thumbnail/route.ts | 1 - src/app/layout.tsx | 5 +- src/app/production/[id]/page.tsx | 146 +++++++----------- src/components/button/MonitoringButton.tsx | 8 +- .../createProduction/CreateProduction.tsx | 22 +-- src/components/filter/FilterOptions.tsx | 3 +- .../headerNavigation/HeaderNavigation.tsx | 16 ++ .../homePageContent/HomePageContent.tsx | 12 +- src/components/image/ImageComponent.tsx | 94 +++++++++++ src/components/inventory/Inventory.tsx | 101 +++--------- .../inventory/InventoryPageContent.tsx | 12 +- .../editView/AudioChannels/AudioChannels.tsx | 6 +- .../editView/AudioChannels/Outputs.tsx | 6 +- .../inventory/editView/EditView.tsx | 61 +++----- .../inventory/editView/GeneralSettings.tsx | 14 +- src/components/lockButton/LockButton.tsx | 16 +- src/components/modal/Modal.tsx | 2 +- .../DeleteProductionButton.tsx | 8 +- .../productionsList/ProductionsList.tsx | 6 +- .../productionsList/ProductionsListItem.tsx | 19 +-- src/components/sourceCard/SourceCard.tsx | 27 ++-- src/components/sourceCards/SourceCards.tsx | 6 +- .../SourceList.module.scss} | 0 src/components/sourceList/SourceList.tsx | 92 +++++++++++ .../sourceListItem/PreviewThumbnail.tsx | 31 ---- .../sourceListItem/SourceListItem.tsx | 107 +++++-------- .../SourceListItemThumbnail.tsx | 50 ++++++ .../inventory => contexts}/FilterContext.tsx | 2 +- src/contexts/GlobalContext.tsx | 50 ++++++ src/i18n/locales/en.ts | 1 + src/i18n/locales/sv.ts | 1 + 32 files changed, 517 insertions(+), 412 deletions(-) create mode 100644 src/components/image/ImageComponent.tsx rename src/components/{inventory/Inventory.module.scss => sourceList/SourceList.module.scss} (100%) create mode 100644 src/components/sourceList/SourceList.tsx delete mode 100644 src/components/sourceListItem/PreviewThumbnail.tsx create mode 100644 src/components/sourceListItem/SourceListItemThumbnail.tsx rename src/{components/inventory => contexts}/FilterContext.tsx (95%) create mode 100644 src/contexts/GlobalContext.tsx diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index 0c7b5218..396ef5a1 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -106,6 +106,7 @@ export async function getSourceThumbnail( process.env.LIVE_URL ), { + next: { tags: ['image'] }, method: 'POST', body: JSON.stringify({ encoder: 'auto', @@ -114,7 +115,8 @@ export async function getSourceThumbnail( width }), headers: { - authorization: getAuthorizationHeader() + authorization: getAuthorizationHeader(), + cache: 'no-store' } } ); diff --git a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts index 926d3e33..8d69748f 100644 --- a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts +++ b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts @@ -20,7 +20,6 @@ export async function GET( status: 403 }); } - try { const ingestUuid = await getUuidFromIngestName(params.ingest_name); const sourceId = await getSourceIdFromSourceName( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eab1456f..622ac7a0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import { Toaster } from 'react-hot-toast'; import DefaultLayout from '../components/layout/DefaultLayout'; import './globals.css'; +import GlobalContextProvider from '../contexts/GlobalContext'; export default async function RootLayout({ children @@ -21,7 +22,9 @@ export default async function RootLayout({ } }} /> - {children} + + {children} + ); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 65375959..f06d24e8 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,8 +1,6 @@ 'use client'; -import React, { useEffect, useState, KeyboardEvent } from 'react'; +import React, { useEffect, useState, KeyboardEvent, useContext } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; -import SourceListItem from '../../../components/sourceListItem/SourceListItem'; -import FilterOptions from '../../../components/filter/FilterOptions'; import { AddSource } from '../../../components/addSource/AddSource'; import { IconX } from '@tabler/icons-react'; import { useSources } from '../../../hooks/sources/useSources'; @@ -18,8 +16,6 @@ import { updateSetupItem } from '../../../hooks/items/updateSetupItem'; import { removeSetupItem } from '../../../hooks/items/removeSetupItem'; import { addSetupItem } from '../../../hooks/items/addSetupItem'; import HeaderNavigation from '../../../components/headerNavigation/HeaderNavigation'; -import styles from './page.module.scss'; -import FilterProvider from '../../../components/inventory/FilterContext'; import { useGetPresets } from '../../../hooks/presets'; import { Preset } from '../../../interfaces/preset'; import SourceCards from '../../../components/sourceCards/SourceCards'; @@ -42,7 +38,9 @@ import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { ISource } from '../../../hooks/useDragableItems'; import { useMultiviews } from '../../../hooks/multiviews'; +import SourceList from '../../../components/sourceList/SourceList'; import { LockButton } from '../../../components/lockButton/LockButton'; +import { GlobalContext } from '../../../contexts/GlobalContext'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -89,7 +87,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); - const [isLocked, setIsLocked] = useState(true); + const { locked } = useContext(GlobalContext); useEffect(() => { refreshPipelines(); @@ -365,43 +363,32 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode[] { - return Array.from(filteredSources.values()).map((source, index) => { - return ( - { - if (productionSetup && productionSetup.isActive) { - setSelectedSource(source); - setAddSourceModal(true); - } else if (productionSetup) { - const updatedSetup = addSetupItem( - { - _id: source._id.toString(), - label: source.ingest_source_name, - input_slot: getFirstEmptySlot() - }, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); - } - }} - /> + + const addSourceAction = (source: SourceWithId) => { + if (productionSetup && productionSetup.isActive) { + setSelectedSource(source); + setAddSourceModal(true); + } else if (productionSetup) { + const updatedSetup = addSetupItem( + { + _id: source._id.toString(), + label: source.ingest_source_name, + input_slot: getFirstEmptySlot() + }, + productionSetup ); - }); - } + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + setAddSourceModal(false); + setSelectedSource(undefined); + }); + } + }; + + const isDisabledFunction = (source: SourceWithId): boolean => { + return selectedProductionItems?.includes(source._id.toString()); + }; const getFirstEmptySlot = () => { if (!productionSetup) throw 'no_production'; @@ -621,20 +608,16 @@ export default function ProductionConfiguration({ params }: PageProps) { } }} onBlur={() => updateConfigName(configurationName)} - disabled={isLocked} />
    - setIsLocked(!isLocked)} - /> +
    @@ -666,42 +649,24 @@ export default function ProductionConfiguration({ params }: PageProps) { inventoryVisible ? 'min-w-[35%] ml-2 mt-2 max-h-[89vh]' : '' }`} > -
    -
    - -
    -
    - - ) => { - setFilteredSources(new Map(filtered)); - }} - /> - -
    -
      - {getSourcesToDisplay(filteredSources)} - {addSourceModal && selectedSource && ( - - )} -
    -
    + setInventoryVisible(false)} + isDisabledFunc={isDisabledFunction} + locked={locked} + /> + {addSourceModal && selectedSource && ( + + )}
    {removeSourceModal && selectedSourceRef && ( { setInventoryVisible(true); @@ -777,7 +741,7 @@ export default function ProductionConfiguration({ params }: PageProps) { (pipeline, i) => { return ( ({ @@ -793,7 +757,7 @@ export default function ProductionConfiguration({ params }: PageProps) { )} {productionSetup?.production_settings && ( ({ option: controlPanel.name, available: controlPanel.outgoing_connections?.length === 0 @@ -814,7 +778,7 @@ export default function ProductionConfiguration({ params }: PageProps) {
    )} diff --git a/src/components/button/MonitoringButton.tsx b/src/components/button/MonitoringButton.tsx index 375844c7..f8410ee3 100644 --- a/src/components/button/MonitoringButton.tsx +++ b/src/components/button/MonitoringButton.tsx @@ -7,20 +7,20 @@ import { IconAlertTriangleFilled } from '@tabler/icons-react'; type MonitoringButtonProps = { id: string; - isLocked: boolean; + locked: boolean; }; -export const MonitoringButton = ({ id, isLocked }: MonitoringButtonProps) => { +export const MonitoringButton = ({ id, locked }: MonitoringButtonProps) => { const t = useTranslate(); const [hasError, loading] = useMonitoringError(id); return ( void; - isLocked: boolean; -}; - -export function CreateProduction({ onClick, isLocked }: CreateProductionProps) { +export function CreateProduction() { const router = useRouter(); const postProduction = usePostProduction(); @@ -54,19 +49,14 @@ export function CreateProduction({ onClick, isLocked }: CreateProductionProps) { {t('production_configuration')}
    - + @@ -105,11 +95,7 @@ export function CreateProduction({ onClick, isLocked }: CreateProductionProps) {
    {children}
    diff --git a/src/components/homePageContent/HomePageContent.tsx b/src/components/homePageContent/HomePageContent.tsx index 170acd0f..8c358505 100644 --- a/src/components/homePageContent/HomePageContent.tsx +++ b/src/components/homePageContent/HomePageContent.tsx @@ -1,26 +1,24 @@ 'use client'; import { CreateProduction } from '../createProduction/CreateProduction'; -import { Suspense, useState } from 'react'; +import { Suspense, useContext } from 'react'; import { LoadingCover } from '../loader/LoadingCover'; import ProductionsList from '../productionsList/ProductionsList'; import { Production } from '../../interfaces/production'; +import { GlobalContext } from '../../contexts/GlobalContext'; type HomePageContentProps = { productions: Production[]; }; export const HomePageContent = ({ productions }: HomePageContentProps) => { - const [isLocked, setIsLocked] = useState(true); + const { locked } = useContext(GlobalContext); return (
    - setIsLocked(!isLocked)} - /> +
    }> - +
    diff --git a/src/components/image/ImageComponent.tsx b/src/components/image/ImageComponent.tsx new file mode 100644 index 00000000..cdb20ee8 --- /dev/null +++ b/src/components/image/ImageComponent.tsx @@ -0,0 +1,94 @@ +import { + PropsWithChildren, + SyntheticEvent, + useContext, + useEffect, + useRef, + useState +} from 'react'; +import Image from 'next/image'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { Loader } from '../loader/Loader'; +import { GlobalContext } from '../../contexts/GlobalContext'; + +interface ImageComponentProps extends PropsWithChildren { + src: string; + alt?: string; +} + +const ImageComponent: React.FC = (props) => { + const { src, alt = 'Image', children } = props; + const { imageRefetchIndex } = useContext(GlobalContext); + const [imgSrc, setImgSrc] = useState(); + const [loaded, setLoaded] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState>(); + const timeout = useRef>(); + + const refetchImage = () => { + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + setError(undefined); + setLoading(true); + clearTimeout(timeout.current); + timeout.current = setTimeout(() => setLoading(false), 500); + }; + + useEffect(() => { + refetchImage(); + }, [imageRefetchIndex]); + + useEffect(() => { + setError(undefined); + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + }, [src]); + + useEffect(() => { + return () => { + clearTimeout(timeout.current); + }; + }, []); + + return ( +
    + {((!imgSrc || error) && ( + + )) || ( + <> + {alt} { + setError(undefined); + setLoaded(false); + }} + onLoadingComplete={() => { + setLoaded(true); + }} + onError={(err) => { + setError(err); + }} + placeholder="empty" + width={0} + height={0} + sizes="20vh" + style={{ + width: 'auto', + height: '100%' + }} + /> + + + )} + {children} +
    + ); +}; + +export default ImageComponent; diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index b2d5d846..78e5bf05 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -3,28 +3,19 @@ import { useEffect, useState } from 'react'; import { useSources } from '../../hooks/sources/useSources'; import { useSetSourceToPurge } from '../../hooks/sources/useSetSourceToPurge'; -import FilterOptions from '../../components/filter/FilterOptions'; -import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; -import FilterContext from './FilterContext'; -import styles from './Inventory.module.scss'; +import SourceList from '../sourceList/SourceList'; +import { useTranslate } from '../../i18n/useTranslate'; -type InventoryProps = { - isLocked: boolean; -}; - -export default function Inventory({ isLocked }: InventoryProps) { +export default function Inventory({ locked }: { locked: boolean }) { const [removeInventorySource, reloadList] = useSetSourceToPurge(); const [updatedSource, setUpdatedSource] = useState< SourceWithId | undefined >(); const [sources] = useSources(reloadList, updatedSource); const [currentSource, setCurrentSource] = useState(); - const [filteredSources, setFilteredSources] = - useState>(sources); - - const inventoryVisible = true; + const t = useTranslate(); useEffect(() => { if (updatedSource && typeof updatedSource !== 'boolean') { @@ -39,73 +30,27 @@ export default function Inventory({ isLocked }: InventoryProps) { }, [reloadList]); const editSource = (source: SourceWithId) => { - setCurrentSource(() => source); + setCurrentSource(source); }; - - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode { - return Array.from(filteredSources.values()).map((source, index) => { - if (source.status !== 'purge') { - return ( - { - editSource(source); - }} - isLocked={isLocked} - /> - ); - } - }); - } - return ( - -
    -
    -
    -
    - ) => - setFilteredSources(new Map(filtered)) - } - /> -
    -
      - {getSourcesToDisplay(filteredSources)} -
    -
    +
    + + {currentSource ? ( +
    + setUpdatedSource(source)} + close={() => setCurrentSource(null)} + removeInventorySource={removeInventorySource} + locked={locked} + />
    - - {currentSource ? ( -
    - setUpdatedSource(source)} - close={() => setCurrentSource(null)} - removeInventorySource={(source) => removeInventorySource(source)} - /> -
    - ) : null} -
    - + ) : null} +
    ); } diff --git a/src/components/inventory/InventoryPageContent.tsx b/src/components/inventory/InventoryPageContent.tsx index 06e5ee6a..a773ed63 100644 --- a/src/components/inventory/InventoryPageContent.tsx +++ b/src/components/inventory/InventoryPageContent.tsx @@ -1,13 +1,14 @@ 'use client'; -import { useState, Suspense } from 'react'; +import { Suspense, useContext } from 'react'; import { LockButton } from '../lockButton/LockButton'; import { useTranslate } from '../../i18n/useTranslate'; import HeaderNavigation from '../headerNavigation/HeaderNavigation'; import Inventory from './Inventory'; +import { GlobalContext } from '../../contexts/GlobalContext'; export const InventoryPageContent = () => { - const [isLocked, setIsLocked] = useState(true); const t = useTranslate(); + const { locked } = useContext(GlobalContext); return ( <> @@ -17,15 +18,12 @@ export const InventoryPageContent = () => {

    {t('inventory')}

    - setIsLocked(!isLocked)} - /> +
    - + ); diff --git a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx index 4315e645..def43470 100644 --- a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx +++ b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx @@ -11,10 +11,10 @@ import { channel, mapAudio } from '../../../../utils/audioMapping'; interface IAudioChannels { source: Source; - isLocked: boolean; + locked: boolean; } -export default function AudioChannels({ source, isLocked }: IAudioChannels) { +export default function AudioChannels({ source, locked }: IAudioChannels) { type TOutputs = 'audio_mapping.outL' | 'audio_mapping.outR'; const t = useTranslate(); const outputs: TOutputs[] = ['audio_mapping.outL', 'audio_mapping.outR']; @@ -227,7 +227,7 @@ export default function AudioChannels({ source, isLocked }: IAudioChannels) { outputRows={outputRows} rowIndex={rowIndex} max={max} - isLocked={isLocked} + locked={locked} updateRows={updateRows} />
    diff --git a/src/components/inventory/editView/AudioChannels/Outputs.tsx b/src/components/inventory/editView/AudioChannels/Outputs.tsx index f9dcf4b2..7bf3b0e7 100644 --- a/src/components/inventory/editView/AudioChannels/Outputs.tsx +++ b/src/components/inventory/editView/AudioChannels/Outputs.tsx @@ -18,7 +18,7 @@ interface IOutput { rowIndex: number; max: number; small?: boolean; - isLocked: boolean; + locked: boolean; updateRows?: (e: IEvent, rowIndex: number, index: number, id: string) => void; } @@ -28,7 +28,7 @@ export default function Outputs({ rowIndex, max, small = false, - isLocked, + locked, updateRows }: IOutput) { return ( @@ -52,7 +52,7 @@ export default function Outputs({ } relative ${styles.checkbox}`} > diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 6587c1ac..b857dc5b 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,66 +1,41 @@ -import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; -import { IconExclamationCircle } from '@tabler/icons-react'; - -type EditViewProps = { - source: SourceWithId; - isLocked: boolean; - updateSource: (source: SourceWithId) => void; - close: () => void; - removeInventorySource: (source: SourceWithId) => void; -}; +import ImageComponent from '../../image/ImageComponent'; export default function EditView({ source, - isLocked, updateSource, close, - removeInventorySource -}: EditViewProps) { - const [loaded, setLoaded] = useState(false); - const src = useMemo(() => getSourceThumbnail(source), [source]); - + removeInventorySource, + locked +}: { + source: SourceWithId; + updateSource: (source: SourceWithId) => void; + close: () => void; + removeInventorySource: (source: SourceWithId) => void; + locked: boolean; +}) { return ( -
    - {source.status === 'gone' ? ( -
    - -
    - ) : ( - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={300} - height={0} - style={{ - objectFit: 'contain' - }} - /> - )} - - +
    +
    + +
    +
    - +
    ); diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index 42c137fe..852efa43 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -1,16 +1,16 @@ import { useContext } from 'react'; import { EditViewContext, IInput } from '../EditViewContext'; -import { FilterContext } from '../FilterContext'; +import { FilterContext } from '../../../contexts/FilterContext'; import { useTranslate } from '../../../i18n/useTranslate'; import SelectOptions from './SelectOptions'; import { getHertz } from '../../../utils/stream'; import videoSettings from '../../../utils/videoSettings'; type GeneralSettingsProps = { - isLocked: boolean; + locked: boolean; }; -export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { +export default function GeneralSettings({ locked }: GeneralSettingsProps) { const { input: [input, setInput], saved: [saved, setSaved], @@ -46,11 +46,11 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { value={input.name} onChange={(e) => onChange('name', e.target.value)} className={`${ - isLocked + locked ? 'pointer-events-none bg-gray-700/50 border-gray-600/50 placeholder-gray-400/50 text-p/50' : 'pointer-events-auto bg-gray-700 border-gray-600 placeholder-gray-400 text-p' } 'cursor-pointer ml-5 border justify-center text-sm rounded-lg w-full pl-2 pt-1 pb-1 focus:ring-blue-500 focus:border-blue-500'`} - disabled={isLocked} + disabled={locked} />
    @@ -59,7 +59,7 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { name="type" options={types} selected={input.type} - disabled={isLocked} + disabled={locked} onChange={(e) => onChange('type', e.target.value.toLowerCase())} />
    @@ -68,7 +68,7 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { name="location" options={locations} selected={input.location} - disabled={isLocked} + disabled={locked} onChange={(e) => onChange('location', e.target.value.toLowerCase())} />
    diff --git a/src/components/lockButton/LockButton.tsx b/src/components/lockButton/LockButton.tsx index 40ad54e8..89011b38 100644 --- a/src/components/lockButton/LockButton.tsx +++ b/src/components/lockButton/LockButton.tsx @@ -1,19 +1,17 @@ import { IconLock, IconLockOpen } from '@tabler/icons-react'; +import { useContext } from 'react'; +import { GlobalContext } from '../../contexts/GlobalContext'; type LockButtonProps = { - isLocked: boolean; classNames?: string; - onClick: () => void; }; -export const LockButton = ({ - isLocked, - classNames, - onClick -}: LockButtonProps) => { +export const LockButton = ({ classNames }: LockButtonProps) => { + const { locked, toggleLocked } = useContext(GlobalContext); + return ( - + )} +
    +
      + {getSourcesToDisplay(filteredSources)} +
    + + + + + ); +}; + +export default SourceList; diff --git a/src/components/sourceListItem/PreviewThumbnail.tsx b/src/components/sourceListItem/PreviewThumbnail.tsx deleted file mode 100644 index dcda882d..00000000 --- a/src/components/sourceListItem/PreviewThumbnail.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from 'next/image'; -import { useState } from 'react'; - -type PreviewProps = { src: string }; - -export const PreviewThumbnail = ({ src }: PreviewProps) => { - const [loaded, setLoaded] = useState(false); - - return ( -
    - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={0} - height={0} - sizes="20vh" - style={{ - width: 'auto', - height: '100%' - }} - /> -
    - ); -}; diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 02be92d6..7d3f9cbf 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceWithId } from '../../interfaces/Source'; -import { PreviewThumbnail } from './PreviewThumbnail'; -import { getSourceThumbnail } from '../../utils/source'; +import { SourceWithId } from '../../interfaces/Source'; import videoSettings from '../../utils/videoSettings'; import { getHertz } from '../../utils/stream'; import { useTranslate } from '../../i18n/useTranslate'; @@ -11,55 +9,29 @@ import Outputs from '../inventory/editView/AudioChannels/Outputs'; import { mapAudio } from '../../utils/audioMapping'; import { oneBased } from '../inventory/editView/AudioChannels/utils'; import capitalize from '../../utils/capitalize'; +import { SourceListItemThumbnail } from './SourceListItemThumbnail'; type SourceListItemProps = { source: SourceWithId; - edit?: boolean; + actionText?: string; disabled: unknown; - isLocked: boolean; - action: (source: SourceWithId) => void; + action?: (source: SourceWithId) => void; + locked: boolean; }; -const getIcon = (source: Source) => { - const isGone = source.status === 'gone'; - const className = isGone ? 'text-error' : 'text-brand'; - - const types = { - camera: ( - - ), - microphone: ( - - ), - graphics: ( - - ) - }; - - return types[source.type]; -}; - -function InventoryListItem({ +function SourceListItem({ source, action, disabled, - edit = false, - isLocked + locked, + actionText }: SourceListItemProps) { const t = useTranslate(); const [previewVisible, setPreviewVisible] = useState(false); const [outputRows, setOutputRows] = useState< { id: string; value: string }[][] >([]); + const timeoutRef = useRef(); const { video_stream: videoStream, audio_stream: audioStream } = source; @@ -104,15 +76,10 @@ function InventoryListItem({ className={`relative w-full items-center border-b border-gray-600 ${ disabled ? 'bg-unclickable-bg' : 'hover:bg-zinc-700' }`} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} > - {source.status !== 'gone' && - source.type === 'camera' && - previewVisible && }
    -
    {getIcon(source)}
    +
    ))} @@ -180,36 +147,38 @@ function InventoryListItem({
    - {!disabled ? ( -
    - -
    - ) : null} + {actionText} +
    + + + ); } -export default InventoryListItem; +export default SourceListItem; diff --git a/src/components/sourceListItem/SourceListItemThumbnail.tsx b/src/components/sourceListItem/SourceListItemThumbnail.tsx new file mode 100644 index 00000000..4c831868 --- /dev/null +++ b/src/components/sourceListItem/SourceListItemThumbnail.tsx @@ -0,0 +1,50 @@ +import { Source, SourceWithId } from '../../interfaces/Source'; +import { getSourceThumbnail } from '../../utils/source'; +import Icons from '../icons/Icons'; +import ImageComponent from '../image/ImageComponent'; + +type SourceThumbnailProps = { source: SourceWithId }; + +export const SourceListItemThumbnail = (props: SourceThumbnailProps) => { + const { source } = props; + + const getIcon = (source: Source) => { + const isGone = source.status === 'gone'; + const className = isGone ? 'text-error' : 'text-brand'; + + const types = { + camera: ( + + ), + microphone: ( + + ), + graphics: ( + + ) + }; + + return types[source.type]; + }; + + return ( +
    + {/* TODO perhaps add alts to translations */} + +
    {getIcon(source)}
    +
    +
    + ); +}; diff --git a/src/components/inventory/FilterContext.tsx b/src/contexts/FilterContext.tsx similarity index 95% rename from src/components/inventory/FilterContext.tsx rename to src/contexts/FilterContext.tsx index 5bd67896..4121694a 100644 --- a/src/components/inventory/FilterContext.tsx +++ b/src/contexts/FilterContext.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, useState, useEffect, ReactNode } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; +import { SourceWithId } from '../interfaces/Source'; interface IContext { locations: string[]; diff --git a/src/contexts/GlobalContext.tsx b/src/contexts/GlobalContext.tsx new file mode 100644 index 00000000..65cc63f1 --- /dev/null +++ b/src/contexts/GlobalContext.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { createContext, useState, PropsWithChildren } from 'react'; + +interface IGlobalContext { + locked: boolean; + imageRefetchIndex: number; + incrementImageRefetchIndex: () => void; + toggleLocked: () => void; +} + +export const GlobalContext = createContext({ + locked: false, + imageRefetchIndex: 0, + incrementImageRefetchIndex: () => { + // outsmarting lint + }, + toggleLocked: () => { + // outsmarting lint + } +}); + +const GlobalContextProvider = (props: PropsWithChildren) => { + const { children } = props; + const [locked, setLocked] = useState(false); + const [imageRefetchIndex, setImageRefetchIndex] = useState(0); + + const incrementImageRefetchIndex = () => { + setImageRefetchIndex(imageRefetchIndex + 1); + }; + + const toggleLocked = () => { + setLocked(!locked); + }; + + return ( + + {children} + + ); +}; + +export default GlobalContextProvider; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e57..5843374c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -584,6 +584,7 @@ export const en = { }, online: 'ONLINE', offline: 'OFFLINE', + refresh_images: 'Refresh Thumbnails', server_error: 'No connection with {{string}}', system_controller: 'System controller', database: 'Database', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index b33112df..b1275066 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -587,6 +587,7 @@ export const sv = { }, online: 'ONLINE', offline: 'OFFLINE', + refresh_images: 'Uppdatera Tumnaglar', server_error: '{{string}}:n inte ansluten', system_controller: 'Systemkontroller', database: 'Databas', From 535371227b145d3b09b4f3d8a47f6de23b06d394 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 10 Sep 2024 15:50:10 +0200 Subject: [PATCH 36/56] feat: add management lock --- src/app/production/[id]/page.tsx | 2 ++ .../createProduction/CreateProduction.tsx | 1 + src/components/inventory/Inventory.tsx | 2 +- .../inventory/InventoryHeaderContent.tsx | 32 +++++++++++++++++++ .../inventory/editView/EditView.tsx | 7 ++++ src/components/sourceCard/SourceCard.tsx | 4 ++- src/components/sourceCards/SourceCards.tsx | 6 +++- 7 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/components/inventory/InventoryHeaderContent.tsx diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index f06d24e8..5d6d2ee3 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -608,6 +608,7 @@ export default function ProductionConfiguration({ params }: PageProps) { } }} onBlur={() => updateConfigName(configurationName)} + disabled={locked} />
    {removeSourceModal && selectedSourceRef && ( } + disabled={isLocked} > {t('create_new')} diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index 78e5bf05..61315b97 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { useSources } from '../../hooks/sources/useSources'; import { useSetSourceToPurge } from '../../hooks/sources/useSetSourceToPurge'; import { SourceWithId } from '../../interfaces/Source'; -import EditView from './editView/EditView'; +import EditView from '../../../orchestration-gui/src/components/inventory/editView/EditView'; import SourceList from '../sourceList/SourceList'; import { useTranslate } from '../../i18n/useTranslate'; diff --git a/src/components/inventory/InventoryHeaderContent.tsx b/src/components/inventory/InventoryHeaderContent.tsx new file mode 100644 index 00000000..a7b93e12 --- /dev/null +++ b/src/components/inventory/InventoryHeaderContent.tsx @@ -0,0 +1,32 @@ +'use client'; +import { useState, Suspense } from 'react'; +import { LockButton } from '../lockButton/LockButton'; +import { useTranslate } from '../../i18n/useTranslate'; +import HeaderNavigation from '../headerNavigation/HeaderNavigation'; +import Inventory from './Inventory'; + +export const InventoryHeaderContent = () => { + const [isLocked, setIsLocked] = useState(true); + const t = useTranslate(); + + return ( + <> + +
    +
    +

    + {t('inventory')} +

    + setIsLocked(!isLocked)} + /> +
    +
    +
    + + + + + ); +}; diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index b857dc5b..53ec3883 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -8,17 +8,22 @@ import ImageComponent from '../../image/ImageComponent'; export default function EditView({ source, + isLocked, updateSource, close, removeInventorySource, locked }: { source: SourceWithId; + isLocked: boolean; updateSource: (source: SourceWithId) => void; close: () => void; removeInventorySource: (source: SourceWithId) => void; locked: boolean; }) { + const [loaded, setLoaded] = useState(false); + const src = useMemo(() => getSourceThumbnail(source), [source]); + return (
    @@ -32,6 +37,8 @@ export default function EditView({
    ; style?: object; src: string; + isLocked: boolean; }; export default function SourceCard({ @@ -30,7 +31,8 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + isLocked }: SourceCardProps) { const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9666bccc..44903f85 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -12,12 +12,14 @@ export default function SourceCards({ productionSetup, updateProduction, onSourceUpdate, - onSourceRemoval + onSourceRemoval, + isLocked }: { productionSetup: Production; updateProduction: (updated: Production) => void; onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; onSourceRemoval: (source: SourceReference) => void; + isLocked: boolean; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); @@ -72,6 +74,7 @@ export default function SourceCards({ onSelectingText={(isSelecting: boolean) => setSelectingText(isSelecting) } + isLocked={isLocked} /> ); @@ -87,6 +90,7 @@ export default function SourceCards({ onSelectingText={(isSelecting: boolean) => setSelectingText(isSelecting) } + isLocked={isLocked} /> ); } From 34f5d5ee45cf9478af8fdc341f7f8631463fac44 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Mon, 16 Sep 2024 16:21:11 +0200 Subject: [PATCH 37/56] fix: use global context instead of state --- src/app/production/[id]/page.tsx | 1 + src/components/button/MonitoringButton.tsx | 3 +++ .../createProduction/CreateProduction.tsx | 1 - src/components/inventory/Inventory.tsx | 3 ++- .../editView/AudioChannels/AudioChannels.tsx | 2 +- src/components/inventory/editView/EditView.tsx | 11 ++++------- .../inventory/editView/GeneralSettings.tsx | 1 + .../inventory/editView/UpdateButtons.tsx | 16 ++++++++-------- src/components/modal/AddSourceModal.tsx | 6 ++++-- .../productionsList/DeleteProductionButton.tsx | 4 ++++ .../productionsList/ProductionsListItem.tsx | 4 ++++ src/components/sourceCard/SourceCard.tsx | 5 +---- src/components/sourceCards/SourceCards.tsx | 6 +----- src/components/sourceListItem/SourceListItem.tsx | 1 + src/contexts/GlobalContext.tsx | 2 +- 15 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 5d6d2ee3..d2802c13 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -666,6 +666,7 @@ export default function ProductionConfiguration({ params }: PageProps) { onConfirm={handleAddSource} status={addSourceStatus} loading={loadingCreateStream} + locked={locked} /> )}
    diff --git a/src/components/button/MonitoringButton.tsx b/src/components/button/MonitoringButton.tsx index f8410ee3..2feb33b5 100644 --- a/src/components/button/MonitoringButton.tsx +++ b/src/components/button/MonitoringButton.tsx @@ -4,6 +4,8 @@ import { useTranslate } from '../../i18n/useTranslate'; import { useMonitoringError } from '../../hooks/monitoring'; import { IconLoader } from '@tabler/icons-react'; import { IconAlertTriangleFilled } from '@tabler/icons-react'; +import { useContext } from 'react'; +import { GlobalContext } from '../../contexts/GlobalContext'; type MonitoringButtonProps = { id: string; @@ -13,6 +15,7 @@ type MonitoringButtonProps = { export const MonitoringButton = ({ id, locked }: MonitoringButtonProps) => { const t = useTranslate(); const [hasError, loading] = useMonitoringError(id); + const { locked } = useContext(GlobalContext); return ( } - disabled={isLocked} > {t('create_new')} diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index 61315b97..3382f113 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { useSources } from '../../hooks/sources/useSources'; import { useSetSourceToPurge } from '../../hooks/sources/useSetSourceToPurge'; import { SourceWithId } from '../../interfaces/Source'; -import EditView from '../../../orchestration-gui/src/components/inventory/editView/EditView'; +import EditView from './editView/EditView'; import SourceList from '../sourceList/SourceList'; import { useTranslate } from '../../i18n/useTranslate'; @@ -43,6 +43,7 @@ export default function Inventory({ locked }: { locked: boolean }) { {currentSource ? (
    setUpdatedSource(source)} close={() => setCurrentSource(null)} diff --git a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx index def43470..7024fede 100644 --- a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx +++ b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx @@ -227,8 +227,8 @@ export default function AudioChannels({ source, locked }: IAudioChannels) { outputRows={outputRows} rowIndex={rowIndex} max={max} - locked={locked} updateRows={updateRows} + locked={locked} />
    ))} diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 53ec3883..ebfd23e8 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -5,24 +5,23 @@ import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; import ImageComponent from '../../image/ImageComponent'; +import { useContext } from 'react'; +import { GlobalContext } from '../../../contexts/GlobalContext'; export default function EditView({ source, - isLocked, updateSource, close, removeInventorySource, locked }: { source: SourceWithId; - isLocked: boolean; updateSource: (source: SourceWithId) => void; close: () => void; removeInventorySource: (source: SourceWithId) => void; locked: boolean; }) { - const [loaded, setLoaded] = useState(false); - const src = useMemo(() => getSourceThumbnail(source), [source]); + console.log('LOCKED THREE: ', locked); return ( @@ -38,11 +37,9 @@ export default function EditView({ ); diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index 852efa43..4cde521b 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -5,6 +5,7 @@ import { useTranslate } from '../../../i18n/useTranslate'; import SelectOptions from './SelectOptions'; import { getHertz } from '../../../utils/stream'; import videoSettings from '../../../utils/videoSettings'; +import { GlobalContext } from '../../../contexts/GlobalContext'; type GeneralSettingsProps = { locked: boolean; diff --git a/src/components/inventory/editView/UpdateButtons.tsx b/src/components/inventory/editView/UpdateButtons.tsx index 622f7602..440dd33b 100644 --- a/src/components/inventory/editView/UpdateButtons.tsx +++ b/src/components/inventory/editView/UpdateButtons.tsx @@ -9,16 +9,16 @@ import { IconTrash } from '@tabler/icons-react'; type UpdateButtonsProps = { source: SourceWithId; - isLocked: boolean; removeInventorySource: (source: SourceWithId) => void; close: () => void; + locked: boolean; }; export default function UpdateButtons({ source, - isLocked, close, - removeInventorySource + removeInventorySource, + locked }: UpdateButtonsProps) { const t = useTranslate(); const { @@ -39,9 +39,9 @@ export default function UpdateButtons({ @@ -58,12 +58,12 @@ export default function UpdateButtons({ - + // <> + // + // {modalOpen && ( + //
    + // + // + //
    + // )} + // + )} +
    void; + columnStyle?: boolean; } -export default function Options({ label, options, value, update }: IOPtions) { +export default function Options({ + label, + options, + value, + update, + columnStyle +}: IOptions) { + const t = useTranslate(); return ( -
    +