From e39d090e523a1be3801159a15a5dcafc25273704 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Mon, 26 Aug 2024 08:55:01 +0200 Subject: [PATCH 1/8] feat: sorting ui --- src/components/filter/FilterDropdown.tsx | 22 ++++++++++++++++++++++ src/components/filter/SortSelect.tsx | 21 +++++++++++++++++++++ src/i18n/locales/en.ts | 4 +++- src/i18n/locales/sv.ts | 4 +++- src/interfaces/Source.ts | 1 + 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/components/filter/SortSelect.tsx diff --git a/src/components/filter/FilterDropdown.tsx b/src/components/filter/FilterDropdown.tsx index 6d874d0e..45e81d61 100644 --- a/src/components/filter/FilterDropdown.tsx +++ b/src/components/filter/FilterDropdown.tsx @@ -1,5 +1,6 @@ import React, { ChangeEvent, useEffect, useState } from 'react'; import { useTranslate } from '../../i18n/useTranslate'; +import { SortSelect } from './SortSelect'; function FilterDropdown({ close, @@ -30,6 +31,11 @@ function FilterDropdown({ }) { const [searchedTypes, setSearchedTypes] = useState([]); const [searchedLocations, setSearchedLocations] = useState([]); + const [selectedValue, setSelectedValue] = useState(); + + useEffect(() => { + console.log('selected: ', selectedValue); + }, [selectedValue]); useEffect(() => { setSearchedTypes(types); @@ -54,6 +60,10 @@ function FilterDropdown({ setSelectedTags(new Set(temp)); }; + // const handleChangeSorting = (event: React.ChangeEvent) => { + // setSelectedValue(event.target.value); + // }; + function addFilterComponent(type: string, component: string, index: number) { const id = `${type}-${component}-id`; const key = `${type}-${index}`; @@ -215,6 +225,18 @@ function FilterDropdown({ })} +
+ + {t('inventory_list.sort_by')} + + ) => + setSelectedValue(e.target.value) + } + options={['Newest', 'Oldest']} + /> +
  • diff --git a/src/components/filter/SortSelect.tsx b/src/components/filter/SortSelect.tsx new file mode 100644 index 00000000..42cdd4a2 --- /dev/null +++ b/src/components/filter/SortSelect.tsx @@ -0,0 +1,21 @@ +type SelectProps = { + value: string; + onChange: (e: React.ChangeEvent) => void; + options: readonly string[]; +}; + +export const SortSelect = ({ value, onChange, options }: SelectProps) => { + return ( + + ); +}; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index ebb621de..fa09b468 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -510,7 +510,9 @@ export const en = { locations: 'Location', active_sources: 'Active Sources', add: 'Add', - edit: 'Edit' + edit: 'Edit', + sort_by: 'Sort by', + no_sorting_applied: 'No sorting selected' }, clear: 'Clear', apply: 'Apply', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index d511d36a..83a8d76c 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -512,7 +512,9 @@ export const sv = { locations: 'Plats', active_sources: 'Aktiva källor', add: 'Lägg till', - edit: 'Redigera' + edit: 'Redigera', + sort_by: 'Sortera på', + no_sorting_applied: 'Ingen sortering vald' }, clear: 'Rensa', apply: 'Applicera', diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 99e57042..e6ff762c 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -28,6 +28,7 @@ export interface Source { ingest_source_name: string; video_stream: VideoStream; audio_stream: AudioStream; + lastConnected?: Date; } export interface SourceReference { From 2446174561fe702848644235bf0fb6a65eff6faf Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 28 Aug 2024 13:01:03 +0200 Subject: [PATCH 2/8] feat: add sort based on lastConnected --- src/api/manager/job/syncInventory.ts | 88 +++++++++++++----------- src/components/filter/FilterDropdown.tsx | 65 ++++++++++++----- src/components/filter/FilterOptions.tsx | 13 ++++ src/components/filter/SortSelect.tsx | 2 +- src/i18n/locales/en.ts | 3 +- src/i18n/locales/sv.ts | 3 +- src/interfaces/Source.ts | 2 +- 7 files changed, 117 insertions(+), 59 deletions(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index e5635670..71dda455 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -5,6 +5,9 @@ import { upsertSource } from '../sources'; import { getDatabase } from '../../mongoClient/dbClient'; import { WithId } from 'mongodb'; +type SourceWithoutLastConnected = Omit; + +// TODO: getSourcesFromAPI should return ResourcesSourceResponse and changed to our model later async function getSourcesFromAPI() { const ingests = await getIngests(); const resolvedIngests = ( @@ -15,32 +18,35 @@ async function getSourcesFromAPI() { result.status === 'fulfilled' ) .map((result) => result.value); - const sources: Source[] = resolvedIngests.flatMap((ingest) => { - return ingest.sources.map( - (source) => - ({ - status: source.active ? 'new' : 'gone', - name: source.name, - type: 'camera', - tags: { - location: 'Unknown' - }, - ingest_name: ingest.name, - ingest_source_name: source.name, - video_stream: { - width: source?.video_stream?.width, - height: source?.video_stream?.height, - frame_rate: - source?.video_stream?.frame_rate_n / - source?.video_stream?.frame_rate_d - }, - audio_stream: { - number_of_channels: source?.audio_stream?.number_of_channels, - sample_rate: source?.audio_stream?.sample_rate - } - } satisfies Source) - ); - }); + + const sources: SourceWithoutLastConnected[] = resolvedIngests.flatMap( + (ingest) => { + return ingest.sources.map( + (source) => + ({ + status: source.active ? 'new' : 'gone', + name: source.name, + type: 'camera', + tags: { + location: 'Unknown' + }, + ingest_name: ingest.name, + ingest_source_name: source.name, + video_stream: { + width: source?.video_stream?.width, + height: source?.video_stream?.height, + frame_rate: + source?.video_stream?.frame_rate_n / + source?.video_stream?.frame_rate_d + }, + audio_stream: { + number_of_channels: source?.audio_stream?.number_of_channels, + sample_rate: source?.audio_stream?.sample_rate + } + } satisfies SourceWithoutLastConnected) + ); + } + ); return sources; } @@ -67,26 +73,30 @@ 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 + // 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 return { ...inventorySource, - status: apiSource.status + status: apiSource.status, + lastConnected: new Date() } satisfies WithId; }); // Look for new sources that doesn't already exist in the inventory, // these should all be added to the inventory, status of these are set in getSourcesFromAPI. - const newSourcesToUpsert = apiSources.filter((source) => { - const existingSource = dbInventoryWithCorrectStatus.find( - (inventorySource) => { - return ( - source.ingest_name === inventorySource.ingest_name && - source.ingest_source_name === inventorySource.ingest_source_name - ); - } - ); - return !existingSource; - }); + + const newSourcesToUpsert = apiSources + .filter((source) => { + const existingSource = dbInventoryWithCorrectStatus.find( + (inventorySource) => { + return ( + source.ingest_name === inventorySource.ingest_name && + source.ingest_source_name === inventorySource.ingest_source_name + ); + } + ); + return !existingSource; + }) + .map((source) => ({ ...source, lastConnected: new Date() })); const sourcesToUpsert = [ ...newSourcesToUpsert, diff --git a/src/components/filter/FilterDropdown.tsx b/src/components/filter/FilterDropdown.tsx index 45e81d61..bcc17e0e 100644 --- a/src/components/filter/FilterDropdown.tsx +++ b/src/components/filter/FilterDropdown.tsx @@ -1,6 +1,7 @@ import React, { ChangeEvent, useEffect, useState } from 'react'; import { useTranslate } from '../../i18n/useTranslate'; import { SortSelect } from './SortSelect'; +import { IconArrowsSort } from '@tabler/icons-react'; function FilterDropdown({ close, @@ -14,7 +15,8 @@ function FilterDropdown({ setIsTypeHidden, setIsLocationHidden, setSelectedTags, - setOnlyShowActiveSources: setOnlyShowConfigSources + setOnlyShowActiveSources: setOnlyShowConfigSources, + handleSorting }: { close: () => void; types: string[]; @@ -28,20 +30,31 @@ function FilterDropdown({ setIsLocationHidden: React.Dispatch>; setOnlyShowActiveSources: React.Dispatch>; setSelectedTags: React.Dispatch>>; + handleSorting: (reversedOrder: boolean) => void; }) { + const t = useTranslate(); + const [searchedTypes, setSearchedTypes] = useState([]); const [searchedLocations, setSearchedLocations] = useState([]); - const [selectedValue, setSelectedValue] = useState(); - - useEffect(() => { - console.log('selected: ', selectedValue); - }, [selectedValue]); + const [selectedValue, setSelectedValue] = useState( + t('inventory_list.no_sorting_applied') + ); + const [reverseSortOrder, setReverseSortOrder] = useState(false); useEffect(() => { setSearchedTypes(types); setSearchedLocations(locations); }, [types, locations]); + useEffect(() => { + if ( + selectedValue === t('inventory_list.no_sorting_applied') && + reverseSortOrder + ) { + setReverseSortOrder(false); + } + }, [selectedValue, reverseSortOrder]); + const hideLocationDiv = () => { setIsLocationHidden(true); }; @@ -60,9 +73,13 @@ function FilterDropdown({ setSelectedTags(new Set(temp)); }; - // const handleChangeSorting = (event: React.ChangeEvent) => { - // setSelectedValue(event.target.value); - // }; + useEffect(() => { + if ( + reverseSortOrder || + selectedValue === t('inventory_list.most_recent_connection') + ) + handleSorting(reverseSortOrder); + }, [reverseSortOrder, selectedValue]); function addFilterComponent(type: string, component: string, index: number) { const id = `${type}-${component}-id`; @@ -132,7 +149,6 @@ function FilterDropdown({ setSearchedLocations(temp); }; - const t = useTranslate(); return ( -
    +
    {t('inventory_list.sort_by')} ) => - setSelectedValue(e.target.value) - } - options={['Newest', 'Oldest']} + value={selectedValue} + onChange={(e) => { + setSelectedValue(e.target.value); + handleSorting(reverseSortOrder); + }} + options={[ + t('inventory_list.no_sorting_applied'), + t('inventory_list.most_recent_connection') + ]} /> +
  • diff --git a/src/components/filter/FilterOptions.tsx b/src/components/filter/FilterOptions.tsx index 48dbe9c0..c51ee355 100644 --- a/src/components/filter/FilterOptions.tsx +++ b/src/components/filter/FilterOptions.tsx @@ -100,6 +100,18 @@ function FilterOptions({ } }; + const handleSorting = (reversedOrder: boolean) => { + const sortedSourcesArray = Array.from(tempSet.values()).sort((a, b) => { + const dateA = new Date(a.lastConnected).getTime(); + const dateB = new Date(b.lastConnected).getTime(); + return reversedOrder ? dateA - dateB : dateB - dateA; + }); + tempSet = new Map( + sortedSourcesArray.map((source) => [source._id.toString(), source]) + ); + onFilteredSources(tempSet); + }; + return ( { @@ -127,6 +139,7 @@ function FilterOptions({ setIsLocationHidden={setIsLocationHidden} setSelectedTags={setSelectedTags} setOnlyShowActiveSources={setOnlyShowActiveSources} + handleSorting={handleSorting} /> diff --git a/src/components/filter/SortSelect.tsx b/src/components/filter/SortSelect.tsx index 42cdd4a2..6b67671d 100644 --- a/src/components/filter/SortSelect.tsx +++ b/src/components/filter/SortSelect.tsx @@ -7,7 +7,7 @@ type SelectProps = { export const SortSelect = ({ value, onChange, options }: SelectProps) => { return ( Date: Thu, 29 Aug 2024 11:43:26 +0200 Subject: [PATCH 5/8] fix: add date to source card --- src/components/inventory/EditViewContext.tsx | 8 +++++++- src/components/inventory/editView/GeneralSettings.tsx | 9 +++++++++ src/components/sourceListItem/SourceListItem.tsx | 4 ++++ src/i18n/locales/en.ts | 3 ++- src/i18n/locales/sv.ts | 3 ++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/inventory/EditViewContext.tsx b/src/components/inventory/EditViewContext.tsx index f5b78ba7..182f2eac 100644 --- a/src/components/inventory/EditViewContext.tsx +++ b/src/components/inventory/EditViewContext.tsx @@ -22,6 +22,7 @@ export interface IInput { name: string; location: string; type: SourceType | ''; + lastConnected: Date | ''; audioMapping?: Numbers[]; } @@ -41,7 +42,10 @@ interface IContext { } export const EditViewContext = createContext({ - input: [{ name: '', location: '', type: '', audioMapping: [] }, () => null], + input: [ + { name: '', location: '', type: '', lastConnected: '', audioMapping: [] }, + () => null + ], saved: [undefined, () => null], loading: false, isSame: true, @@ -66,6 +70,7 @@ export default function Context({ name: source.name, location: source.tags.location, type: source.type, + lastConnected: source.lastConnected, // audioMapping: source?.stream_settings?.audio_mapping || [] audioMapping: source?.audio_stream.audio_mapping || [] }); @@ -81,6 +86,7 @@ export default function Context({ name: source.name, location: source.tags.location, type: source.type, + lastConnected: source.lastConnected, // audioMapping: source?.stream_settings?.audio_mapping || [] audioMapping: source?.audio_stream.audio_mapping || [] })); diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index 0090d17d..2210064a 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -70,6 +70,15 @@ export default function GeneralSettings() { +
    +

    + {t('source.last_connected')} +

    +
    +

    {new Date(input.location).toLocaleString()}

    +
    +
    + {height && width && (

    {t('video')}

    diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index a76ca053..6e9aadf7 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -129,6 +129,10 @@ function InventoryListItem({ : capitalize(source.tags.location) })} +

    + {t('source.last_connected')}:{' '} + {new Date(source.lastConnected).toLocaleString()} +

    {t('source.ingest', { ingest: source.ingest_name diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index bd67b67c..2467307c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -45,7 +45,8 @@ export const en = { audio: 'Audio: {{audio}}', orig: 'Original Name: {{name}}', metadata: 'Source Metadata', - location_unknown: 'Unknown' + location_unknown: 'Unknown', + last_connected: 'Last connection' }, delete_source_status: { delete_stream: 'Delete stream', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index e4619fd5..585d3739 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -47,7 +47,8 @@ export const sv = { audio: 'Ljud: {{audio}}', orig: 'Enhetsnamn: {{name}}', metadata: 'Käll-metadata', - location_unknown: 'Okänd' + location_unknown: 'Okänd', + last_connected: 'Senast uppkoppling' }, delete_source_status: { delete_stream: 'Radera ström', From 4ce566326e5158f760df7daa29701d8bd1ec5ef7 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Thu, 29 Aug 2024 11:50:09 +0200 Subject: [PATCH 6/8] fixup! --- src/components/inventory/editView/GeneralSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index 2210064a..ae2c4c66 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -75,7 +75,7 @@ export default function GeneralSettings() { {t('source.last_connected')}

    -

    {new Date(input.location).toLocaleString()}

    +

    {new Date(input.lastConnected).toLocaleString()}

    From ad64c9d74cfd59935750ac3985384889d4911c7f Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 30 Aug 2024 09:24:57 +0200 Subject: [PATCH 7/8] fix: add status-not-gone check --- 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 71dda455..273551e2 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -77,7 +77,7 @@ export async function runSyncInventory() { return { ...inventorySource, status: apiSource.status, - lastConnected: new Date() + lastConnected: apiSource.status !== 'gone' ? new Date() : inventorySource.lastConnected } satisfies WithId; }); From 23477518b33c67c439a516a8e05b5d529f778815 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 30 Aug 2024 09:28:44 +0200 Subject: [PATCH 8/8] fix: linting error --- src/api/manager/job/syncInventory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 273551e2..40ae637d 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -77,7 +77,8 @@ export async function runSyncInventory() { return { ...inventorySource, status: apiSource.status, - lastConnected: apiSource.status !== 'gone' ? new Date() : inventorySource.lastConnected + lastConnected: + apiSource.status !== 'gone' ? new Date() : inventorySource.lastConnected } satisfies WithId; });