Skip to content

Commit

Permalink
feat(recordings): Add search filters to target recording tables (#486)
Browse files Browse the repository at this point in the history
* feat(recordings): add empty filter components

* add datetime picker props

* fix filter chip indexing

* implement delete name filters

* implement duration setter

* add recording state chips

* Add units to category labels

* extract/format dateRange, add duration chips

* rename filter component

* attempt to fix callback deps

* hoist filtering logic to table

* extract name filter to typeahead select

* fix available date range

* add label filter

* label fixup

* filter archived recordings

* set date picker timezone to UTC

* split up date range into before or after date

* clean up filter setters and fix empty label query

* reuse filter logic for both tables

* fixup! clean up filter setters and fix empty label query

* mock filters in unit tests

* fix license header

* fix typo and minor refactor

* revert all archive view, fix target archive clear filter
  • Loading branch information
Janelle Law authored Jul 28, 2022
1 parent 43fe7b5 commit 6a70f83
Show file tree
Hide file tree
Showing 12 changed files with 879 additions and 35 deletions.
76 changes: 61 additions & 15 deletions src/app/Recordings/ActiveRecordingsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,48 @@ import { concatMap, filter, first } from 'rxjs/operators';
import { LabelCell } from '../RecordingMetadata/LabelCell';
import { RecordingActions } from './RecordingActions';
import { RecordingLabelsPanel } from './RecordingLabelsPanel';
import { filterRecordings, RecordingFilters } from './RecordingFilters';
import { RecordingsTable } from './RecordingsTable';
import { ReportFrame } from './ReportFrame';
import { DeleteWarningModal } from '../Modal/DeleteWarningModal';
import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils';

export enum PanelContent {
LABELS,
}
export interface ActiveRecordingsTableProps {
archiveEnabled: boolean;
}

export interface RecordingFiltersCategories {
Name: string[],
Labels: string[],
State?: RecordingState[],
StartedBeforeDate?: string[],
StartedAfterDate?: string[],
DurationSeconds?: string[],
}

export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTableProps> = (props) => {
const context = React.useContext(ServiceContext);
const routerHistory = useHistory();

const [recordings, setRecordings] = React.useState([] as ActiveRecording[]);
const [filteredRecordings, setFilteredRecordings] = React.useState([] as ActiveRecording[]);
const [headerChecked, setHeaderChecked] = React.useState(false);
const [checkedIndices, setCheckedIndices] = React.useState([] as number[]);
const [expandedRows, setExpandedRows] = React.useState([] as string[]);
const [showDetailsPanel, setShowDetailsPanel] = React.useState(false);
const [warningModalOpen, setWarningModalOpen] = React.useState(false);
const [panelContent, setPanelContent] = React.useState(PanelContent.LABELS);
const [filters, setFilters] = React.useState({
Name: [],
Labels: [],
State: [],
StartedBeforeDate: [],
StartedAfterDate: [],
DurationSeconds: [],
} as RecordingFiltersCategories);
const [isLoading, setIsLoading] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const { url } = useRouteMatch();
Expand All @@ -95,15 +118,16 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl

const handleHeaderCheck = React.useCallback((event, checked) => {
setHeaderChecked(checked);
setCheckedIndices(checked ? Array.from(new Array(recordings.length), (x, i) => i) : []);
}, [setHeaderChecked, setCheckedIndices, recordings]);
setCheckedIndices(checked ? Array.from(new Array(filteredRecordings.length), (x, i) => i) : []);
}, [setHeaderChecked, setCheckedIndices, filteredRecordings]);

const handleCreateRecording = React.useCallback(() => {
routerHistory.push(`${url}/create`);
}, [routerHistory]);

const handleEditLabels = React.useCallback(() => {
setShowDetailsPanel(true);
setPanelContent(PanelContent.LABELS);
}, [setShowDetailsPanel]);

