diff --git a/src/api/agileLive/ingest.ts b/src/api/agileLive/ingest.ts index 3ff9d20..26fd3d2 100644 --- a/src/api/agileLive/ingest.ts +++ b/src/api/agileLive/ingest.ts @@ -127,3 +127,25 @@ export async function getSourceThumbnail( } throw await response.json(); } + +export async function deleteSrtSource(ingestUuid: string, sourceId: number) { + const response = await fetch( + new URL( + AGILE_BASE_API_PATH + `/ingests/${ingestUuid}/sources/${sourceId}`, + process.env.AGILE_URL + ), + { + method: 'DELETE', + headers: { + authorization: getAuthorizationHeader() + }, + next: { + revalidate: 0 + } + } + ); + if (response.ok) { + return response.status; + } + throw await response.text(); +} diff --git a/src/api/manager/inventory.ts b/src/api/manager/inventory.ts index e5796ca..7d47e1e 100644 --- a/src/api/manager/inventory.ts +++ b/src/api/manager/inventory.ts @@ -1,6 +1,7 @@ -import { ObjectId } from 'mongodb'; +import { ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Numbers } from '../../interfaces/Source'; +import { Log } from '../logger'; interface IResponse { audio_stream?: { @@ -20,3 +21,21 @@ export async function getAudioMapping(id: ObjectId): Promise { throw `Could not find audio mapping for source: ${id.toString()}`; })) as IResponse; } + +export async function purgeInventorySourceItem( + id: string +): Promise> { + const db = await getDatabase(); + const objectId = new ObjectId(id); + + // Not possible to delete from API so this adds a purge-flag + // to the source + const result = await db + .collection('inventory') + .updateOne({ _id: objectId, status: 'gone' }, { $set: { status: 'purge' } }) + .catch((error) => { + throw `Was not able to set source-id for ${id} to purge: ${error}`; + }); + + return result as UpdateResult; +} diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 8f63785..368f137 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -57,8 +57,29 @@ async function getSourcesFromAPI() { */ export async function runSyncInventory() { const db = await getDatabase(); - const dbInventory = await db.collection('inventory').find().toArray(); const apiSources = await getSourcesFromAPI(); + const dbInventory = await db.collection('inventory').find().toArray(); + + const statusUpdateCheck = ( + inventorySource: WithId, + apiSource: SourceWithoutLastConnected, + lastConnected: Date + ) => { + const databaseStatus = inventorySource.status; + const apiStatus = apiSource.status; + const currentTime = new Date().getTime(); + const lastConnectedTime = new Date(lastConnected).getTime(); + const monthInMilliseconds = 30 * 24 * 60 * 60 * 1000; + const expiryTime = lastConnectedTime + monthInMilliseconds; + + if (databaseStatus === 'purge' && apiStatus === 'gone') { + return databaseStatus; + } else if (apiStatus === 'gone' && currentTime > expiryTime) { + return 'purge'; + } else { + return apiStatus; + } + }; // Update status of all sources in the inventory to the status found in API. // If a source is not found in the API, it is marked as gone. @@ -73,12 +94,14 @@ export async function runSyncInventory() { // If source was not found in response from API, always mark it as gone return { ...inventorySource, status: 'gone' } satisfies WithId; } - // Keep all old fields from the inventory source (name, tags, id, audio_stream etc), but update the status and set the lastConnected to the current date + const lastConnected = + apiSource.status !== 'gone' ? new Date() : inventorySource.lastConnected; + + // Keep all old fields from the inventory source (name, tags, id, audio_stream etc), but update the status return { ...inventorySource, - status: apiSource.status, - lastConnected: - apiSource.status !== 'gone' ? new Date() : inventorySource.lastConnected + status: statusUpdateCheck(inventorySource, apiSource, lastConnected), + lastConnected: lastConnected } satisfies WithId; }); diff --git a/src/app/api/manager/inventory/[_id]/route.ts b/src/app/api/manager/inventory/[_id]/route.ts new file mode 100644 index 0000000..86eb382 --- /dev/null +++ b/src/app/api/manager/inventory/[_id]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { isAuthenticated } from '../../../../../api/manager/auth'; +import { Params } from 'next/dist/shared/lib/router/utils/route-matcher'; +import { purgeInventorySourceItem } from '../../../../../api/manager/inventory'; + +export async function PUT( + request: NextRequest, + { params }: { params: Params } +): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + const response = await purgeInventorySourceItem(params._id); + if (response.acknowledged && response.modifiedCount === 0) { + return new NextResponse(`Did not match requirements`, { + status: 204 + }); + } else if (response.acknowledged) { + return new NextResponse(null, { + status: 200 + }); + } else { + return new NextResponse(`Could not update database-status`, { + status: 500 + }); + } + } catch (error) { + return new NextResponse( + `Error occurred while posting to DB! Error: ${error}`, + { + status: 500 + } + ); + } +} diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index b7bd5b1..f0feddd 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -649,13 +649,13 @@ export default function ProductionConfiguration({ params }: PageProps) { /> -
+
-
+
    diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 77f557b..673a0ca 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren } from 'react'; import { twMerge } from 'tailwind-merge'; type ButtonProps = { - type?: 'submit'; + type?: 'submit' | 'button'; className?: string; onClick?: () => void; disabled?: boolean; diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index 1943bf1..7603616 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -2,6 +2,7 @@ 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'; @@ -10,13 +11,15 @@ import FilterContext from './FilterContext'; import styles from './Inventory.module.scss'; export default function Inventory() { + const [removeInventorySource, reloadList] = useSetSourceToPurge(); const [updatedSource, setUpdatedSource] = useState< SourceWithId | undefined >(); - const [sources] = useSources(updatedSource); + const [sources] = useSources(reloadList, updatedSource); const [currentSource, setCurrentSource] = useState(); const [filteredSources, setFilteredSources] = useState>(sources); + const inventoryVisible = true; useEffect(() => { @@ -25,6 +28,12 @@ export default function Inventory() { } }, [updatedSource]); + useEffect(() => { + if (reloadList) { + setCurrentSource(null); + } + }, [reloadList]); + const editSource = (source: SourceWithId) => { setCurrentSource(() => source); }; @@ -35,23 +44,25 @@ export default function Inventory() { return Array.from( filteredSources.size > 0 ? filteredSources.values() : sources.values() ).map((source, index) => { - return ( - { - editSource(source); - }} - /> - ); + if (source.status !== 'purge') { + return ( + { + editSource(source); + }} + /> + ); + } }); } return ( -
    +
      {getSourcesToDisplay(filteredSources)}
    @@ -79,11 +90,14 @@ export default function Inventory() {
    {currentSource ? ( -
    +
    setUpdatedSource(source)} close={() => setCurrentSource(null)} + removeInventorySource={(source) => removeInventorySource(source)} />
    ) : null} diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index b86ff12..80d498a 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -11,14 +11,17 @@ import { IconExclamationCircle } from '@tabler/icons-react'; export default function EditView({ source, updateSource, - close + close, + removeInventorySource }: { source: SourceWithId; updateSource: (source: SourceWithId) => void; close: () => void; + removeInventorySource: (source: SourceWithId) => void; }) { const [loaded, setLoaded] = useState(false); const src = useMemo(() => getSourceThumbnail(source), [source]); + return (
    @@ -49,8 +52,11 @@ export default function EditView({
    - - + ); } diff --git a/src/components/inventory/editView/UpdateButtons.tsx b/src/components/inventory/editView/UpdateButtons.tsx index 88c5f03..1f3309d 100644 --- a/src/components/inventory/editView/UpdateButtons.tsx +++ b/src/components/inventory/editView/UpdateButtons.tsx @@ -4,8 +4,18 @@ import { EditViewContext } from '../EditViewContext'; import { useTranslate } from '../../../i18n/useTranslate'; import styles from './animation.module.scss'; import { Loader } from '../../loader/Loader'; +import { SourceWithId } from '../../../interfaces/Source'; +import { IconTrash } from '@tabler/icons-react'; -export default function UpdateButtons({ close }: { close: () => void }) { +export default function UpdateButtons({ + close, + removeInventorySource, + source +}: { + close: () => void; + removeInventorySource: (source: SourceWithId) => void; + source: SourceWithId; +}) { const t = useTranslate(); const { saved: [saved], @@ -22,6 +32,15 @@ export default function UpdateButtons({ close }: { close: () => void }) {
    + diff --git a/src/components/layout/DefaultLayout.tsx b/src/components/layout/DefaultLayout.tsx index 9723e84..8ddef3b 100644 --- a/src/components/layout/DefaultLayout.tsx +++ b/src/components/layout/DefaultLayout.tsx @@ -7,7 +7,7 @@ export default function DefaultLayout({ }) { return (
    -
    +
    {children}
    diff --git a/src/hooks/sources/useSetSourceToPurge.tsx b/src/hooks/sources/useSetSourceToPurge.tsx new file mode 100644 index 0000000..75818cf --- /dev/null +++ b/src/hooks/sources/useSetSourceToPurge.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 useSetSourceToPurge(): 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]; +} diff --git a/src/hooks/sources/useSources.tsx b/src/hooks/sources/useSources.tsx index fb7ed1d..c3a27ce 100644 --- a/src/hooks/sources/useSources.tsx +++ b/src/hooks/sources/useSources.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; export function useSources( + deleteComplete?: boolean, updatedSource?: SourceWithId ): [Map, boolean] { const [sources, setSources] = useState>( @@ -10,7 +11,7 @@ export function useSources( const [loading, setLoading] = useState(true); useEffect(() => { - if (!updatedSource) { + if (!updatedSource || deleteComplete) { fetch('/api/manager/sources?mocked=false', { method: 'GET', // TODO: Implement api key @@ -33,6 +34,6 @@ export function useSources( } sources.set(updatedSource._id.toString(), updatedSource); setSources(new Map(sources)); - }, [updatedSource]); + }, [updatedSource, deleteComplete, sources]); return [sources, loading]; } diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index db4838f..e59afa4 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,6 +1,6 @@ import { ObjectId, WithId } from 'mongodb'; export type SourceType = 'camera' | 'graphics' | 'microphone'; -export type SourceStatus = 'ready' | 'new' | 'gone'; +export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; export type VideoStream = { height?: number; width?: number;