Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Source management sorting #3

Merged
merged 8 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 49 additions & 39 deletions src/api/manager/job/syncInventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { upsertSource } from '../sources';
import { getDatabase } from '../../mongoClient/dbClient';
import { WithId } from 'mongodb';

type SourceWithoutLastConnected = Omit<Source, 'lastConnected'>;

// TODO: getSourcesFromAPI should return ResourcesSourceResponse and changed to our model later
async function getSourcesFromAPI() {
const ingests = await getIngests();
const resolvedIngests = (
Expand All @@ -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;
}

Expand All @@ -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<Source>;
}
// 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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just learned ( yesterday ) that the API now actually stores some data about previously connected sources ( they used to just disappear... ) so we have check the status here and only update lastConnected if status !== "gone".

} satisfies WithId<Source>;
});

// 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,
Expand Down
58 changes: 56 additions & 2 deletions src/components/filter/FilterDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +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,
Expand All @@ -13,7 +15,8 @@ function FilterDropdown({
setIsTypeHidden,
setIsLocationHidden,
setSelectedTags,
setOnlyShowActiveSources: setOnlyShowConfigSources
setOnlyShowActiveSources: setOnlyShowConfigSources,
handleSorting
}: {
close: () => void;
types: string[];
Expand All @@ -27,15 +30,31 @@ function FilterDropdown({
setIsLocationHidden: React.Dispatch<React.SetStateAction<boolean>>;
setOnlyShowActiveSources: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string>>>;
handleSorting: (reversedOrder: boolean) => void;
}) {
const t = useTranslate();

const [searchedTypes, setSearchedTypes] = useState<string[]>([]);
const [searchedLocations, setSearchedLocations] = useState<string[]>([]);
const [selectedValue, setSelectedValue] = useState<string>(
t('inventory_list.no_sorting_applied')
);
const [reverseSortOrder, setReverseSortOrder] = useState<boolean>(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);
};
Expand All @@ -54,6 +73,14 @@ function FilterDropdown({
setSelectedTags(new Set<string>(temp));
};

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`;
const key = `${type}-${index}`;
Expand Down Expand Up @@ -122,7 +149,6 @@ function FilterDropdown({
setSearchedLocations(temp);
};

const t = useTranslate();
return (
<div
id="dropdownDefaultCheckbox"
Expand Down Expand Up @@ -215,6 +241,34 @@ function FilterDropdown({
})}
</ul>
</div>
<div className="flex items-center mt-2 text-p">
<span className="flex min-w-[20%] mr-5">
{t('inventory_list.sort_by')}
</span>
<SortSelect
value={selectedValue}
onChange={(e) => {
setSelectedValue(e.target.value);
}}
options={[
t('inventory_list.no_sorting_applied'),
t('inventory_list.most_recent_connection')
]}
/>
<button
className={`ml-2 p-1 rounded-md ${
selectedValue === t('inventory_list.no_sorting_applied')
? 'text-white/50'
: 'text-white'
} ${reverseSortOrder ? 'bg-zinc-800' : 'bg-zinc-600'}`}
onClick={() => setReverseSortOrder(!reverseSortOrder)}
disabled={
selectedValue === t('inventory_list.no_sorting_applied')
}
>
<IconArrowsSort />
</button>
</div>
</li>
<li className="relative rounded w-full px-3">
<div className="flex flex-row justify-between mt-4">
Expand Down
13 changes: 13 additions & 0 deletions src/components/filter/FilterOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ClickAwayListener
onClickAway={() => {
Expand Down Expand Up @@ -127,6 +139,7 @@ function FilterOptions({
setIsLocationHidden={setIsLocationHidden}
setSelectedTags={setSelectedTags}
setOnlyShowActiveSources={setOnlyShowActiveSources}
handleSorting={handleSorting}
/>
</div>
</ClickAwayListener>
Expand Down
21 changes: 21 additions & 0 deletions src/components/filter/SortSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type SortSelectProps = {
value: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
options: readonly string[];
};

export const SortSelect = ({ value, onChange, options }: SortSelectProps) => {
return (
<select
className="border justify-center text-sm rounded-lg w-1/2 pl-2 py-1.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-p"
value={value}
onChange={onChange}
>
{options.map((value) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
);
};
8 changes: 7 additions & 1 deletion src/components/inventory/EditViewContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface IInput {
name: string;
location: string;
type: SourceType | '';
lastConnected: Date | '';
audioMapping?: Numbers[];
}

Expand All @@ -41,7 +42,10 @@ interface IContext {
}

export const EditViewContext = createContext<IContext>({
input: [{ name: '', location: '', type: '', audioMapping: [] }, () => null],
input: [
{ name: '', location: '', type: '', lastConnected: '', audioMapping: [] },
() => null
],
saved: [undefined, () => null],
loading: false,
isSame: true,
Expand All @@ -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 || []
});
Expand All @@ -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 || []
}));
Expand Down
9 changes: 9 additions & 0 deletions src/components/inventory/editView/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ export default function GeneralSettings() {
</div>
</div>

<div className="flex mb-5">
<h2 className="flex w-[100px] items-center">
{t('source.last_connected')}
</h2>
<div className="flex-col">
<p>{new Date(input.lastConnected).toLocaleString()}</p>
</div>
</div>

{height && width && (
<div className="flex mb-5">
<h2 className="flex w-[100px] items-center">{t('video')}</h2>
Expand Down
4 changes: 4 additions & 0 deletions src/components/sourceListItem/SourceListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ function InventoryListItem({
: capitalize(source.tags.location)
})}
</h2>
<h2 className="text-sm">
{t('source.last_connected')}:{' '}
{new Date(source.lastConnected).toLocaleString()}
</h2>
<h2 className="text-xs">
{t('source.ingest', {
ingest: source.ingest_name
Expand Down
8 changes: 6 additions & 2 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -510,7 +511,10 @@ 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',
most_recent_connection: 'Most recent connection'
},
clear: 'Clear',
apply: 'Apply',
Expand Down
8 changes: 6 additions & 2 deletions src/i18n/locales/sv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -512,7 +513,10 @@ 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',
most_recent_connection: 'Senast anslutning'
},
clear: 'Rensa',
apply: 'Applicera',
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Source {
ingest_source_name: string;
video_stream: VideoStream;
audio_stream: AudioStream;
lastConnected: Date;
}

export interface SourceReference {
Expand Down
Loading