const handleRecordings = React.useCallback((recordings) => {
Expand Down Expand Up @@ -243,7 +267,7 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl

const handleArchiveRecordings = React.useCallback(() => {
const tasks: Observable<boolean>[] = [];
recordings.forEach((r: ActiveRecording, idx) => {
filteredRecordings.forEach((r: ActiveRecording, idx) => {
if (checkedIndices.includes(idx)) {
handleRowCheck(false, idx);
tasks.push(
Expand All @@ -254,11 +278,11 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
addSubscription(
forkJoin(tasks).subscribe(() => {} /* do nothing */, window.console.error)
);
}, [recordings, checkedIndices, handleRowCheck, context.api, addSubscription]);
}, [filteredRecordings, checkedIndices, handleRowCheck, context.api, addSubscription]);

const handleStopRecordings = React.useCallback(() => {
const tasks: Observable<boolean>[] = [];
recordings.forEach((r: ActiveRecording, idx) => {
filteredRecordings.forEach((r: ActiveRecording, idx) => {
if (checkedIndices.includes(idx)) {
handleRowCheck(false, idx);
if (r.state === RecordingState.RUNNING || r.state === RecordingState.STARTING) {
Expand All @@ -271,11 +295,11 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
addSubscription(
forkJoin(tasks).subscribe((() => {} /* do nothing */), window.console.error)
);
}, [recordings, checkedIndices, handleRowCheck, context.api, addSubscription]);
}, [filteredRecordings, checkedIndices, handleRowCheck, context.api, addSubscription]);

const handleDeleteRecordings = React.useCallback(() => {
const tasks: Observable<{}>[] = [];
recordings.forEach((r: ActiveRecording, idx) => {
filteredRecordings.forEach((r: ActiveRecording, idx) => {
if (checkedIndices.includes(idx)) {
context.reports.delete(r);
tasks.push(
Expand All @@ -286,7 +310,23 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
addSubscription(
forkJoin(tasks).subscribe((() => {} /* do nothing */), window.console.error)
);
}, [recordings, checkedIndices, context.reports, context.api, addSubscription]);
}, [filteredRecordings, checkedIndices, context.reports, context.api, addSubscription]);


const handleClearFilters = React.useCallback(() => {
setFilters({
Name: [],
Labels: [],
State: [],
StartedBeforeDate: [],
StartedAfterDate: [],
DurationSeconds: [],
} as RecordingFiltersCategories);
}, [setFilters]);

React.useEffect(() => {
setFilteredRecordings(filterRecordings(recordings, filters));
}, [recordings, filters]);

React.useEffect(() => {
if (!context.settings.autoRefreshEnabled()) {
Expand Down Expand Up @@ -445,10 +485,10 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
if (!checkedIndices.length) {
return true;
}
const filtered = recordings.filter((r: ActiveRecording, idx: number) => checkedIndices.includes(idx));
const filtered = filteredRecordings.filter((r: ActiveRecording, idx: number) => checkedIndices.includes(idx));
const anyRunning = filtered.some((r: ActiveRecording) => r.state === RecordingState.RUNNING || r.state == RecordingState.STARTING);
return !anyRunning;
}, [checkedIndices, recordings]);
}, [checkedIndices, filteredRecordings]);

const buttons = React.useMemo(() => {
const arr = [
Expand Down Expand Up @@ -489,8 +529,9 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
}, [recordings, checkedIndices]);

return (
<Toolbar id="active-recordings-toolbar">
<Toolbar id="active-recordings-toolbar" clearAllFilters={handleClearFilters}>
<ToolbarContent>
<RecordingFilters filters={filters} setFilters={setFilters} />
{ buttons }
{ deleteActiveWarningModal }
</ToolbarContent>
Expand All @@ -499,8 +540,8 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
};

const recordingRows = React.useMemo(() => {
return recordings.map((r, idx) => <RecordingRow key={idx} recording={r} index={idx}/>)
}, [recordings, expandedRows, checkedIndices]);
return filteredRecordings.map((r, idx) => <RecordingRow key={idx} recording={r} index={idx}/>)
}, [filteredRecordings, expandedRows, checkedIndices]);

const LabelsPanel = React.useMemo(() => (
<RecordingLabelsPanel
Expand All @@ -512,8 +553,11 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl

return (
<Drawer isExpanded={showDetailsPanel} isInline>
{/* TODO change drawer panel content depending on which RecordingsToolbar button was clicked */}
<DrawerContent panelContent={LabelsPanel} className='recordings-table-drawer-content'>
<DrawerContent panelContent={
{
[PanelContent.LABELS]: LabelsPanel,
}[panelContent]
} className='recordings-table-drawer-content'>
<DrawerContentBody hasPadding>
<RecordingsTable
tableTitle="Active Flight Recordings"
Expand All @@ -522,6 +566,8 @@ export const ActiveRecordingsTable: React.FunctionComponent<ActiveRecordingsTabl
isHeaderChecked={headerChecked}
onHeaderCheck={handleHeaderCheck}
isEmpty={!recordings.length}
isEmptyFilterResult={!filteredRecordings.length}
clearFilters={handleClearFilters}
isLoading ={isLoading}
errorMessage ={errorMessage}
>
Expand Down
36 changes: 28 additions & 8 deletions src/app/Recordings/ArchivedRecordingsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,25 @@ import { LabelCell } from '../RecordingMetadata/LabelCell';
import { RecordingLabelsPanel } from './RecordingLabelsPanel';
import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal';
import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils';
import { RecordingFiltersCategories } from './ActiveRecordingsTable';
import { filterRecordings, RecordingFilters } from './RecordingFilters';

export interface ArchivedRecordingsTableProps { }

export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordingsTableProps> = () => {
const context = React.useContext(ServiceContext);

const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]);
const [filteredRecordings, setFilteredRecordings] = React.useState([] as ArchivedRecording[]);
const [headerChecked, setHeaderChecked] = React.useState(false);
const [checkedIndices, setCheckedIndices] = React.useState([] as number[]);
const [expandedRows, setExpandedRows] = React.useState([] as string[]);
const [showDetailsPanel, setShowDetailsPanel] = React.useState(false);
const [warningModalOpen, setWarningModalOpen] = React.useState(false);
const [filters, setFilters] = React.useState({
Name: [],
Labels: [],
} as RecordingFiltersCategories);
const [isLoading, setIsLoading] = React.useState(false);
const addSubscription = useSubscriptions();

Expand All @@ -75,8 +82,8 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings

const handleHeaderCheck = React.useCallback((event, checked) => {
setHeaderChecked(checked);
setCheckedIndices(checked ? Array.from(new Array(recordings.length), (x, i) => i) : []);
}, [setHeaderChecked, setCheckedIndices, recordings]);
setCheckedIndices(checked ? Array.from(new Array(filteredRecordings.length), (x, i) => i) : []);
}, [setHeaderChecked, setCheckedIndices, filteredRecordings]);

const handleRowCheck = React.useCallback((checked, index) => {
if (checked) {
Expand Down Expand Up @@ -126,6 +133,13 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings
);
}, [addSubscription, context, context.api, setIsLoading, handleRecordings]);

const handleClearFilters = React.useCallback(() => {
setFilters({
Name: [],
Labels: [],
} as RecordingFiltersCategories);
}, [setFilters]);

React.useEffect(() => {
addSubscription(
context.target.target().subscribe(refreshRecordingList)
Expand Down Expand Up @@ -197,7 +211,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings

const handleDeleteRecordings = React.useCallback(() => {
const tasks: Observable<any>[] = [];
recordings.forEach((r: ArchivedRecording, idx) => {
filteredRecordings.forEach((r: ArchivedRecording, idx) => {
if (checkedIndices.includes(idx)) {
context.reports.delete(r);
tasks.push(
Expand All @@ -208,13 +222,17 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings
addSubscription(
forkJoin(tasks).subscribe()
);
}, [recordings, checkedIndices, context.reports, context.api, addSubscription]);
}, [filteredRecordings, checkedIndices, context.reports, context.api, addSubscription]);

const toggleExpanded = (id) => {
const idx = expandedRows.indexOf(id);
setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]);
};

React.useEffect(() => {
setFilteredRecordings(filterRecordings(recordings, filters));
}, [recordings, filters]);

React.useEffect(() => {
if (!context.settings.autoRefreshEnabled()) {
return;
Expand Down Expand Up @@ -327,8 +345,9 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings
}, [recordings, checkedIndices]);

return (
<Toolbar id="archived-recordings-toolbar">
<Toolbar id="archived-recordings-toolbar" clearAllFilters={handleClearFilters}>
<ToolbarContent>
<RecordingFilters filters={filters} setFilters={setFilters} />
<ToolbarGroup variant="button-group">
<ToolbarItem>
<Button key="edit labels" variant="secondary" onClick={handleEditLabels} isDisabled={!checkedIndices.length}>Edit Labels</Button>
Expand All @@ -344,8 +363,8 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings
};

const recordingRows = React.useMemo(() => {
return recordings.map((r, idx) => <RecordingRow key={idx} recording={r} index={idx}/>)
}, [recordings, expandedRows, checkedIndices]);
return filteredRecordings.map((r, idx) => <RecordingRow key={idx} recording={r} index={idx}/>)
}, [filteredRecordings, expandedRows, checkedIndices]);

const LabelsPanel = React.useMemo(() => (
<RecordingLabelsPanel
Expand All @@ -357,7 +376,6 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings

return (
<Drawer isExpanded={showDetailsPanel} isInline>
{/* TODO change drawer panel content depending on which RecordingsToolbar button was clicked */}
<DrawerContent panelContent={LabelsPanel} className='recordings-table-drawer-content'>
<DrawerContentBody hasPadding>
<RecordingsTable
Expand All @@ -368,6 +386,8 @@ export const ArchivedRecordingsTable: React.FunctionComponent<ArchivedRecordings
onHeaderCheck={handleHeaderCheck}
isLoading={isLoading}
isEmpty={!recordings.length}
isEmptyFilterResult={!filteredRecordings.length}
clearFilters={handleClearFilters}
errorMessage=''
>
{recordingRows}
Expand Down
Loading

0 comments on commit 6a70f83

Please sign in to comment.