From c3a4259393d85626249581aac0dacc2453ec1ff1 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 19 Apr 2022 18:47:00 -0400 Subject: [PATCH 01/69] Populate TreeView with Targets --- .../AllArchivedRecordingsTreeView.tsx | 94 +++++++++++++++++++ src/app/Archives/Archives.tsx | 3 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/app/Archives/AllArchivedRecordingsTreeView.tsx diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx new file mode 100644 index 000000000..353edc576 --- /dev/null +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -0,0 +1,94 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { TreeView , TreeViewDataItem } from '@patternfly/react-core'; +import { ArchivedRecording } from '@app/Shared/Services/Api.service'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { Target } from '@app/Shared/Services/Target.service'; +import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; +import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { RecordingActions } from '@app/Recordings/RecordingActions'; +import { RecordingsTable } from '@app/Recordings/RecordingsTable'; +import { ReportFrame } from '@app/Recordings/ReportFrame'; +import { Observable, forkJoin, merge } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { PlusIcon } from '@patternfly/react-icons'; +import { ArchiveUploadModal } from './ArchiveUploadModal'; +import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; + +export interface AllArchivedRecordingsTreeViewProps { } + +export const AllArchivedRecordingsTreeView: React.FunctionComponent = () => { + const context = React.useContext(ServiceContext); + const [targets, setTargets] = React.useState([{name : 'Uploaded'}] as TreeViewDataItem[]); + const [isLoading, setLoading] = React.useState(false); + const addSubscription = useSubscriptions(); + + React.useEffect(() => { + const sub = context.targets.targets().subscribe((targets) => { + targets.map((t: Target) => ( + (t.alias == t.connectUrl) || !t.alias ? + setTargets(old => old.concat([{ name: `${t.connectUrl}`}])) + : + setTargets(old => old.concat([{ name: `${t.alias} (${t.connectUrl})` }])) + )) + }); + return () => sub.unsubscribe(); + }, [context, context.targets, setTargets]); + + const refreshTargetList = React.useCallback(() => { + setLoading(true); + addSubscription( + context.targets.queryForTargets().subscribe(() => setLoading(false)) + ); + }, [setLoading, addSubscription, context.targets]); + + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval(() => refreshTargetList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + return () => window.clearInterval(id); + }, [context.target, context.settings, refreshTargetList]); + + return (<> + + ); +}; \ No newline at end of file diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 3e5633cb1..7b6054cf0 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -40,6 +40,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Card, CardBody, CardHeader, EmptyState, EmptyStateIcon, Text, TextVariants, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { AllArchivedRecordingsTable } from './AllArchivedRecordingsTable'; +import { AllArchivedRecordingsTreeView } from './AllArchivedRecordingsTreeView'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; export const Archives = () => { @@ -64,7 +65,7 @@ export const Archives = () => { } return (<> Archived Recordings (All Targets) - + ); }, [archiveEnabled]); From 044fb034ec1e1af8ae5829f20d68a860e4b6c053 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 20 Apr 2022 17:33:28 -0400 Subject: [PATCH 02/69] Formatting --- src/app/Archives/AllArchivedRecordingsTreeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 353edc576..ad323e980 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -65,7 +65,7 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { targets.map((t: Target) => ( (t.alias == t.connectUrl) || !t.alias ? - setTargets(old => old.concat([{ name: `${t.connectUrl}`}])) + setTargets(old => old.concat([{ name: `${t.connectUrl}` }])) : setTargets(old => old.concat([{ name: `${t.alias} (${t.connectUrl})` }])) )) From eba85ee81e8123f225913865193941115cd4c5e5 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 4 May 2022 17:56:50 -0400 Subject: [PATCH 03/69] Start converting TreeView into a nested Table --- .../AllArchivedRecordingsTreeView.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index ad323e980..3160d8a95 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -43,7 +43,7 @@ import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; -import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { RecordingActions } from '@app/Recordings/RecordingActions'; import { RecordingsTable } from '@app/Recordings/RecordingsTable'; import { ReportFrame } from '@app/Recordings/ReportFrame'; @@ -57,18 +57,13 @@ export interface AllArchivedRecordingsTreeViewProps { } export const AllArchivedRecordingsTreeView: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); - const [targets, setTargets] = React.useState([{name : 'Uploaded'}] as TreeViewDataItem[]); + const [targets, setTargets] = React.useState([] as Target[]); const [isLoading, setLoading] = React.useState(false); const addSubscription = useSubscriptions(); React.useEffect(() => { const sub = context.targets.targets().subscribe((targets) => { - targets.map((t: Target) => ( - (t.alias == t.connectUrl) || !t.alias ? - setTargets(old => old.concat([{ name: `${t.connectUrl}` }])) - : - setTargets(old => old.concat([{ name: `${t.alias} (${t.connectUrl})` }])) - )) + setTargets(targets); }); return () => sub.unsubscribe(); }, [context, context.targets, setTargets]); @@ -88,7 +83,28 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent window.clearInterval(id); }, [context.target, context.settings, refreshTargetList]); + let name; return (<> - + + + + + Target + + + + + {targets.map((t: Target) => ( + + + {(t.alias == t.connectUrl) || !t.alias ? + `${t.connectUrl}` + : + `${t.alias} (${t.connectUrl})`} + + + ))} + + ); }; \ No newline at end of file From 16160c94119daf0ce1115c6674308f96095f0ae6 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 4 May 2022 19:21:27 -0400 Subject: [PATCH 04/69] Start building nested table --- .../AllArchivedRecordingsTreeView.tsx | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 3160d8a95..11fa631f0 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -57,10 +57,16 @@ export interface AllArchivedRecordingsTreeViewProps { } export const AllArchivedRecordingsTreeView: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); + const [targets, setTargets] = React.useState([] as Target[]); + const [expandedRows, setExpandedRows] = React.useState([] as string[]); const [isLoading, setLoading] = React.useState(false); const addSubscription = useSubscriptions(); + const tableColumns: string[] = [ + 'Target', + ]; + React.useEffect(() => { const sub = context.targets.targets().subscribe((targets) => { setTargets(targets); @@ -83,28 +89,53 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent window.clearInterval(id); }, [context.target, context.settings, refreshTargetList]); - let name; + const toggleExpanded = (id) => { + const idx = expandedRows.indexOf(id); + setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); + }; + + const TargetRow = (props) => { + const expandedRowId =`target-table-row-${props.index}-exp`; + const handleToggle = () => { + toggleExpanded(expandedRowId); + }; + + const isExpanded = React.useMemo(() => { + return expandedRows.includes(expandedRowId); + }, [expandedRows, expandedRowId]); + + const parentRow = React.useMemo(() => { + return( + + + + + + {(t.alias == t.connectUrl) || !t.alias ? + `${t.connectUrl}` + : + `${t.alias} (${t.connectUrl})`} + + + ); + }, [props.target, props.index, isExpanded, ]); + } + + const targetRows = React.useMemo(() => { + return targets.map((t, idx) => ) + }, [targets, expandedRows]); + return (<> - - Target - + + {tableColumns.map((key , idx) => ( + {key} + ))} - - {targets.map((t: Target) => ( - - - {(t.alias == t.connectUrl) || !t.alias ? - `${t.connectUrl}` - : - `${t.alias} (${t.connectUrl})`} - - - ))} - + {targetRows} ); }; \ No newline at end of file From b254ce5bbab39bc3a6eff56382a8e5aadfffad36 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 5 May 2022 13:03:14 -0400 Subject: [PATCH 05/69] Make target rows expandable --- .../AllArchivedRecordingsTreeView.tsx | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 11fa631f0..3166e9fd3 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -94,6 +94,10 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); }; + const RecordingRow = (props) => { + + } + const TargetRow = (props) => { const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { @@ -107,18 +111,31 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { return( - - - - - {(t.alias == t.connectUrl) || !t.alias ? - `${t.connectUrl}` + + + {(props.target.alias == props.target.connectUrl) || !props.target.alias ? + `${props.target.connectUrl}` : - `${t.alias} (${t.connectUrl})`} + `${props.target.alias} (${props.target.connectUrl})`} ); - }, [props.target, props.index, isExpanded, ]); + }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); + + return ( + + {parentRow} + + ); } const targetRows = React.useMemo(() => { From 78671ff01736436f08b124eabd20f7c1c2275ea7 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 5 May 2022 14:57:41 -0400 Subject: [PATCH 06/69] Add a nested archived recordings table; refresh is not working, however --- .../AllArchivedRecordingsTreeView.tsx | 30 +- .../NestedArchivedRecordingsTable.tsx | 334 ++++++++++++++++++ 2 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 src/app/Archives/NestedArchivedRecordingsTable.tsx diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 3166e9fd3..8364cba93 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -52,6 +52,7 @@ import { first } from 'rxjs/operators'; import { PlusIcon } from '@patternfly/react-icons'; import { ArchiveUploadModal } from './ArchiveUploadModal'; import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; +import { NestedArchivedRecordingsTable } from './NestedArchivedRecordingsTable'; export interface AllArchivedRecordingsTreeViewProps { } @@ -60,7 +61,7 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { - setLoading(true); + setIsLoading(true); addSubscription( - context.targets.queryForTargets().subscribe(() => setLoading(false)) + context.targets.queryForTargets().subscribe(() => setIsLoading(false)) ); - }, [setLoading, addSubscription, context.targets]); + }, [setIsLoading, addSubscription, context.targets]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { @@ -94,10 +95,6 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); }; - const RecordingRow = (props) => { - - } - const TargetRow = (props) => { const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { @@ -131,9 +128,26 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { + return ( + + + + + + + + ) + }, [props.target, props.index, isExpanded, tableColumns]); + return ( {parentRow} + {childRow} ); } diff --git a/src/app/Archives/NestedArchivedRecordingsTable.tsx b/src/app/Archives/NestedArchivedRecordingsTable.tsx new file mode 100644 index 000000000..bec1fb625 --- /dev/null +++ b/src/app/Archives/NestedArchivedRecordingsTable.tsx @@ -0,0 +1,334 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { ArchivedRecording } from '@app/Shared/Services/Api.service'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; +import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { RecordingActions } from '@app/Recordings/RecordingActions'; +import { RecordingsTable } from '@app/Recordings/RecordingsTable'; +import { ReportFrame } from '@app/Recordings/ReportFrame'; +import { Observable, forkJoin, merge, combineLatest } from 'rxjs'; +import { concatMap, filter, first, map } from 'rxjs/operators'; +import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; +import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; + +export interface NestedArchivedRecordingsTableProps { + target: Target; +} + +export const NestedArchivedRecordingsTable: React.FunctionComponent = (props) => { + const context = React.useContext(ServiceContext); + + const [recordings, setRecordings] = 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 [isLoading, setIsLoading] = React.useState(false); + const addSubscription = useSubscriptions(); + + const tableColumns: string[] = [ + 'Name', + 'Labels', + ]; + + const handleHeaderCheck = React.useCallback((event, checked) => { + setHeaderChecked(checked); + setCheckedIndices(checked ? Array.from(new Array(recordings.length), (x, i) => i) : []); + }, [setHeaderChecked, setCheckedIndices, recordings]); + + const handleRowCheck = React.useCallback((checked, index) => { + if (checked) { + setCheckedIndices(ci => ([...ci, index])); + } else { + setHeaderChecked(false); + setCheckedIndices(ci => ci.filter(v => v !== index)); + } + }, [setCheckedIndices, setHeaderChecked]); + + const handleRecordings = React.useCallback((recordings) => { + setRecordings(recordings); + setIsLoading(false); + }, [setRecordings, setIsLoading]); + + const refreshRecordingList = React.useCallback(() => { + setIsLoading(true); + addSubscription( + context.api.graphql(` + query { + targetNodes(filter: { name: "${props.target.connectUrl}" }) { + recordings { + archived { + name + downloadUrl + reportUrl + metadata { + labels + } + } + } + } + }`) + .pipe( + map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + ) + .subscribe(handleRecordings) + ); + }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); + + React.useEffect(() => { + addSubscription( + combineLatest([ + merge( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), + ), + ]) + .subscribe(parts => { + const event = parts[0]; + if (props.target.connectUrl != event.message.target) { + return; + } + setRecordings(old => old.concat(event.message.recording)); + }) + ); + }, [addSubscription, context, context.notificationChannel, setRecordings]); + + React.useEffect(() => { + addSubscription( + combineLatest([ + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), + ]) + .subscribe(parts => { + const event = parts[0]; + if (props.target.connectUrl != event.message.target) { + return; + } + setRecordings(old => old.filter(o => o.name != event.message.recording.name)); + }) + ); + }, [addSubscription, context, context.notificationChannel, setRecordings]); + + React.useEffect(() => { + addSubscription( + combineLatest([ + context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), + ]) + .subscribe(parts => { + const event = parts[0]; + if (props.target.connectUrl != event.message.target) { + return; + } + setRecordings(old => old.map( + o => o.name == event.message.recordingName + ? { ...o, metadata: { labels: event.message.metadata.labels } } + : o)); + }) + ); + }, [addSubscription, context, context.notificationChannel, setRecordings]); + + const handleDeleteRecordings = () => { + const tasks: Observable[] = []; + recordings.forEach((r: ArchivedRecording, idx) => { + if (checkedIndices.includes(idx)) { + handleRowCheck(false, idx); + context.reports.delete(r); + tasks.push( + context.api.deleteArchivedRecording(r.name).pipe(first()) + ); + } + }); + addSubscription( + forkJoin(tasks).subscribe() + ); + }; + + 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(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval(() => refreshRecordingList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + return () => window.clearInterval(id); + }, [context, context.settings, refreshRecordingList]); + + const RecordingRow = (props) => { + const parsedLabels = React.useMemo(() => { + return parseLabels(props.recording.metadata.labels); + }, [props.recording.metadata.labels]); + + const expandedRowId =`archived-table-row-${props.index}-exp`; + const handleToggle = () => { + toggleExpanded(expandedRowId); + }; + const [rowLabels, setRowLabels] = React.useState(parsedLabels); + const [editingMetadata, setEditingMetadata] = React.useState(false); + + const isExpanded = React.useMemo(() => { + return expandedRows.includes(expandedRowId); + }, [expandedRows, expandedRowId]); + + const handleCheck = (checked) => { + handleRowCheck(checked, props.index); + }; + + const handleSubmitLabelPatch = React.useCallback(() => { + context.api.postRecordingMetadata(props.recording.name, rowLabels).subscribe(() => {} /* do nothing */); + setEditingMetadata(false); + }, [props.recording.name, rowLabels, context, context.api, setEditingMetadata]); + + const handleCancelLabelPatch = React.useCallback(() => { + setRowLabels(parseLabels(props.recording.metadata.labels)); + setEditingMetadata(false); + }, [props.recording.metadata.labels, setRowLabels, setEditingMetadata]); + + const parentRow = React.useMemo(() => { + return( + + + + + + + {props.recording.name} + + + {editingMetadata ? + + : rowLabels.length ? rowLabels.map(l => ( + + )) + : - + } + + context.api.uploadArchivedRecordingToGrafana(props.recording.name)} + editMetadataFn={() => setEditingMetadata(true)} + /> + + ); + }, [props.recording, props.recording.metadata.labels, props.recording.name, props.index, handleCheck, checkedIndices, isExpanded, handleToggle, tableColumns, context.api]); + + const childRow = React.useMemo(() => { + return ( + + + + + + + + ) + }, [props.recording, props.recording.name, props.index, isExpanded, tableColumns]); + + return ( + + {parentRow} + {childRow} + + ); + }; + + const RecordingsToolbar = () => { + return ( + + + + + + + + + + ); + }; + + const recordingRows = React.useMemo(() => { + return recordings.map((r, idx) => ) + }, [recordings, expandedRows, checkedIndices]); + + return (<> + } + tableColumns={tableColumns} + isHeaderChecked={headerChecked} + onHeaderCheck={handleHeaderCheck} + isLoading={isLoading} + isEmpty={!recordings.length} + errorMessage='' + > + {recordingRows} + + ); +}; From 469dd95c17a9c339858854781a3610f834aea3b7 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 5 May 2022 19:02:24 -0400 Subject: [PATCH 07/69] Retrieving updated nested archived recordings table works. Try refactoring into one file for efficiency --- .../AllArchivedRecordingsTreeView.tsx | 42 +++++++++-- .../NestedArchivedRecordingsTable.tsx | 75 ++++++++++++------- .../Recordings/ArchivedRecordingsTable.tsx | 19 +++-- 3 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 8364cba93..c050e4c3e 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -44,11 +44,12 @@ import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.s import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { RecordingActions } from '@app/Recordings/RecordingActions'; import { RecordingsTable } from '@app/Recordings/RecordingsTable'; import { ReportFrame } from '@app/Recordings/ReportFrame'; -import { Observable, forkJoin, merge } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { Observable, forkJoin, merge, of } from 'rxjs'; +import { first, map } from 'rxjs/operators'; import { PlusIcon } from '@patternfly/react-icons'; import { ArchiveUploadModal } from './ArchiveUploadModal'; import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; @@ -96,10 +97,39 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { + const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); + + const handleRecordings = React.useCallback((recordings) => { + setRecordings(recordings); + setIsLoading(false); + toggleExpanded(expandedRowId) + }, [setRecordings, setIsLoading]); + const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { - toggleExpanded(expandedRowId); - }; + addSubscription( + context.api.graphql(` + query { + targetNodes(filter: { name: "${props.target.connectUrl}" }) { + recordings { + archived { + name + downloadUrl + reportUrl + metadata { + labels + } + } + } + } + }`) + .pipe( + map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + ) + .subscribe(recordings => ( + handleRecordings(recordings) + )) + )}; const isExpanded = React.useMemo(() => { return expandedRows.includes(expandedRowId); @@ -137,12 +167,12 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent - + ) - }, [props.target, props.index, isExpanded, tableColumns]); + }, [props.target, props.index, context.api, recordings,isExpanded, tableColumns]); return ( diff --git a/src/app/Archives/NestedArchivedRecordingsTable.tsx b/src/app/Archives/NestedArchivedRecordingsTable.tsx index bec1fb625..5a5e1033a 100644 --- a/src/app/Archives/NestedArchivedRecordingsTable.tsx +++ b/src/app/Archives/NestedArchivedRecordingsTable.tsx @@ -51,7 +51,7 @@ import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecor import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; export interface NestedArchivedRecordingsTableProps { - target: Target; + target: Observable; } export const NestedArchivedRecordingsTable: React.FunctionComponent = (props) => { @@ -91,39 +91,52 @@ export const NestedArchivedRecordingsTable: React.FunctionComponent { setIsLoading(true); addSubscription( - context.api.graphql(` - query { - targetNodes(filter: { name: "${props.target.connectUrl}" }) { - recordings { - archived { - name - downloadUrl - reportUrl - metadata { - labels + props.target + .pipe( + filter(target => target !== NO_TARGET), + first(), + concatMap(target => + context.api.graphql(` + query { + targetNodes(filter: { name: "${target.connectUrl}" }) { + recordings { + archived { + name + downloadUrl + reportUrl + metadata { + labels + } + } } } - } - } - }`) - .pipe( - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), - ) + }`) + ), + map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + ) .subscribe(handleRecordings) ); }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); + React.useEffect(() => { + addSubscription( + props.target.subscribe(refreshRecordingList) + ); + }, [addSubscription, context, context.target, refreshRecordingList]); + React.useEffect(() => { addSubscription( combineLatest([ + props.target, merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), ), ]) .subscribe(parts => { - const event = parts[0]; - if (props.target.connectUrl != event.message.target) { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { return; } setRecordings(old => old.concat(event.message.recording)); @@ -134,14 +147,16 @@ export const NestedArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ + props.target, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]) - .subscribe(parts => { - const event = parts[0]; - if (props.target.connectUrl != event.message.target) { - return; - } - setRecordings(old => old.filter(o => o.name != event.message.recording.name)); + .subscribe(parts => { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { + return; + } + setRecordings(old => old.filter(o => o.name != event.message.recording.name)); }) ); }, [addSubscription, context, context.notificationChannel, setRecordings]); @@ -149,11 +164,13 @@ export const NestedArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), - ]) + props.target, + context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), + ]) .subscribe(parts => { - const event = parts[0]; - if (props.target.connectUrl != event.message.target) { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { return; } setRecordings(old => old.map( diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 27b81b0b4..0c0973608 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -56,11 +56,14 @@ import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { RecordingFiltersCategories } from './ActiveRecordingsTable'; import { filterRecordings, RecordingFilters } from './RecordingFilters'; -export interface ArchivedRecordingsTableProps { } +export interface ArchivedRecordingsTableProps { + target?: Observable; +} -export const ArchivedRecordingsTable: React.FunctionComponent = () => { +export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); + const target = (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target; const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); const [filteredRecordings, setFilteredRecordings] = React.useState([] as ArchivedRecording[]); const [headerChecked, setHeaderChecked] = React.useState(false); @@ -106,7 +109,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setIsLoading(true); addSubscription( - context.target.target() + target .pipe( filter(target => target !== NO_TARGET), first(), @@ -131,7 +134,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -142,14 +145,14 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( - context.target.target().subscribe(refreshRecordingList) + target.subscribe(refreshRecordingList) ); - }, [addSubscription, context, context.target, refreshRecordingList]); + }, [addSubscription, target, refreshRecordingList]); React.useEffect(() => { addSubscription( combineLatest([ - context.target.target(), + target, merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), @@ -169,7 +172,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - context.target.target(), + target, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]) .subscribe(parts => { From f7538b10f938fe3e84a25fbe26e0ab0b78fc7fe5 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 6 May 2022 12:37:42 -0400 Subject: [PATCH 08/69] Revert changes to the archived recordings table --- .../Recordings/ArchivedRecordingsTable.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 0c0973608..27b81b0b4 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -56,14 +56,11 @@ import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { RecordingFiltersCategories } from './ActiveRecordingsTable'; import { filterRecordings, RecordingFilters } from './RecordingFilters'; -export interface ArchivedRecordingsTableProps { - target?: Observable; -} +export interface ArchivedRecordingsTableProps { } -export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { +export const ArchivedRecordingsTable: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); - const target = (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target; const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); const [filteredRecordings, setFilteredRecordings] = React.useState([] as ArchivedRecording[]); const [headerChecked, setHeaderChecked] = React.useState(false); @@ -109,7 +106,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setIsLoading(true); addSubscription( - target + context.target.target() .pipe( filter(target => target !== NO_TARGET), first(), @@ -134,7 +131,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -145,14 +142,14 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( - target.subscribe(refreshRecordingList) + context.target.target().subscribe(refreshRecordingList) ); - }, [addSubscription, target, refreshRecordingList]); + }, [addSubscription, context, context.target, refreshRecordingList]); React.useEffect(() => { addSubscription( combineLatest([ - target, + context.target.target(), merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), @@ -172,7 +169,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - target, + context.target.target(), context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]) .subscribe(parts => { From 9504516f24e467930fff38b6d77dbbfe05bf6685 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 6 May 2022 12:43:47 -0400 Subject: [PATCH 09/69] Remove handling retrieving recordings from the AllArchivedRecordings view --- .../AllArchivedRecordingsTreeView.tsx | 37 ++----------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index c050e4c3e..378a30398 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -97,39 +97,10 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent { - const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); - - const handleRecordings = React.useCallback((recordings) => { - setRecordings(recordings); - setIsLoading(false); - toggleExpanded(expandedRowId) - }, [setRecordings, setIsLoading]); - const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { - addSubscription( - context.api.graphql(` - query { - targetNodes(filter: { name: "${props.target.connectUrl}" }) { - recordings { - archived { - name - downloadUrl - reportUrl - metadata { - labels - } - } - } - } - }`) - .pipe( - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), - ) - .subscribe(recordings => ( - handleRecordings(recordings) - )) - )}; + toggleExpanded(expandedRowId); + }; const isExpanded = React.useMemo(() => { return expandedRows.includes(expandedRowId); @@ -167,12 +138,12 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent - + ) - }, [props.target, props.index, context.api, recordings,isExpanded, tableColumns]); + }, [props.target, props.index, context.api, isExpanded, tableColumns]); return ( From b9460a69537c47df37a30acd6dab73a5780ae5b4 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 6 May 2022 14:45:48 -0400 Subject: [PATCH 10/69] Consolidate nested and regular archived recordings tables into one component --- .../AllArchivedRecordingsTreeView.tsx | 5 +- .../NestedArchivedRecordingsTable.tsx | 351 ------------------ .../Recordings/ArchivedRecordingsTable.tsx | 79 ++-- 3 files changed, 51 insertions(+), 384 deletions(-) delete mode 100644 src/app/Archives/NestedArchivedRecordingsTable.tsx diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllArchivedRecordingsTreeView.tsx index 378a30398..a58ac4f1f 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllArchivedRecordingsTreeView.tsx @@ -53,7 +53,6 @@ import { first, map } from 'rxjs/operators'; import { PlusIcon } from '@patternfly/react-icons'; import { ArchiveUploadModal } from './ArchiveUploadModal'; import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; -import { NestedArchivedRecordingsTable } from './NestedArchivedRecordingsTable'; export interface AllArchivedRecordingsTreeViewProps { } @@ -135,10 +134,10 @@ export const AllArchivedRecordingsTreeView: React.FunctionComponent - + diff --git a/src/app/Archives/NestedArchivedRecordingsTable.tsx b/src/app/Archives/NestedArchivedRecordingsTable.tsx deleted file mode 100644 index 5a5e1033a..000000000 --- a/src/app/Archives/NestedArchivedRecordingsTable.tsx +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import { ArchivedRecording } from '@app/Shared/Services/Api.service'; -import { ServiceContext } from '@app/Shared/Services/Services'; -import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; -import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; -import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; -import { RecordingActions } from '@app/Recordings/RecordingActions'; -import { RecordingsTable } from '@app/Recordings/RecordingsTable'; -import { ReportFrame } from '@app/Recordings/ReportFrame'; -import { Observable, forkJoin, merge, combineLatest } from 'rxjs'; -import { concatMap, filter, first, map } from 'rxjs/operators'; -import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; -import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; - -export interface NestedArchivedRecordingsTableProps { - target: Observable; -} - -export const NestedArchivedRecordingsTable: React.FunctionComponent = (props) => { - const context = React.useContext(ServiceContext); - - const [recordings, setRecordings] = 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 [isLoading, setIsLoading] = React.useState(false); - const addSubscription = useSubscriptions(); - - const tableColumns: string[] = [ - 'Name', - 'Labels', - ]; - - const handleHeaderCheck = React.useCallback((event, checked) => { - setHeaderChecked(checked); - setCheckedIndices(checked ? Array.from(new Array(recordings.length), (x, i) => i) : []); - }, [setHeaderChecked, setCheckedIndices, recordings]); - - const handleRowCheck = React.useCallback((checked, index) => { - if (checked) { - setCheckedIndices(ci => ([...ci, index])); - } else { - setHeaderChecked(false); - setCheckedIndices(ci => ci.filter(v => v !== index)); - } - }, [setCheckedIndices, setHeaderChecked]); - - const handleRecordings = React.useCallback((recordings) => { - setRecordings(recordings); - setIsLoading(false); - }, [setRecordings, setIsLoading]); - - const refreshRecordingList = React.useCallback(() => { - setIsLoading(true); - addSubscription( - props.target - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => - context.api.graphql(` - query { - targetNodes(filter: { name: "${target.connectUrl}" }) { - recordings { - archived { - name - downloadUrl - reportUrl - metadata { - labels - } - } - } - } - }`) - ), - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), - ) - .subscribe(handleRecordings) - ); - }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); - - React.useEffect(() => { - addSubscription( - props.target.subscribe(refreshRecordingList) - ); - }, [addSubscription, context, context.target, refreshRecordingList]); - - React.useEffect(() => { - addSubscription( - combineLatest([ - props.target, - merge( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), - ), - ]) - .subscribe(parts => { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } - setRecordings(old => old.concat(event.message.recording)); - }) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - React.useEffect(() => { - addSubscription( - combineLatest([ - props.target, - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), - ]) - .subscribe(parts => { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } - setRecordings(old => old.filter(o => o.name != event.message.recording.name)); - }) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - React.useEffect(() => { - addSubscription( - combineLatest([ - props.target, - context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), - ]) - .subscribe(parts => { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } - setRecordings(old => old.map( - o => o.name == event.message.recordingName - ? { ...o, metadata: { labels: event.message.metadata.labels } } - : o)); - }) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - const handleDeleteRecordings = () => { - const tasks: Observable[] = []; - recordings.forEach((r: ArchivedRecording, idx) => { - if (checkedIndices.includes(idx)) { - handleRowCheck(false, idx); - context.reports.delete(r); - tasks.push( - context.api.deleteArchivedRecording(r.name).pipe(first()) - ); - } - }); - addSubscription( - forkJoin(tasks).subscribe() - ); - }; - - 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(() => { - if (!context.settings.autoRefreshEnabled()) { - return; - } - const id = window.setInterval(() => refreshRecordingList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); - return () => window.clearInterval(id); - }, [context, context.settings, refreshRecordingList]); - - const RecordingRow = (props) => { - const parsedLabels = React.useMemo(() => { - return parseLabels(props.recording.metadata.labels); - }, [props.recording.metadata.labels]); - - const expandedRowId =`archived-table-row-${props.index}-exp`; - const handleToggle = () => { - toggleExpanded(expandedRowId); - }; - const [rowLabels, setRowLabels] = React.useState(parsedLabels); - const [editingMetadata, setEditingMetadata] = React.useState(false); - - const isExpanded = React.useMemo(() => { - return expandedRows.includes(expandedRowId); - }, [expandedRows, expandedRowId]); - - const handleCheck = (checked) => { - handleRowCheck(checked, props.index); - }; - - const handleSubmitLabelPatch = React.useCallback(() => { - context.api.postRecordingMetadata(props.recording.name, rowLabels).subscribe(() => {} /* do nothing */); - setEditingMetadata(false); - }, [props.recording.name, rowLabels, context, context.api, setEditingMetadata]); - - const handleCancelLabelPatch = React.useCallback(() => { - setRowLabels(parseLabels(props.recording.metadata.labels)); - setEditingMetadata(false); - }, [props.recording.metadata.labels, setRowLabels, setEditingMetadata]); - - const parentRow = React.useMemo(() => { - return( - - - - - - - {props.recording.name} - - - {editingMetadata ? - - : rowLabels.length ? rowLabels.map(l => ( - - )) - : - - } - - context.api.uploadArchivedRecordingToGrafana(props.recording.name)} - editMetadataFn={() => setEditingMetadata(true)} - /> - - ); - }, [props.recording, props.recording.metadata.labels, props.recording.name, props.index, handleCheck, checkedIndices, isExpanded, handleToggle, tableColumns, context.api]); - - const childRow = React.useMemo(() => { - return ( - - - - - - - - ) - }, [props.recording, props.recording.name, props.index, isExpanded, tableColumns]); - - return ( - - {parentRow} - {childRow} - - ); - }; - - const RecordingsToolbar = () => { - return ( - - - - - - - - - - ); - }; - - const recordingRows = React.useMemo(() => { - return recordings.map((r, idx) => ) - }, [recordings, expandedRows, checkedIndices]); - - return (<> - } - tableColumns={tableColumns} - isHeaderChecked={headerChecked} - onHeaderCheck={handleHeaderCheck} - isLoading={isLoading} - isEmpty={!recordings.length} - errorMessage='' - > - {recordingRows} - - ); -}; diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 27b81b0b4..d47c136bf 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -56,9 +56,11 @@ import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { RecordingFiltersCategories } from './ActiveRecordingsTable'; import { filterRecordings, RecordingFilters } from './RecordingFilters'; -export interface ArchivedRecordingsTableProps { } +export interface ArchivedRecordingsTableProps { + target?: Observable; +} -export const ArchivedRecordingsTable: React.FunctionComponent = () => { +export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); @@ -103,34 +105,51 @@ export const ArchivedRecordingsTable: React.FunctionComponent { + return context.api.graphql(` + query { + targetNodes(filter: { name: "${connectUrl}" }) { + recordings { + archived { + name + downloadUrl + reportUrl + metadata { + labels + } + } + } + } + }`) + } + const refreshRecordingList = React.useCallback(() => { setIsLoading(true); - addSubscription( - context.target.target() - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => - context.api.graphql(` - query { - targetNodes(filter: { name: "${target.connectUrl}" }) { - recordings { - archived { - name - downloadUrl - reportUrl - metadata { - labels - } - } - } - } - }`) - ), - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), - ) - .subscribe(handleRecordings) - ); + if (typeof props.target === 'undefined' || props.target === null) { + addSubscription( + context.target.target() + .pipe( + filter(target => target !== NO_TARGET), + first(), + concatMap(target => + queryTargetRecordings(target.connectUrl) + ), + map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + ) + .subscribe(handleRecordings) + ); + } else { + addSubscription( + props.target + .pipe( + concatMap(target => + queryTargetRecordings(target.connectUrl) + ), + map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + ) + .subscribe(handleRecordings) + ); + } }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); const handleClearFilters = React.useCallback(() => { @@ -149,7 +168,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - context.target.target(), + (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target, merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), @@ -169,7 +188,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - context.target.target(), + (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]) .subscribe(parts => { From 89bf3a4a0fb5e1d27be04ecba259e07e6cdc4c2c Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 10 May 2022 18:28:43 -0400 Subject: [PATCH 11/69] ArchivedRecordingsTable uses the archivedRecordings GraphQL query to populate the table --- .../Recordings/ArchivedRecordingsTable.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index d47c136bf..440fbe7a1 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -108,16 +108,12 @@ export const ArchivedRecordingsTable: React.FunctionComponent { return context.api.graphql(` query { - targetNodes(filter: { name: "${connectUrl}" }) { - recordings { - archived { - name - downloadUrl - reportUrl - metadata { - labels - } - } + archivedRecordings(filter: { sourceTarget: "${connectUrl}" }) { + name + downloadUrl + reportUrl + metadata { + labels } } }`) @@ -134,7 +130,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent queryTargetRecordings(target.connectUrl) ), - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + map(v => v.data.archivedRecordings as ArchivedRecording[]), ) .subscribe(handleRecordings) ); @@ -145,7 +141,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent queryTargetRecordings(target.connectUrl) ), - map(v => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + map(v => v.data.archivedRecordings as ArchivedRecording[]), ) .subscribe(handleRecordings) ); From 01af98748bd2ccb2cbb7e4f619e9c63c2eabe596 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 10 May 2022 19:05:53 -0400 Subject: [PATCH 12/69] Add separate table for uploaded recordings. Refactor component names. --- ...x => AllTargetsArchivedRecordingsTable.tsx} | 4 ++-- src/app/Archives/Archives.tsx | 12 +++++++++--- ...tsx => UploadedArchivedRecordingsTable.tsx} | 18 ++++++++++++++---- 3 files changed, 25 insertions(+), 9 deletions(-) rename src/app/Archives/{AllArchivedRecordingsTreeView.tsx => AllTargetsArchivedRecordingsTable.tsx} (97%) rename src/app/Archives/{AllArchivedRecordingsTable.tsx => UploadedArchivedRecordingsTable.tsx} (95%) diff --git a/src/app/Archives/AllArchivedRecordingsTreeView.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx similarity index 97% rename from src/app/Archives/AllArchivedRecordingsTreeView.tsx rename to src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index a58ac4f1f..4c7e79e74 100644 --- a/src/app/Archives/AllArchivedRecordingsTreeView.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -54,9 +54,9 @@ import { PlusIcon } from '@patternfly/react-icons'; import { ArchiveUploadModal } from './ArchiveUploadModal'; import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; -export interface AllArchivedRecordingsTreeViewProps { } +export interface AllTargetsArchivedRecordingsTableProps { } -export const AllArchivedRecordingsTreeView: React.FunctionComponent = () => { +export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); const [targets, setTargets] = React.useState([] as Target[]); diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 7b6054cf0..e4023f7d3 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -39,8 +39,8 @@ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; import { Card, CardBody, CardHeader, EmptyState, EmptyStateIcon, Text, TextVariants, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; -import { AllArchivedRecordingsTable } from './AllArchivedRecordingsTable'; -import { AllArchivedRecordingsTreeView } from './AllArchivedRecordingsTreeView'; +import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; +import { UploadedArchivedRecordingsTable } from './UploadedArchivedRecordingsTable'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; export const Archives = () => { @@ -65,7 +65,7 @@ export const Archives = () => { } return (<> Archived Recordings (All Targets) - + ); }, [archiveEnabled]); @@ -76,6 +76,12 @@ export const Archives = () => { { cardBody } + + + Archived Recordings (Uploads) + + + ); }; diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/UploadedArchivedRecordingsTable.tsx similarity index 95% rename from src/app/Archives/AllArchivedRecordingsTable.tsx rename to src/app/Archives/UploadedArchivedRecordingsTable.tsx index 42eed0fc8..f5d8787fb 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/UploadedArchivedRecordingsTable.tsx @@ -54,9 +54,9 @@ import { LabelCell } from '@app/RecordingMetadata/LabelCell'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; -export interface AllArchivedRecordingsTableProps { } +export interface UploadedArchivedRecordingsTableProps { } -export const AllArchivedRecordingsTable: React.FunctionComponent = () => { +export const UploadedArchivedRecordingsTable: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); @@ -95,8 +95,18 @@ export const AllArchivedRecordingsTable: React.FunctionComponent { setIsLoading(true); addSubscription( - context.api.doGet('recordings', 'v1') - .subscribe(handleRecordings) + context.api.graphql(` + query { + archivedRecordings(filter: { sourceTarget: "uploads" }) { + name + downloadUrl + reportUrl + metadata { + labels + } + } + }`) + .subscribe(v => handleRecordings(v.data.archivedRecordings as ArchivedRecording[])) ); }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); From 22e9230eef7c2f0ad679d07b134908e9aca39936 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 11 May 2022 16:08:40 -0400 Subject: [PATCH 13/69] Refactoring --- .../Archives/AllTargetsArchivedRecordingsTable.tsx | 6 ++---- src/app/Archives/Archives.tsx | 14 ++++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 4c7e79e74..ae9602c55 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -61,7 +61,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - setIsLoading(true); addSubscription( - context.targets.queryForTargets().subscribe(() => setIsLoading(false)) + context.targets.queryForTargets().subscribe(() => {} /* do nothing */) ); - }, [setIsLoading, addSubscription, context.targets]); + }, [addSubscription, context.targets]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index e4023f7d3..8be43de49 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -52,7 +52,7 @@ export const Archives = () => { return () => sub.unsubscribe(); }, [context.api]); - const cardBody = React.useMemo(() => { + const allTargetsCardBody = React.useMemo(() => { if (!archiveEnabled) { return (<> @@ -69,17 +69,23 @@ export const Archives = () => { ); }, [archiveEnabled]); + const uploadedCardBody = React.useMemo(() => { + return (<> + Archived Recordings (Uploads) + + ); + },[]); + return ( - { cardBody } + { allTargetsCardBody } - Archived Recordings (Uploads) - + { uploadedCardBody } From 0e7770d5ff858ff7965cd136ee7deeabbe8b7a8b Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 12 May 2022 18:33:52 -0400 Subject: [PATCH 14/69] Update target list upon recieving TargetJvmDiscovery notification --- .../AllTargetsArchivedRecordingsTable.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index ae9602c55..53eefc891 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -53,6 +53,7 @@ import { first, map } from 'rxjs/operators'; import { PlusIcon } from '@patternfly/react-icons'; import { ArchiveUploadModal } from './ArchiveUploadModal'; import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; +import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -88,6 +89,21 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent window.clearInterval(id); }, [context.target, context.settings, refreshTargetList]); + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery) + .subscribe(v => { + const evt: TargetDiscoveryEvent = v.message.event; + const target: Target = evt.serviceRef; + if (evt.kind === 'FOUND') { + setTargets(old => old.concat(target)); + } else if (evt.kind === 'LOST') { + setTargets(old => old.filter(o => o.alias != target.alias && o.connectUrl != target.connectUrl)) + } + }) + ); + }, [addSubscription, context, context.notificationChannel, setTargets]); + const toggleExpanded = (id) => { const idx = expandedRows.indexOf(id); setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); From 52bc59c5170cbf3628e92999313e7fc0eee6ca06 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 2 Jun 2022 16:53:43 -0400 Subject: [PATCH 15/69] Ensure nested archived recordings table data is only queried for upon expansion --- .../Archives/AllTargetsArchivedRecordingsTable.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 53eefc891..89e5ee275 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -141,7 +141,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent ); }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); - + const childRow = React.useMemo(() => { return ( @@ -150,9 +150,12 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - - - + {isExpanded ? + + + + : + null} ) From fac547af5a3662f34ab7e4814daf0c3458ee3677 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 2 Jun 2022 18:32:48 -0400 Subject: [PATCH 16/69] Add targets search feature --- .../AllTargetsArchivedRecordingsTable.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 89e5ee275..6a4ca7a01 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -42,8 +42,8 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; -import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput } from '@patternfly/react-core'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, ISortBy } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { RecordingActions } from '@app/Recordings/RecordingActions'; import { RecordingsTable } from '@app/Recordings/RecordingsTable'; @@ -60,7 +60,9 @@ export interface AllTargetsArchivedRecordingsTableProps { } export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); + const [search, setSearch] = React.useState(''); const [targets, setTargets] = React.useState([] as Target[]); + const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); const [expandedRows, setExpandedRows] = React.useState([] as string[]); const addSubscription = useSubscriptions(); @@ -89,6 +91,17 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent window.clearInterval(id); }, [context.target, context.settings, refreshTargetList]); + React.useEffect(() => { + let searched; + if (!search) { + searched = targets; + } else { + const searchText = search.trim().toLowerCase(); + searched = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) + } + setSearchedTargets([...searched]); + }, [search, targets]); + React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery) @@ -170,10 +183,16 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return targets.map((t, idx) => ) - }, [targets, expandedRows]); + return searchedTargets.map((t, idx) => ) + }, [searchedTargets, expandedRows]); return (<> + setSearch('')} + /> From 10e59c65bab228d461864ef66d6a3fe67aa3a69c Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 2 Jun 2022 18:53:14 -0400 Subject: [PATCH 17/69] fixup! Add targets search feature --- .../AllTargetsArchivedRecordingsTable.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 6a4ca7a01..7313c9081 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -187,12 +187,20 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - setSearch('')} - /> + + + + + setSearch('')} + /> + + + + From 3dbca06181aacadf452069f8aab08951640dea7b Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 2 Jun 2022 19:24:38 -0400 Subject: [PATCH 18/69] Place 'All Targets' and 'Uploads' tables into separate Tabs --- src/app/Archives/Archives.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 8be43de49..328743274 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -37,7 +37,7 @@ */ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; -import { Card, CardBody, CardHeader, EmptyState, EmptyStateIcon, Text, TextVariants, Title } from '@patternfly/react-core'; +import { Card, CardBody, CardHeader, EmptyState, EmptyStateIcon, Tab, Tabs, Text, TextVariants, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; import { UploadedArchivedRecordingsTable } from './UploadedArchivedRecordingsTable'; @@ -45,6 +45,7 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; export const Archives = () => { const context = React.useContext(ServiceContext); + const [activeTab, setActiveTab] = React.useState(0); const [archiveEnabled, setArchiveEnabled] = React.useState(false); React.useEffect(() => { @@ -52,7 +53,7 @@ export const Archives = () => { return () => sub.unsubscribe(); }, [context.api]); - const allTargetsCardBody = React.useMemo(() => { + const allTargets = React.useMemo(() => { if (!archiveEnabled) { return (<> @@ -64,14 +65,12 @@ export const Archives = () => { ); } return (<> - Archived Recordings (All Targets) ); }, [archiveEnabled]); - const uploadedCardBody = React.useMemo(() => { + const uploads = React.useMemo(() => { return (<> - Archived Recordings (Uploads) ); },[]); @@ -80,12 +79,14 @@ export const Archives = () => { - { allTargetsCardBody } - - - - - { uploadedCardBody } + setActiveTab(Number(idx))}> + + { allTargets } + + + { uploads } + + From 9cf6a3111ddf16c50f90b04e344d190bbc9af812 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 3 Jun 2022 13:24:56 -0400 Subject: [PATCH 19/69] Only load the uploaded archived recordings if the tab for it is selected --- src/app/Archives/Archives.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 328743274..6470e74a3 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -84,7 +84,10 @@ export const Archives = () => { { allTargets } - { uploads } + {activeTab == 1 ? + uploads + : + null} From 8de4ca63f6d6a0176482069d5216005ab83bada1 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 3 Jun 2022 17:30:52 -0400 Subject: [PATCH 20/69] Fix imports --- .../Archives/AllTargetsArchivedRecordingsTable.tsx | 13 ++----------- src/app/Recordings/ArchivedRecordingsTable.tsx | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 7313c9081..1b0d9e124 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -36,23 +36,14 @@ * SOFTWARE. */ import * as React from 'react'; -import { TreeView , TreeViewDataItem } from '@patternfly/react-core'; -import { ArchivedRecording } from '@app/Shared/Services/Api.service'; import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Checkbox, Label, Text, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput } from '@patternfly/react-core'; +import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput } from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, ISortBy } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; -import { RecordingActions } from '@app/Recordings/RecordingActions'; -import { RecordingsTable } from '@app/Recordings/RecordingsTable'; -import { ReportFrame } from '@app/Recordings/ReportFrame'; -import { Observable, forkJoin, merge, of } from 'rxjs'; -import { first, map } from 'rxjs/operators'; -import { PlusIcon } from '@patternfly/react-icons'; -import { ArchiveUploadModal } from './ArchiveUploadModal'; -import { EditRecordingLabels, parseLabels } from '@app/CreateRecording/EditRecordingLabels'; +import { of } from 'rxjs'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; export interface AllTargetsArchivedRecordingsTableProps { } diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 440fbe7a1..871f52cfb 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -47,7 +47,7 @@ import { RecordingsTable } from './RecordingsTable'; import { ReportFrame } from './ReportFrame'; import { Observable, forkJoin, merge, combineLatest } from 'rxjs'; import { concatMap, filter, first, map } from 'rxjs/operators'; -import { NO_TARGET } from '@app/Shared/Services/Target.service'; +import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; import { parseLabels } from '@app/RecordingMetadata/RecordingLabel'; import { LabelCell } from '../RecordingMetadata/LabelCell'; import { RecordingLabelsPanel } from './RecordingLabelsPanel'; From 95866fa2b85da410cc1651d13c5cccfc3f9cf77a Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 3 Jun 2022 19:19:19 -0400 Subject: [PATCH 21/69] Start combining the UploadedArchivedRecordingsTable with the ArchivedRecordingsTable --- .../AllTargetsArchivedRecordingsTable.tsx | 2 +- src/app/Archives/Archives.tsx | 3 +- .../Recordings/ArchivedRecordingsTable.tsx | 72 ++++++++++++++----- src/app/Recordings/Recordings.tsx | 2 +- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 1b0d9e124..6a37fea9a 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -156,7 +156,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent {isExpanded ? - + : null} diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 6470e74a3..cd2d38f0a 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -42,6 +42,7 @@ import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; import { UploadedArchivedRecordingsTable } from './UploadedArchivedRecordingsTable'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; +import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; export const Archives = () => { const context = React.useContext(ServiceContext); @@ -71,7 +72,7 @@ export const Archives = () => { const uploads = React.useMemo(() => { return (<> - + ); },[]); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 871f52cfb..6dc5e335a 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -42,6 +42,7 @@ import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.s import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Button, Checkbox, Drawer, DrawerContent, DrawerContentBody, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { PlusIcon } from '@patternfly/react-icons'; import { RecordingActions } from './RecordingActions'; import { RecordingsTable } from './RecordingsTable'; import { ReportFrame } from './ReportFrame'; @@ -55,19 +56,23 @@ import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { RecordingFiltersCategories } from './ActiveRecordingsTable'; import { filterRecordings, RecordingFilters } from './RecordingFilters'; +import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { target?: Observable; + isUploadsTable?: boolean; } export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); + const [target, setTarget] = React.useState(((props.target === null || props.target === undefined) ? context.target.target() : props.target) as Observable) 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 [showUploadModal, setShowUploadModal] = React.useState(false); const [showDetailsPanel, setShowDetailsPanel] = React.useState(false); const [warningModalOpen, setWarningModalOpen] = React.useState(false); const [filters, setFilters] = React.useState({ @@ -119,25 +124,32 @@ export const ArchivedRecordingsTable: React.FunctionComponent { + return context.api.graphql(` + query { + archivedRecordings(filter: { sourceTarget: "uploads" }) { + name + downloadUrl + reportUrl + metadata { + labels + } + } + }`) + } + const refreshRecordingList = React.useCallback(() => { setIsLoading(true); - if (typeof props.target === 'undefined' || props.target === null) { + if (props.isUploadsTable) { addSubscription( - context.target.target() - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => - queryTargetRecordings(target.connectUrl) - ), - map(v => v.data.archivedRecordings as ArchivedRecording[]), - ) - .subscribe(handleRecordings) + queryUploadedRecordings().subscribe(v => handleRecordings(v.data.archivedRecordings as ArchivedRecording[])) ); } else { addSubscription( - props.target + target .pipe( + filter(target => target !== NO_TARGET), + first(), concatMap(target => queryTargetRecordings(target.connectUrl) ), @@ -155,16 +167,18 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - addSubscription( - context.target.target().subscribe(refreshRecordingList) - ); - }, [addSubscription, context, context.target, refreshRecordingList]); + if (!props.isUploadsTable) { + React.useEffect(() => { + addSubscription( + target.subscribe(refreshRecordingList) + ); + }, [addSubscription, context, context.target, refreshRecordingList]); + } React.useEffect(() => { addSubscription( combineLatest([ - (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target, + target, merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), @@ -184,7 +198,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( combineLatest([ - (typeof props.target === 'undefined' || props.target === null) ? context.target.target() : props.target, + target, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]) .subscribe(parts => { @@ -372,6 +386,15 @@ export const ArchivedRecordingsTable: React.FunctionComponent { deleteArchivedWarningModal } + {props.isUploadsTable ? + + + + + + : + null + } ); @@ -381,6 +404,11 @@ export const ArchivedRecordingsTable: React.FunctionComponent ) }, [filteredRecordings, expandedRows, checkedIndices]); + const handleModalClose = React.useCallback(() => { + setShowUploadModal(false); + refreshRecordingList(); + }, [setShowUploadModal, refreshRecordingList]); + const LabelsPanel = React.useMemo(() => ( {recordingRows} + + {props.isUploadsTable ? + + : + null + } diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index fbeb9f6eb..8f28c36e5 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -59,7 +59,7 @@ export const Recordings = () => { - + ) : ( From 714232a3f92b07cf190448a0a8721dd71c0992b1 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 3 Jun 2022 19:50:32 -0400 Subject: [PATCH 22/69] Continue refactoring the ArchivedRecordingsTable --- .../Recordings/ArchivedRecordingsTable.tsx | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 6dc5e335a..0e0593788 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -158,7 +158,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -167,46 +167,48 @@ export const ArchivedRecordingsTable: React.FunctionComponent { + React.useEffect(() => { + if (!props.isUploadsTable) { addSubscription( target.subscribe(refreshRecordingList) ); - }, [addSubscription, context, context.target, refreshRecordingList]); - } + } + }, [addSubscription, target, props.isUploadsTable, refreshRecordingList]); React.useEffect(() => { - addSubscription( - combineLatest([ - target, + if (props.isUploadsTable) { + addSubscription( merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), - ), - ]) - .subscribe(parts => { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } - setRecordings(old => old.concat(event.message.recording)); - }) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - React.useEffect(() => { - addSubscription( - combineLatest([ - target, - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), - ]) + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) + ).subscribe(v => setRecordings(old => old.concat(v.message.recording))) + ); + } else { + addSubscription( + combineLatest([ + target, + merge( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), + ), + ]) .subscribe(parts => { const currentTarget = parts[0]; const event = parts[1]; if (currentTarget.connectUrl != event.message.target) { return; } + setRecordings(old => old.concat(event.message.recording)); + }) + ); + } + }, [addSubscription, context, context.notificationChannel, target, props.isUploadsTable, setRecordings]); + + React.useEffect(() => { + if (props.isUploadsTable) { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) + .subscribe(event => { let deleted; setRecordings((old) => { @@ -222,9 +224,39 @@ export const ArchivedRecordingsTable: React.FunctionComponent v !== deleted) .map(ci => ci > deleted ? ci - 1 : ci) ); - }) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings, setCheckedIndices]); + }) + ); + } else { + addSubscription( + combineLatest([ + target, + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), + ]) + .subscribe(parts => { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { + return; + } + let deleted; + + setRecordings((old) => { + return old.filter((r, i) => { + if (r.name == event.message.recording.name) { + deleted = i; + return false; + } + return true; + }); + }); + setCheckedIndices(old => old + .filter((v) => v !== deleted) + .map(ci => ci > deleted ? ci - 1 : ci) + ); + }) + ); + } + }, [addSubscription, context, context.notificationChannel, target, props.isUploadsTable, setRecordings, setCheckedIndices]); React.useEffect(() => { addSubscription( From e0bbd12280d529aa8b0ac08b87d783b0ef43de7f Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Mon, 6 Jun 2022 16:29:58 -0400 Subject: [PATCH 23/69] Ensure the Uploads table properly refreshes its recordings list --- src/app/Recordings/ArchivedRecordingsTable.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 0e0593788..5ec078987 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -158,7 +158,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -172,8 +172,12 @@ export const ArchivedRecordingsTable: React.FunctionComponent { if (props.isUploadsTable) { From f13aa2505ee64475d072f188c7046765ab27bd76 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 7 Jun 2022 13:24:53 -0400 Subject: [PATCH 24/69] Remove unnecessary imports --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 2 +- src/app/Archives/Archives.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 6a37fea9a..a7c75640c 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -41,7 +41,7 @@ import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput } from '@patternfly/react-core'; -import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, ISortBy } from '@patternfly/react-table'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { of } from 'rxjs'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index cd2d38f0a..a80b6d45c 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -37,10 +37,9 @@ */ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; -import { Card, CardBody, CardHeader, EmptyState, EmptyStateIcon, Tab, Tabs, Text, TextVariants, Title } from '@patternfly/react-core'; +import { Card, CardBody, EmptyState, EmptyStateIcon, Tab, Tabs, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; -import { UploadedArchivedRecordingsTable } from './UploadedArchivedRecordingsTable'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; From 3aa69119b7f453bc4509cdee9c86087f3c26faf1 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 7 Jun 2022 16:41:47 -0400 Subject: [PATCH 25/69] Start updating tests --- .../Recordings/ArchivedRecordingsTable.tsx | 2 +- .../ArchivedRecordingsTable.test.tsx | 32 ++++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 5ec078987..cbcd3280c 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -60,7 +60,7 @@ import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { target?: Observable; - isUploadsTable?: boolean; + isUploadsTable: boolean; } export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 6ebf6373f..a95946c4c 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -55,17 +55,13 @@ const mockRecording: ArchivedRecording = { reportUrl: 'http://reportUrl', metadata: { labels: mockRecordingLabels }, }; + const mockArchivedRecordingsResponse = { data: { - targetNodes: [ - { - recordings: { - archived: [mockRecording] as ArchivedRecording[], - }, - }, - ], + archivedRecordings: [mockRecording] as ArchivedRecording[], }, -}; +} + const mockAnotherRecording = { ...mockRecording, name: 'anotherRecording' }; const mockCreateNotification = { message: { target: mockConnectUrl, recording: mockAnotherRecording }, @@ -134,7 +130,7 @@ describe('', () => { await act(async () => { tree = renderer.create( - + ); }); @@ -144,7 +140,7 @@ describe('', () => { it('adds a recording after receiving a notification', () => { render( - + ); expect(screen.getByText('someRecording')).toBeInTheDocument(); @@ -154,7 +150,7 @@ describe('', () => { it('updates the recording labels after receiving a notification', () => { render( - + ); expect(screen.getByText('someLabel: someUpdatedValue')).toBeInTheDocument(); @@ -164,7 +160,7 @@ describe('', () => { it('removes a recording after receiving a notification', () => { render( - + ); expect(screen.queryByText('someRecording')).not.toBeInTheDocument(); @@ -173,7 +169,7 @@ describe('', () => { it('displays the toolbar buttons', () => { render( - + ); @@ -184,7 +180,7 @@ describe('', () => { it('opens the labels drawer when Edit Labels is clicked', () => { render( - + ); @@ -223,7 +219,7 @@ describe('', () => { it('deletes the recording when Delete is clicked w/o popup warning', () => { render( - + ); @@ -242,7 +238,7 @@ describe('', () => { it('downloads a recording when Download Recording is clicked', () => { render( - + ); @@ -258,7 +254,7 @@ describe('', () => { it('displays the automated analysis report when View Report is clicked', () => { render( - + ); @@ -273,7 +269,7 @@ describe('', () => { it('uploads a recording to Grafana when View in Grafana is clicked', () => { render( - + ); From 1ffd3f9027318cccb430b699b6a98558599d199b Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 8 Jun 2022 12:45:47 -0400 Subject: [PATCH 26/69] Add new Archives folder under test --- src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx | 0 src/test/Archives/Archives.test.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx create mode 100644 src/test/Archives/Archives.test.tsx diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/Archives/Archives.test.tsx b/src/test/Archives/Archives.test.tsx new file mode 100644 index 000000000..e69de29bb From 3c726e83a391a915db4fb0d645bc5bf498dfca68 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 15 Jun 2022 15:22:10 -0400 Subject: [PATCH 27/69] Use the new aggregate count field to build a Map of targets to their respective archived recordings counts --- .../AllTargetsArchivedRecordingsTable.tsx | 76 +++++++++++++------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index a7c75640c..fe79ab99f 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -40,10 +40,10 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput } from '@patternfly/react-core'; +import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; -import { of } from 'rxjs'; +import { count, of } from 'rxjs'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -52,46 +52,73 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent()); const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); const [expandedRows, setExpandedRows] = React.useState([] as string[]); const addSubscription = useSubscriptions(); const tableColumns: string[] = [ 'Target', + 'Count' ]; - React.useEffect(() => { - const sub = context.targets.targets().subscribe((targets) => { - setTargets(targets); - }); - return () => sub.unsubscribe(); - }, [context, context.targets, setTargets]); + const handleTargetsAndCounts = React.useCallback((targetNodes) => { + let updated = new Map(); + for (const node of targetNodes) { + const target: Target = { + connectUrl: node.target.serviceUri, + alias: node.alias, + } + updated.set(target, node.recordings.archived.aggregate.count as number); + } + setTargetsAndCounts(updated); + },[setTargetsAndCounts]); - const refreshTargetList = React.useCallback(() => { + const refreshTargetsAndCounts = React.useCallback(() => { addSubscription( - context.targets.queryForTargets().subscribe(() => {} /* do nothing */) + context.api.graphql(` + query { + targetNodes { + target { + serviceUri + alias + } + recordings { + archived { + aggregate { + count + } + } + } + } + }`) + .subscribe(v => handleTargetsAndCounts(v.data.targetNodes)) ); - }, [addSubscription, context.targets]); + }, [addSubscription, context, context.api]); + + React.useEffect(() => { + refreshTargetsAndCounts(); + }, [refreshTargetsAndCounts]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { return; } - const id = window.setInterval(() => refreshTargetList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + const id = window.setInterval(() => refreshTargetsAndCounts(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); return () => window.clearInterval(id); - }, [context.target, context.settings, refreshTargetList]); + }, [context.target, context.settings, refreshTargetsAndCounts]); React.useEffect(() => { + let targets = Array.from(targetsAndCounts.keys()); let searched; if (!search) { - searched = targets; + searched = [...targets]; } else { const searchText = search.trim().toLowerCase(); searched = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) } setSearchedTargets([...searched]); - }, [search, targets]); + }, [search, targetsAndCounts]); React.useEffect(() => { addSubscription( @@ -99,14 +126,12 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { const evt: TargetDiscoveryEvent = v.message.event; const target: Target = evt.serviceRef; - if (evt.kind === 'FOUND') { - setTargets(old => old.concat(target)); - } else if (evt.kind === 'LOST') { - setTargets(old => old.filter(o => o.alias != target.alias && o.connectUrl != target.connectUrl)) - } + if (evt.kind === 'FOUND' || evt.kind === 'LOST') { + refreshTargetsAndCounts(); + } }) ); - }, [addSubscription, context, context.notificationChannel, setTargets]); + }, [addSubscription, context, context.notificationChannel, refreshTargetsAndCounts]); const toggleExpanded = (id) => { const idx = expandedRows.indexOf(id); @@ -142,6 +167,11 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent + + + {targetsAndCounts.get(props.target)} + + ); }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); @@ -162,7 +192,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - ) + ); }, [props.target, props.index, context.api, isExpanded, tableColumns]); return ( From 58eaebb5e86defe318395e110fd7dc37f8eb6f95 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 15 Jun 2022 15:33:17 -0400 Subject: [PATCH 28/69] fixup! Use the new aggregate count field to build a Map of targets to their respective archived recordings counts --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index fe79ab99f..86eedc41c 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -67,7 +67,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - + {targetsAndCounts.get(props.target)} From b29248feccd6fee28f1b60b05ce8d13dbf1d27bf Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 15 Jun 2022 19:09:04 -0400 Subject: [PATCH 29/69] Update target/count state using notifications --- .../AllTargetsArchivedRecordingsTable.tsx | 77 ++++++++++++++++++- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 86eedc41c..992a691d9 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -96,6 +96,36 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + return new Map(JSON.parse( + JSON.stringify(Array.from(targetsAndCounts)) + )); + } + + const handleNewTargetAndCount = React.useCallback((target: Target, count: number) => { + const deepCopy = getDeepCopyOfTargetsAndCounts(); + deepCopy.set(target, count); + setTargetsAndCounts(deepCopy); + },[targetsAndCounts, setTargetsAndCounts]) + + const getCountForNewTarget = React.useCallback((target: Target) => { + addSubscription( + context.api.graphql(` + query { + targetNodes(filter: { name: "${target.connectUrl}" }) { + recordings { + archived { + aggregate { + count + } + } + } + } + }`) + .subscribe(v => handleNewTargetAndCount(target, v.data.targetNodes.recordings.archived.aggregate.count)) + ); + },[addSubscription, context, context.api]); + React.useEffect(() => { refreshTargetsAndCounts(); }, [refreshTargetsAndCounts]); @@ -125,14 +155,53 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { const evt: TargetDiscoveryEvent = v.message.event; - const target: Target = evt.serviceRef; - if (evt.kind === 'FOUND' || evt.kind === 'LOST') { - refreshTargetsAndCounts(); - } + const target: Target = { + connectUrl: evt.serviceRef.connectUrl, + alias: evt.serviceRef.alias, + } + if (evt.kind === 'FOUND') { + getCountForNewTarget(target); + } else if (evt.kind === 'LOST') { + const deepCopy = getDeepCopyOfTargetsAndCounts(); + deepCopy.delete(target); + setTargetsAndCounts(deepCopy); + } }) ); }, [addSubscription, context, context.notificationChannel, refreshTargetsAndCounts]); + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) + .subscribe(v => { + const target: Target = { + connectUrl: v.message.target.connectUrl, + alias: v.message.target.alias, + } + const deepCopy = getDeepCopyOfTargetsAndCounts(); + if (deepCopy.has(target)) { + setTargetsAndCounts(deepCopy.set(target, deepCopy.get(target)!+1)); + } + }) + ); + },[addSubscription, context, context.notificationChannel, targetsAndCounts,setTargetsAndCounts]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) + .subscribe(v => { + const target: Target = { + connectUrl: v.message.target.connectUrl, + alias: v.message.target.alias, + } + const deepCopy = getDeepCopyOfTargetsAndCounts(); + if (deepCopy.has(target)) { + setTargetsAndCounts(deepCopy.set(target, deepCopy.get(target)!-1)); + } + }) + ); + },[addSubscription, context, context.notificationChannel, targetsAndCounts,setTargetsAndCounts]); + const toggleExpanded = (id) => { const idx = expandedRows.indexOf(id); setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); From d1e459a129a7530ddedf4e3eb7776de97b018ad2 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 15 Jun 2022 19:13:14 -0400 Subject: [PATCH 30/69] fixup! Update target/count state using notifications --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 992a691d9..427d4440e 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -106,7 +106,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { addSubscription( @@ -124,7 +124,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent handleNewTargetAndCount(target, v.data.targetNodes.recordings.archived.aggregate.count)) ); - },[addSubscription, context, context.api]); + },[addSubscription, context, context.api, handleNewTargetAndCount]); React.useEffect(() => { refreshTargetsAndCounts(); @@ -168,7 +168,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { addSubscription( @@ -184,7 +184,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { addSubscription( @@ -200,7 +200,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { const idx = expandedRows.indexOf(id); From 92fce002a07fd3c5443637d0f3be54ceadd23742 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 16 Jun 2022 18:20:14 -0400 Subject: [PATCH 31/69] Update count using callback from child table(s) --- .../AllTargetsArchivedRecordingsTable.tsx | 49 ++++++------------- .../Recordings/ArchivedRecordingsTable.tsx | 7 +++ 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 427d4440e..3247d661f 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -43,7 +43,7 @@ import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; -import { count, of } from 'rxjs'; +import { of } from 'rxjs'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -62,6 +62,17 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + const deepCopy = getDeepCopyOfTargetsAndCounts(); + for (const [target, count] of Array.from(deepCopy.entries())) { + if (target.connectUrl === connectUrl) { + deepCopy.set(target, count+delta); + setTargetsAndCounts(deepCopy); + break; + } + } + } + const handleTargetsAndCounts = React.useCallback((targetNodes) => { let updated = new Map(); for (const node of targetNodes) { @@ -94,7 +105,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent handleTargetsAndCounts(v.data.targetNodes)) ); - }, [addSubscription, context, context.api]); + }, [addSubscription, context, context.api, handleTargetsAndCounts]); const getDeepCopyOfTargetsAndCounts = () => { return new Map(JSON.parse( @@ -170,38 +181,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) - .subscribe(v => { - const target: Target = { - connectUrl: v.message.target.connectUrl, - alias: v.message.target.alias, - } - const deepCopy = getDeepCopyOfTargetsAndCounts(); - if (deepCopy.has(target)) { - setTargetsAndCounts(deepCopy.set(target, deepCopy.get(target)!+1)); - } - }) - ); - },[addSubscription, context, context.notificationChannel, getDeepCopyOfTargetsAndCounts, setTargetsAndCounts]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) - .subscribe(v => { - const target: Target = { - connectUrl: v.message.target.connectUrl, - alias: v.message.target.alias, - } - const deepCopy = getDeepCopyOfTargetsAndCounts(); - if (deepCopy.has(target)) { - setTargetsAndCounts(deepCopy.set(target, deepCopy.get(target)!-1)); - } - }) - ); - },[addSubscription, context, context.notificationChannel, getDeepCopyOfTargetsAndCounts, setTargetsAndCounts]); - const toggleExpanded = (id) => { const idx = expandedRows.indexOf(id); setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); @@ -255,7 +234,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent {isExpanded ? - + : null} diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index cbcd3280c..6258a32b9 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -61,6 +61,7 @@ import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { target?: Observable; isUploadsTable: boolean; + updateCount?: Function; } export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { @@ -203,6 +204,9 @@ export const ArchivedRecordingsTable: React.FunctionComponent old.concat(event.message.recording)); + if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { + props.updateCount(currentTarget.connectUrl, 1) + } }) ); } @@ -257,6 +261,9 @@ export const ArchivedRecordingsTable: React.FunctionComponent v !== deleted) .map(ci => ci > deleted ? ci - 1 : ci) ); + if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { + props.updateCount(currentTarget.connectUrl, -1) + } }) ); } From 3ff141acef5192e36a4893551ab403a9583643d6 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 16 Jun 2022 18:53:36 -0400 Subject: [PATCH 32/69] Change target from optional to required in the ArchivedRecordingsTable. Clean-up notifications handling as well --- .../AllTargetsArchivedRecordingsTable.tsx | 4 +- src/app/Archives/Archives.tsx | 8 +- .../Recordings/ArchivedRecordingsTable.tsx | 119 ++++++------------ src/app/Recordings/Recordings.tsx | 2 +- .../ArchivedRecordingsTable.test.tsx | 20 +-- 5 files changed, 61 insertions(+), 92 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 3247d661f..4f8a48f3a 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -63,9 +63,9 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - const deepCopy = getDeepCopyOfTargetsAndCounts(); - for (const [target, count] of Array.from(deepCopy.entries())) { + for (const [target, count] of Array.from(targetsAndCounts.entries())) { if (target.connectUrl === connectUrl) { + const deepCopy = getDeepCopyOfTargetsAndCounts(); deepCopy.set(target, count+delta); setTargetsAndCounts(deepCopy); break; diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index a80b6d45c..2ea7e6d24 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -42,6 +42,8 @@ import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; +import { Target } from '@app/Shared/Services/Target.service'; +import { of } from 'rxjs'; export const Archives = () => { const context = React.useContext(ServiceContext); @@ -70,8 +72,12 @@ export const Archives = () => { }, [archiveEnabled]); const uploads = React.useMemo(() => { + const target: Target = { + connectUrl: '', + alias: '', + } return (<> - + ); },[]); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 6258a32b9..515fda8f9 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -59,7 +59,7 @@ import { filterRecordings, RecordingFilters } from './RecordingFilters'; import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { - target?: Observable; + target: Observable; isUploadsTable: boolean; updateCount?: Function; } @@ -67,7 +67,6 @@ export interface ArchivedRecordingsTableProps { export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); - const [target, setTarget] = React.useState(((props.target === null || props.target === undefined) ? context.target.target() : props.target) as Observable) const [recordings, setRecordings] = React.useState([] as ArchivedRecording[]); const [filteredRecordings, setFilteredRecordings] = React.useState([] as ArchivedRecording[]); const [headerChecked, setHeaderChecked] = React.useState(false); @@ -147,7 +146,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent target !== NO_TARGET), first(), @@ -159,7 +158,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -169,54 +168,47 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - if (!props.isUploadsTable) { - addSubscription( - target.subscribe(refreshRecordingList) - ); - } else { - // If this is an instance of the uploads table, then there is no target to subscribe to. - // In this case, simply refresh the recordings list after the render completes. - refreshRecordingList(); - } - }, [addSubscription, context, target, props.isUploadsTable, refreshRecordingList]); + addSubscription( + props.target.subscribe(refreshRecordingList) + ); + }, [addSubscription, context, props.target, props.isUploadsTable, refreshRecordingList]); React.useEffect(() => { - if (props.isUploadsTable) { - addSubscription( + addSubscription( + combineLatest([ + props.target, merge( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) - ).subscribe(v => setRecordings(old => old.concat(v.message.recording))) - ); - } else { - addSubscription( - combineLatest([ - target, - merge( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), - ), - ]) + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved), + ), + ]) + .subscribe(parts => { + const currentTarget = parts[0]; + const event = parts[1]; + if (currentTarget.connectUrl != event.message.target) { + return; + } + setRecordings(old => old.concat(event.message.recording)); + // If this is a nested instance in the All Targets table, update the recordings count for the parent row + if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { + props.updateCount(currentTarget.connectUrl, 1) + } + }) + ); + }, [addSubscription, context, context.notificationChannel, props.target, props.isUploadsTable, setRecordings]); + + React.useEffect(() => { + addSubscription( + combineLatest([ + props.target, + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), + ]) .subscribe(parts => { const currentTarget = parts[0]; const event = parts[1]; if (currentTarget.connectUrl != event.message.target) { return; } - setRecordings(old => old.concat(event.message.recording)); - if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { - props.updateCount(currentTarget.connectUrl, 1) - } - }) - ); - } - }, [addSubscription, context, context.notificationChannel, target, props.isUploadsTable, setRecordings]); - - React.useEffect(() => { - if (props.isUploadsTable) { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) - .subscribe(event => { let deleted; setRecordings((old) => { @@ -232,42 +224,13 @@ export const ArchivedRecordingsTable: React.FunctionComponent v !== deleted) .map(ci => ci > deleted ? ci - 1 : ci) ); - }) - ); - } else { - addSubscription( - combineLatest([ - target, - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), - ]) - .subscribe(parts => { - const currentTarget = parts[0]; - const event = parts[1]; - if (currentTarget.connectUrl != event.message.target) { - return; - } - let deleted; - - setRecordings((old) => { - return old.filter((r, i) => { - if (r.name == event.message.recording.name) { - deleted = i; - return false; - } - return true; - }); - }); - setCheckedIndices(old => old - .filter((v) => v !== deleted) - .map(ci => ci > deleted ? ci - 1 : ci) - ); - if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { - props.updateCount(currentTarget.connectUrl, -1) - } - }) - ); - } - }, [addSubscription, context, context.notificationChannel, target, props.isUploadsTable, setRecordings, setCheckedIndices]); + // If this is a nested instance in the All Targets table, update the recordings count for the parent row + if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { + props.updateCount(currentTarget.connectUrl, -1) + } + }) + ); + }, [addSubscription, context, context.notificationChannel, props.target, props.isUploadsTable, setRecordings, setCheckedIndices]); React.useEffect(() => { addSubscription( diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index 8f28c36e5..7308af31a 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -59,7 +59,7 @@ export const Recordings = () => { - + ) : ( diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index a95946c4c..f585dc365 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -130,7 +130,7 @@ describe('', () => { await act(async () => { tree = renderer.create( - + ); }); @@ -140,7 +140,7 @@ describe('', () => { it('adds a recording after receiving a notification', () => { render( - + ); expect(screen.getByText('someRecording')).toBeInTheDocument(); @@ -150,7 +150,7 @@ describe('', () => { it('updates the recording labels after receiving a notification', () => { render( - + ); expect(screen.getByText('someLabel: someUpdatedValue')).toBeInTheDocument(); @@ -160,7 +160,7 @@ describe('', () => { it('removes a recording after receiving a notification', () => { render( - + ); expect(screen.queryByText('someRecording')).not.toBeInTheDocument(); @@ -169,7 +169,7 @@ describe('', () => { it('displays the toolbar buttons', () => { render( - + ); @@ -180,7 +180,7 @@ describe('', () => { it('opens the labels drawer when Edit Labels is clicked', () => { render( - + ); @@ -219,7 +219,7 @@ describe('', () => { it('deletes the recording when Delete is clicked w/o popup warning', () => { render( - + ); @@ -238,7 +238,7 @@ describe('', () => { it('downloads a recording when Download Recording is clicked', () => { render( - + ); @@ -254,7 +254,7 @@ describe('', () => { it('displays the automated analysis report when View Report is clicked', () => { render( - + ); @@ -269,7 +269,7 @@ describe('', () => { it('uploads a recording to Grafana when View in Grafana is clicked', () => { render( - + ); From 306c57c55e2861a31a2378687cfc34b01094dd4c Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 17 Jun 2022 16:27:19 -0400 Subject: [PATCH 33/69] fixup! Change target from optional to required in the ArchivedRecordingsTable. Clean-up notifications handling as well --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 4f8a48f3a..3247d661f 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -63,9 +63,9 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - for (const [target, count] of Array.from(targetsAndCounts.entries())) { + const deepCopy = getDeepCopyOfTargetsAndCounts(); + for (const [target, count] of Array.from(deepCopy.entries())) { if (target.connectUrl === connectUrl) { - const deepCopy = getDeepCopyOfTargetsAndCounts(); deepCopy.set(target, count+delta); setTargetsAndCounts(deepCopy); break; From 966f6b80b98d340d2b4ce1c3cd9f1155cb8c2543 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 17 Jun 2022 17:29:56 -0400 Subject: [PATCH 34/69] Refactoring --- .../AllTargetsArchivedRecordingsTable.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 3247d661f..b87424da8 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -62,7 +62,13 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + const getDeepCopyOfTargetsAndCounts = React.useCallback(() => { + return new Map(JSON.parse( + JSON.stringify(Array.from(targetsAndCounts)) + )); + },[targetsAndCounts]); + + const updateCount = React.useCallback((connectUrl: string, delta: number) => { const deepCopy = getDeepCopyOfTargetsAndCounts(); for (const [target, count] of Array.from(deepCopy.entries())) { if (target.connectUrl === connectUrl) { @@ -71,7 +77,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { let updated = new Map(); @@ -107,12 +113,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return new Map(JSON.parse( - JSON.stringify(Array.from(targetsAndCounts)) - )); - } - const handleNewTargetAndCount = React.useCallback((target: Target, count: number) => { const deepCopy = getDeepCopyOfTargetsAndCounts(); deepCopy.set(target, count); From 527c276247c45a7b5dcf81ec1343dc588d227437 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Fri, 17 Jun 2022 20:27:32 -0400 Subject: [PATCH 35/69] Remove count updating from the ArchivedRecordingsTable. Start converting the Map state to separate arrays for each --- .../AllTargetsArchivedRecordingsTable.tsx | 32 +- .../AllTargetsArchivedRecordingsTableV2.tsx | 309 ++++++++++++++++++ src/app/Archives/Archives.tsx | 4 +- .../Recordings/ArchivedRecordingsTable.tsx | 9 - 4 files changed, 338 insertions(+), 16 deletions(-) create mode 100644 src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index b87424da8..c05ffd2dc 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -62,13 +62,13 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + const getDeepCopyOfTargetsAndCounts = () => { return new Map(JSON.parse( JSON.stringify(Array.from(targetsAndCounts)) )); - },[targetsAndCounts]); + }; - const updateCount = React.useCallback((connectUrl: string, delta: number) => { + const updateCount = (connectUrl: string, delta: number) => { const deepCopy = getDeepCopyOfTargetsAndCounts(); for (const [target, count] of Array.from(deepCopy.entries())) { if (target.connectUrl === connectUrl) { @@ -77,7 +77,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { let updated = new Map(); @@ -181,6 +181,24 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) + .subscribe(v => { + updateCount(v.message.target, 1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) + .subscribe(v => { + updateCount(v.message.target, -1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); + const toggleExpanded = (id) => { const idx = expandedRows.indexOf(id); setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); @@ -189,7 +207,9 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { - toggleExpanded(expandedRowId); + if (targetsAndCounts.get(props.target) !== 0 || isExpanded) { + toggleExpanded(expandedRowId); + } }; const isExpanded = React.useMemo(() => { @@ -234,7 +254,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent {isExpanded ? - + : null} diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx new file mode 100644 index 000000000..62290b518 --- /dev/null +++ b/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx @@ -0,0 +1,309 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { Target } from '@app/Shared/Services/Target.service'; +import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; +import { of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; + +export interface AllTargetsArchivedRecordingsTableV2Props { } + +export const AllTargetsArchivedRecordingsTableV2: React.FunctionComponent = () => { + const context = React.useContext(ServiceContext); + + const [targets, setTargets] = React.useState([] as Target[]); + const [counts, setCounts] = React.useState([] as number[]); + const [search, setSearch] = React.useState(''); + const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); + const [searchedCounts, setSearchedCounts] = React.useState([] as number[]); + const [expandedRows, setExpandedRows] = React.useState([] as string[]); + const addSubscription = useSubscriptions(); + + const tableColumns: string[] = [ + 'Target', + 'Count' + ]; + + const updateCount = (connectUrl: string, delta: number) => { + // const deepCopy = getDeepCopyOfTargetsAndCounts(); + // for (const [target, count] of Array.from(deepCopy.entries())) { + // if (target.connectUrl === connectUrl) { + // deepCopy.set(target, count+delta); + // setTargetsAndCounts(deepCopy); + // break; + // } + // } + }; + + const handleTargetsAndCounts = React.useCallback((targetNodes) => { + let updatedTargets: Target[] = []; + let updatedCounts: number[] = []; + for (const node of targetNodes) { + const target: Target = { + connectUrl: node.target.serviceUri, + alias: node.target.alias, + } + updatedTargets.push(target); + updatedCounts.push(node.recordings.archived.aggregate.count as number); + } + setTargets(updatedTargets); + setCounts(updatedCounts); + },[setTargets, setCounts]); + + const refreshTargetsAndCounts = () => { + addSubscription( + context.api.graphql(` + query { + targetNodes { + target { + serviceUri + alias + } + recordings { + archived { + aggregate { + count + } + } + } + } + }`) + .pipe( + map(v => v.data.targetNodes) + ) + .subscribe(handleTargetsAndCounts) + ); + }; + + const getCountForNewTarget = React.useCallback((target: Target) => { + addSubscription( + context.api.graphql(` + query { + targetNodes(filter: { name: "${target.connectUrl}" }) { + recordings { + archived { + aggregate { + count + } + } + } + } + }`) + .subscribe(v => setCounts(old => old.concat(v.data.targetNodes.recordings.archived.aggregate.count as number))) + ); + },[addSubscription, context, context.api, setCounts]); + + React.useEffect(() => { + refreshTargetsAndCounts(); + }, [refreshTargetsAndCounts]); + + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval(() => refreshTargetsAndCounts(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + return () => window.clearInterval(id); + }, [context.target, context.settings, refreshTargetsAndCounts]); + + React.useEffect(() => { + let searchedTargets; + let correspondingCounts: number[] = []; + if (!search) { + searchedTargets = targets; + } else { + const searchText = search.trim().toLowerCase(); + searchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) + + for (const t of searchedTargets) { + const idx = targets.indexOf(t); + correspondingCounts.push(counts[idx]); + } + } + setSearchedTargets([...searchedTargets]); + setSearchedCounts([...correspondingCounts]) + }, [search, targets]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery) + .subscribe(v => { + const evt: TargetDiscoveryEvent = v.message.event; + const target: Target = { + connectUrl: evt.serviceRef.connectUrl, + alias: evt.serviceRef.alias, + } + if (evt.kind === 'FOUND') { + setTargets(old => old.concat(target)); + getCountForNewTarget(target); + } else if (evt.kind === 'LOST') { + const idx = targets.indexOf(target); + setTargets(old => old.filter(o => o.connectUrl != target.connectUrl)); + setCounts(old => old.splice(idx, 1)); + } + }) + ); + }, [addSubscription, context, context.notificationChannel, getCountForNewTarget, setTargets, setCounts]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) + .subscribe(v => { + updateCount(v.message.target, 1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) + .subscribe(v => { + updateCount(v.message.target, -1); + }) + ); + }, [addSubscription, context, context.notificationChannel, updateCount]); + + const toggleExpanded = (id) => { + const idx = expandedRows.indexOf(id); + setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); + }; + + const TargetRow = (props) => { + const expandedRowId =`target-table-row-${props.index}-exp`; + const handleToggle = () => { + if (searchedCounts[props.index] !== 0 || isExpanded) { + toggleExpanded(expandedRowId); + } + }; + + const isExpanded = React.useMemo(() => { + return expandedRows.includes(expandedRowId); + }, [expandedRows, expandedRowId]); + + const parentRow = React.useMemo(() => { + return( + + + + {(props.target.alias == props.target.connectUrl) || !props.target.alias ? + `${props.target.connectUrl}` + : + `${props.target.alias} (${props.target.connectUrl})`} + + + + {searchedCounts[props.index]} + + + + ); + }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); + + const childRow = React.useMemo(() => { + return ( + + + {isExpanded ? + + + + : + null} + + + ); + }, [props.target, props.index, context.api, isExpanded, tableColumns]); + + return ( + + {parentRow} + {childRow} + + ); + } + + const targetRows = React.useMemo(() => { + return searchedTargets.map((t, idx) => ) + }, [searchedTargets, expandedRows]); + + return (<> + + + + + setSearch('')} + /> + + + + + + + + + {tableColumns.map((key , idx) => ( + {key} + ))} + + + {targetRows} + + ); +}; \ No newline at end of file diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 2ea7e6d24..c364057fb 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -40,6 +40,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Card, CardBody, EmptyState, EmptyStateIcon, Tab, Tabs, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; +import { AllTargetsArchivedRecordingsTableV2 } from './AllTargetsArchivedRecordingsTableV2'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { Target } from '@app/Shared/Services/Target.service'; @@ -67,7 +68,8 @@ export const Archives = () => { ); } return (<> - + {/* */} + ); }, [archiveEnabled]); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 515fda8f9..08667a02e 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -61,7 +61,6 @@ import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { target: Observable; isUploadsTable: boolean; - updateCount?: Function; } export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { @@ -189,10 +188,6 @@ export const ArchivedRecordingsTable: React.FunctionComponent old.concat(event.message.recording)); - // If this is a nested instance in the All Targets table, update the recordings count for the parent row - if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { - props.updateCount(currentTarget.connectUrl, 1) - } }) ); }, [addSubscription, context, context.notificationChannel, props.target, props.isUploadsTable, setRecordings]); @@ -224,10 +219,6 @@ export const ArchivedRecordingsTable: React.FunctionComponent v !== deleted) .map(ci => ci > deleted ? ci - 1 : ci) ); - // If this is a nested instance in the All Targets table, update the recordings count for the parent row - if (currentTarget.connectUrl != '' && props.updateCount !== null && props.updateCount !== undefined) { - props.updateCount(currentTarget.connectUrl, -1) - } }) ); }, [addSubscription, context, context.notificationChannel, props.target, props.isUploadsTable, setRecordings, setCheckedIndices]); From aabc83ac9833b541c21d13c539e731cec0d05a4a Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Mon, 20 Jun 2022 17:31:20 -0400 Subject: [PATCH 36/69] Finish converting Map state to separate arrays --- .../AllTargetsArchivedRecordingsTable.tsx | 87 ++--- .../AllTargetsArchivedRecordingsTableV2.tsx | 309 ------------------ src/app/Archives/Archives.tsx | 4 +- 3 files changed, 49 insertions(+), 351 deletions(-) delete mode 100644 src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index c05ffd2dc..810054ca5 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -44,6 +44,7 @@ import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -51,9 +52,11 @@ export interface AllTargetsArchivedRecordingsTableProps { } export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = () => { const context = React.useContext(ServiceContext); + const [targets, setTargets] = React.useState([] as Target[]); + const [counts, setCounts] = React.useState([] as number[]); const [search, setSearch] = React.useState(''); - const [targetsAndCounts, setTargetsAndCounts] = React.useState(new Map()); const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); + const [searchedCounts, setSearchedCounts] = React.useState([] as number[]); const [expandedRows, setExpandedRows] = React.useState([] as string[]); const addSubscription = useSubscriptions(); @@ -62,34 +65,35 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return new Map(JSON.parse( - JSON.stringify(Array.from(targetsAndCounts)) - )); - }; - - const updateCount = (connectUrl: string, delta: number) => { - const deepCopy = getDeepCopyOfTargetsAndCounts(); - for (const [target, count] of Array.from(deepCopy.entries())) { - if (target.connectUrl === connectUrl) { - deepCopy.set(target, count+delta); - setTargetsAndCounts(deepCopy); + const updateCount = React.useCallback((connectUrl: string, delta: number) => { + let idx = 0; + for (const t of targets) { + if (t.connectUrl === connectUrl) { + setCounts(old => { + let updated = [...old]; + updated[idx] += delta; + return updated; + }); break; } + idx++; } - }; + }, [targets, setCounts]); const handleTargetsAndCounts = React.useCallback((targetNodes) => { - let updated = new Map(); + let updatedTargets: Target[] = []; + let updatedCounts: number[] = []; for (const node of targetNodes) { const target: Target = { connectUrl: node.target.serviceUri, alias: node.target.alias, } - updated.set(target, node.recordings.archived.aggregate.count as number); + updatedTargets.push(target); + updatedCounts.push(node.recordings.archived.aggregate.count as number); } - setTargetsAndCounts(updated); - },[setTargetsAndCounts]); + setTargets(updatedTargets); + setCounts(updatedCounts); + },[setTargets, setCounts]); const refreshTargetsAndCounts = React.useCallback(() => { addSubscription( @@ -109,16 +113,13 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent handleTargetsAndCounts(v.data.targetNodes)) + .pipe( + map(v => v.data.targetNodes) + ) + .subscribe(handleTargetsAndCounts) ); }, [addSubscription, context, context.api, handleTargetsAndCounts]); - const handleNewTargetAndCount = React.useCallback((target: Target, count: number) => { - const deepCopy = getDeepCopyOfTargetsAndCounts(); - deepCopy.set(target, count); - setTargetsAndCounts(deepCopy); - },[getDeepCopyOfTargetsAndCounts, setTargetsAndCounts]) - const getCountForNewTarget = React.useCallback((target: Target) => { addSubscription( context.api.graphql(` @@ -133,9 +134,9 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent handleNewTargetAndCount(target, v.data.targetNodes.recordings.archived.aggregate.count)) + .subscribe(v => setCounts(old => old.concat(v.data.targetNodes.recordings.archived.aggregate.count as number))) ); - },[addSubscription, context, context.api, handleNewTargetAndCount]); + },[addSubscription, context, context.api, setCounts]); React.useEffect(() => { refreshTargetsAndCounts(); @@ -150,16 +151,23 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - let targets = Array.from(targetsAndCounts.keys()); - let searched; + let searchedTargets; + let correspondingCounts; if (!search) { - searched = [...targets]; + searchedTargets = targets; + correspondingCounts = counts; } else { const searchText = search.trim().toLowerCase(); - searched = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) + searchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) + + for (const t of searchedTargets) { + const idx = targets.indexOf(t); + correspondingCounts.push(counts[idx]); + } } - setSearchedTargets([...searched]); - }, [search, targetsAndCounts]); + setSearchedTargets([...searchedTargets]); + setSearchedCounts([...correspondingCounts]) + }, [search, targets, counts]); React.useEffect(() => { addSubscription( @@ -171,15 +179,16 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent old.concat(target)); getCountForNewTarget(target); } else if (evt.kind === 'LOST') { - const deepCopy = getDeepCopyOfTargetsAndCounts(); - deepCopy.delete(target); - setTargetsAndCounts(deepCopy); + const idx = targets.indexOf(target); + setTargets(old => old.filter(o => o.connectUrl != target.connectUrl)); + setCounts(old => old.splice(idx, 1)); } }) ); - }, [addSubscription, context, context.notificationChannel, getCountForNewTarget, getDeepCopyOfTargetsAndCounts, setTargetsAndCounts]); + }, [addSubscription, context, context.notificationChannel, getCountForNewTarget, setTargets, setCounts]); React.useEffect(() => { addSubscription( @@ -207,7 +216,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { const expandedRowId =`target-table-row-${props.index}-exp`; const handleToggle = () => { - if (targetsAndCounts.get(props.target) !== 0 || isExpanded) { + if (searchedCounts[props.index] !== 0 || isExpanded) { toggleExpanded(expandedRowId); } }; @@ -237,7 +246,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - {targetsAndCounts.get(props.target)} + {searchedCounts[props.index]} diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx deleted file mode 100644 index 62290b518..000000000 --- a/src/app/Archives/AllTargetsArchivedRecordingsTableV2.tsx +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import { ServiceContext } from '@app/Shared/Services/Services'; -import { Target } from '@app/Shared/Services/Target.service'; -import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; -import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; -import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; -import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; -import { of } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; - -export interface AllTargetsArchivedRecordingsTableV2Props { } - -export const AllTargetsArchivedRecordingsTableV2: React.FunctionComponent = () => { - const context = React.useContext(ServiceContext); - - const [targets, setTargets] = React.useState([] as Target[]); - const [counts, setCounts] = React.useState([] as number[]); - const [search, setSearch] = React.useState(''); - const [searchedTargets, setSearchedTargets] = React.useState([] as Target[]); - const [searchedCounts, setSearchedCounts] = React.useState([] as number[]); - const [expandedRows, setExpandedRows] = React.useState([] as string[]); - const addSubscription = useSubscriptions(); - - const tableColumns: string[] = [ - 'Target', - 'Count' - ]; - - const updateCount = (connectUrl: string, delta: number) => { - // const deepCopy = getDeepCopyOfTargetsAndCounts(); - // for (const [target, count] of Array.from(deepCopy.entries())) { - // if (target.connectUrl === connectUrl) { - // deepCopy.set(target, count+delta); - // setTargetsAndCounts(deepCopy); - // break; - // } - // } - }; - - const handleTargetsAndCounts = React.useCallback((targetNodes) => { - let updatedTargets: Target[] = []; - let updatedCounts: number[] = []; - for (const node of targetNodes) { - const target: Target = { - connectUrl: node.target.serviceUri, - alias: node.target.alias, - } - updatedTargets.push(target); - updatedCounts.push(node.recordings.archived.aggregate.count as number); - } - setTargets(updatedTargets); - setCounts(updatedCounts); - },[setTargets, setCounts]); - - const refreshTargetsAndCounts = () => { - addSubscription( - context.api.graphql(` - query { - targetNodes { - target { - serviceUri - alias - } - recordings { - archived { - aggregate { - count - } - } - } - } - }`) - .pipe( - map(v => v.data.targetNodes) - ) - .subscribe(handleTargetsAndCounts) - ); - }; - - const getCountForNewTarget = React.useCallback((target: Target) => { - addSubscription( - context.api.graphql(` - query { - targetNodes(filter: { name: "${target.connectUrl}" }) { - recordings { - archived { - aggregate { - count - } - } - } - } - }`) - .subscribe(v => setCounts(old => old.concat(v.data.targetNodes.recordings.archived.aggregate.count as number))) - ); - },[addSubscription, context, context.api, setCounts]); - - React.useEffect(() => { - refreshTargetsAndCounts(); - }, [refreshTargetsAndCounts]); - - React.useEffect(() => { - if (!context.settings.autoRefreshEnabled()) { - return; - } - const id = window.setInterval(() => refreshTargetsAndCounts(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); - return () => window.clearInterval(id); - }, [context.target, context.settings, refreshTargetsAndCounts]); - - React.useEffect(() => { - let searchedTargets; - let correspondingCounts: number[] = []; - if (!search) { - searchedTargets = targets; - } else { - const searchText = search.trim().toLowerCase(); - searchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) - - for (const t of searchedTargets) { - const idx = targets.indexOf(t); - correspondingCounts.push(counts[idx]); - } - } - setSearchedTargets([...searchedTargets]); - setSearchedCounts([...correspondingCounts]) - }, [search, targets]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery) - .subscribe(v => { - const evt: TargetDiscoveryEvent = v.message.event; - const target: Target = { - connectUrl: evt.serviceRef.connectUrl, - alias: evt.serviceRef.alias, - } - if (evt.kind === 'FOUND') { - setTargets(old => old.concat(target)); - getCountForNewTarget(target); - } else if (evt.kind === 'LOST') { - const idx = targets.indexOf(target); - setTargets(old => old.filter(o => o.connectUrl != target.connectUrl)); - setCounts(old => old.splice(idx, 1)); - } - }) - ); - }, [addSubscription, context, context.notificationChannel, getCountForNewTarget, setTargets, setCounts]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) - .subscribe(v => { - updateCount(v.message.target, 1); - }) - ); - }, [addSubscription, context, context.notificationChannel, updateCount]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) - .subscribe(v => { - updateCount(v.message.target, -1); - }) - ); - }, [addSubscription, context, context.notificationChannel, updateCount]); - - const toggleExpanded = (id) => { - const idx = expandedRows.indexOf(id); - setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); - }; - - const TargetRow = (props) => { - const expandedRowId =`target-table-row-${props.index}-exp`; - const handleToggle = () => { - if (searchedCounts[props.index] !== 0 || isExpanded) { - toggleExpanded(expandedRowId); - } - }; - - const isExpanded = React.useMemo(() => { - return expandedRows.includes(expandedRowId); - }, [expandedRows, expandedRowId]); - - const parentRow = React.useMemo(() => { - return( - - - - {(props.target.alias == props.target.connectUrl) || !props.target.alias ? - `${props.target.connectUrl}` - : - `${props.target.alias} (${props.target.connectUrl})`} - - - - {searchedCounts[props.index]} - - - - ); - }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); - - const childRow = React.useMemo(() => { - return ( - - - {isExpanded ? - - - - : - null} - - - ); - }, [props.target, props.index, context.api, isExpanded, tableColumns]); - - return ( - - {parentRow} - {childRow} - - ); - } - - const targetRows = React.useMemo(() => { - return searchedTargets.map((t, idx) => ) - }, [searchedTargets, expandedRows]); - - return (<> - - - - - setSearch('')} - /> - - - - - - - - - {tableColumns.map((key , idx) => ( - {key} - ))} - - - {targetRows} - - ); -}; \ No newline at end of file diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index c364057fb..2ea7e6d24 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -40,7 +40,6 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Card, CardBody, EmptyState, EmptyStateIcon, Tab, Tabs, Title } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecordingsTable'; -import { AllTargetsArchivedRecordingsTableV2 } from './AllTargetsArchivedRecordingsTableV2'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { Target } from '@app/Shared/Services/Target.service'; @@ -68,8 +67,7 @@ export const Archives = () => { ); } return (<> - {/* */} - + ); }, [archiveEnabled]); From c5f04fc39905406dd443609aedb9ff70346e114d Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 21 Jun 2022 14:55:10 -0400 Subject: [PATCH 37/69] fixup! Finish converting Map state to separate arrays --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 810054ca5..c5866ef16 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -152,7 +152,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { let searchedTargets; - let correspondingCounts; + let correspondingCounts: number[] = []; if (!search) { searchedTargets = targets; correspondingCounts = counts; From 350698f96db83dc7373d3b79f8a8c211a6430fe7 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 21 Jun 2022 17:53:20 -0400 Subject: [PATCH 38/69] Fix error where ActiveRecordingSaved notifications result in duplicate recording added --- src/app/Recordings/ArchivedRecordingsTable.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 08667a02e..9878ff07c 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -141,7 +141,11 @@ export const ArchivedRecordingsTable: React.FunctionComponent handleRecordings(v.data.archivedRecordings as ArchivedRecording[])) + queryUploadedRecordings() + .pipe( + map(v => v.data.archivedRecordings as ArchivedRecording[]) + ) + .subscribe(handleRecordings) ); } else { addSubscription( @@ -173,7 +177,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - addSubscription( + const sub = combineLatest([ props.target, merge( @@ -188,9 +192,9 @@ export const ArchivedRecordingsTable: React.FunctionComponent old.concat(event.message.recording)); - }) - ); - }, [addSubscription, context, context.notificationChannel, props.target, props.isUploadsTable, setRecordings]); + }); + return () => sub.unsubscribe(); + }, [context, context.notificationChannel, props.target, setRecordings]); React.useEffect(() => { addSubscription( @@ -221,7 +225,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( From 893f3dcba1bec8dab2ab284d679723c852066971 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 21 Jun 2022 18:37:57 -0400 Subject: [PATCH 39/69] Add LoadingView to the All Targets table --- .../AllTargetsArchivedRecordingsTable.tsx | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index c5866ef16..54d6be449 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -46,6 +46,7 @@ import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; +import { LoadingView } from '@app/LoadingView/LoadingView'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -58,6 +59,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + setIsLoading(true); addSubscription( context.api.graphql(` query { @@ -118,7 +122,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { addSubscription( @@ -284,6 +288,25 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent ) }, [searchedTargets, expandedRows]); + let view: JSX.Element; + if (isLoading) { + view = (); + } else { + view = (<> + + + + + {tableColumns.map((key , idx) => ( + {key} + ))} + + + {targetRows} + + ) + } + return (<> @@ -299,16 +322,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - - - - - {tableColumns.map((key , idx) => ( - {key} - ))} - - - {targetRows} - + {view} ); }; \ No newline at end of file From 426ec6370c5e59b3d8368ba1616539f4ac522b9f Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 28 Jun 2022 20:38:07 -0400 Subject: [PATCH 40/69] Experiment with separating processing of parent rows from target rows --- .../AllTargetsArchivedRecordingsTable.tsx | 105 ++++++++++++++---- src/app/Archives/Archives.tsx | 5 +- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 54d6be449..ce4be0680 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -47,6 +47,7 @@ import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service'; import { LoadingView } from '@app/LoadingView/LoadingView'; +import _ from 'lodash'; export interface AllTargetsArchivedRecordingsTableProps { } @@ -62,6 +63,8 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { refreshTargetsAndCounts(); - }, [refreshTargetsAndCounts]); + }, []); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { @@ -155,22 +158,32 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - let searchedTargets; - let correspondingCounts: number[] = []; + searchedTargetsRef.current = searchedTargets; + }); + + React.useEffect(() => { + let updatedSearchedTargets; + let updatedSearchedCounts: number[] = []; if (!search) { - searchedTargets = targets; - correspondingCounts = counts; + updatedSearchedTargets = targets; + updatedSearchedCounts = counts; } else { const searchText = search.trim().toLowerCase(); - searchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) + updatedSearchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) - for (const t of searchedTargets) { + for (const t of updatedSearchedTargets) { const idx = targets.indexOf(t); - correspondingCounts.push(counts[idx]); + updatedSearchedCounts.push(counts[idx]); } } - setSearchedTargets([...searchedTargets]); - setSearchedCounts([...correspondingCounts]) + + setSearchedTargets([...updatedSearchedTargets]); + setSearchedCounts([...updatedSearchedCounts]); + + // if (_.isEqual(searchedTargetsRef.current, updatedSearchedTargets) || !_.isEqual(searchedTargetsRef.current, updatedSearchedTargets)) { + // setSearchedTargets([...updatedSearchedTargets]); + // } + // setSearchedCounts([...updatedSearchedCounts]); }, [search, targets, counts]); React.useEffect(() => { @@ -217,18 +230,27 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); }; + const countBadges = React.useMemo(() => { + return searchedCounts.map(c => {c}); + }, [searchedCounts]); + const TargetRow = (props) => { const expandedRowId =`target-table-row-${props.index}-exp`; - const handleToggle = () => { - if (searchedCounts[props.index] !== 0 || isExpanded) { - toggleExpanded(expandedRowId); - } - }; const isExpanded = React.useMemo(() => { return expandedRows.includes(expandedRowId); }, [expandedRows, expandedRowId]); + const isExpandable = React.useMemo(() => { + return searchedCounts[props.index] !== 0 || isExpanded; + }, [searchedCounts]); + + const handleToggle = () => { + if (isExpandable) { + toggleExpanded(expandedRowId); + } + }; + const parentRow = React.useMemo(() => { return( @@ -249,9 +271,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - - {searchedCounts[props.index]} - + {countBadges[props.index]} ); @@ -284,6 +304,53 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + return searchedTargets.map((target, idx) => { + + + + {(target.alias == target.connectUrl) || !target.alias ? + `${target.connectUrl}` + : + `${target.alias} (${target.connectUrl})`} + + + + {searchedCounts[idx]} + + + + }); + }, [searchedTargets, searchedCounts]); + + const childRows = React.useMemo(() => { + return searchedTargets.map((target, idx) => { + + + {isExpanded ? + + + + : + null} + + + }); + }, [searchedTargets]); + const targetRows = React.useMemo(() => { return searchedTargets.map((t, idx) => ) }, [searchedTargets, expandedRows]); @@ -302,7 +369,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - {targetRows} + { targetRows } ) } diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 2ea7e6d24..720fc05d0 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -90,10 +90,7 @@ export const Archives = () => { { allTargets } - {activeTab == 1 ? - uploads - : - null} + { uploads } From d7d4f370fbe4c8b2957c7525ccf543cd17140008 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 29 Jun 2022 17:34:37 -0400 Subject: [PATCH 41/69] Separate processing of parent and child rows. Do a deep compare before updating the searchedTargets state. --- .../AllTargetsArchivedRecordingsTable.tsx | 176 +++++++----------- 1 file changed, 64 insertions(+), 112 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index ce4be0680..b6df1162e 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -145,6 +145,10 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + searchedTargetsRef.current = searchedTargets; + }); + React.useEffect(() => { refreshTargetsAndCounts(); }, []); @@ -157,10 +161,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent window.clearInterval(id); }, [context.target, context.settings, refreshTargetsAndCounts]); - React.useEffect(() => { - searchedTargetsRef.current = searchedTargets; - }); - React.useEffect(() => { let updatedSearchedTargets; let updatedSearchedCounts: number[] = []; @@ -177,13 +177,10 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { @@ -225,135 +222,90 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - const idx = expandedRows.indexOf(id); - setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); - }; - - const countBadges = React.useMemo(() => { - return searchedCounts.map(c => {c}); - }, [searchedCounts]); - - const TargetRow = (props) => { - const expandedRowId =`target-table-row-${props.index}-exp`; - - const isExpanded = React.useMemo(() => { + const isExpanded = React.useMemo(() => { + return searchedTargets.map((target, idx) => { + const expandedRowId =`target-table-row-${idx}-exp`; return expandedRows.includes(expandedRowId); - }, [expandedRows, expandedRowId]); + }); + }, [searchedTargets, expandedRows]); - const isExpandable = React.useMemo(() => { - return searchedCounts[props.index] !== 0 || isExpanded; - }, [searchedCounts]); + const isExpandable = React.useMemo(() => { + return searchedTargets.map((target, idx) => searchedCounts[idx] !== 0 || isExpanded[idx]); + }, [searchedTargets, searchedCounts, isExpanded]); - const handleToggle = () => { - if (isExpandable) { - toggleExpanded(expandedRowId); - } - }; + const toggleExpanded = React.useCallback((id) => { + const idx = expandedRows.indexOf(id); + setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); + }, [expandedRows]); + + const parentRows = React.useMemo(() => { + return searchedTargets.map((target, idx) => { + const handleToggle = () => { + if (isExpandable[idx]) { + const expandedRowId =`target-table-row-${idx}-exp`; + toggleExpanded(expandedRowId); + } + }; - const parentRow = React.useMemo(() => { - return( + return ( - - {(props.target.alias == props.target.connectUrl) || !props.target.alias ? - `${props.target.connectUrl}` + + {(target.alias == target.connectUrl) || !target.alias ? + `${target.connectUrl}` : - `${props.target.alias} (${props.target.connectUrl})`} + `${target.alias} (${target.connectUrl})`} - - {countBadges[props.index]} + + + {searchedCounts[idx]} + - - ); - }, [props.target, props.target.alias, props.target.connectUrl, props.index, isExpanded, handleToggle, tableColumns]); - - const childRow = React.useMemo(() => { + + ) + }); + }, [searchedTargets, searchedCounts, isExpandable]); + + const childRows = React.useMemo(() => { + return searchedTargets.map((target, idx) => { return ( - + - {isExpanded ? + {isExpanded[idx] ? - + : null} ); - }, [props.target, props.index, context.api, isExpanded, tableColumns]); - - return ( - - {parentRow} - {childRow} - - ); - } - - const parentRows = React.useMemo(() => { - return searchedTargets.map((target, idx) => { - - - - {(target.alias == target.connectUrl) || !target.alias ? - `${target.connectUrl}` - : - `${target.alias} (${target.connectUrl})`} - - - - {searchedCounts[idx]} - - - }); - }, [searchedTargets, searchedCounts]); + }, [searchedTargets, isExpanded]); - const childRows = React.useMemo(() => { - return searchedTargets.map((target, idx) => { - - - {isExpanded ? - - - - : - null} - - + const tableRows = React.useMemo(() => { + return parentRows.map((parentRow, idx) => { + return ( + + {parentRow} + {childRows[idx]} + + ) }); - }, [searchedTargets]); - - const targetRows = React.useMemo(() => { - return searchedTargets.map((t, idx) => ) - }, [searchedTargets, expandedRows]); + }, [parentRows, childRows]); let view: JSX.Element; if (isLoading) { @@ -369,7 +321,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - { targetRows } + { tableRows } ) } From 4c9711b1266383297a2f3779b3d610eb8d94ec66 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 30 Jun 2022 19:56:55 -0400 Subject: [PATCH 42/69] Cleanup notification handling --- .../Archives/AllTargetsArchivedRecordingsTable.tsx | 2 +- src/app/Recordings/ArchivedRecordingsTable.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index b6df1162e..c3906a8b3 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -286,7 +286,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent {isExpanded[idx] ? - + : null} diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 9878ff07c..6011a7280 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -161,7 +161,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { setFilters({ @@ -174,10 +174,10 @@ export const ArchivedRecordingsTable: React.FunctionComponent { - const sub = + addSubscription( combineLatest([ props.target, merge( @@ -192,9 +192,9 @@ export const ArchivedRecordingsTable: React.FunctionComponent old.concat(event.message.recording)); - }); - return () => sub.unsubscribe(); - }, [context, context.notificationChannel, props.target, setRecordings]); + }) + ); + }, [addSubscription, context, context.notificationChannel, setRecordings]); React.useEffect(() => { addSubscription( @@ -225,7 +225,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { addSubscription( From 95ac28dfe78797f9e706c9b2a0eeed9ea24c4772 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Sat, 9 Jul 2022 15:51:52 -0400 Subject: [PATCH 43/69] Map expanded rows based on targets instead of row index --- .../AllTargetsArchivedRecordingsTable.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index c3906a8b3..50a3cad53 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -59,7 +59,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return searchedTargets.map((target, idx) => { - const expandedRowId =`target-table-row-${idx}-exp`; - return expandedRows.includes(expandedRowId); + return searchedTargets.map((target) => { + return expandedTargets.includes(target); }); - }, [searchedTargets, expandedRows]); + }, [searchedTargets, expandedTargets]); const isExpandable = React.useMemo(() => { return searchedTargets.map((target, idx) => searchedCounts[idx] !== 0 || isExpanded[idx]); }, [searchedTargets, searchedCounts, isExpanded]); - const toggleExpanded = React.useCallback((id) => { - const idx = expandedRows.indexOf(id); - setExpandedRows(expandedRows => idx >= 0 ? [...expandedRows.slice(0, idx), ...expandedRows.slice(idx + 1, expandedRows.length)] : [...expandedRows, id]); - }, [expandedRows]); + const toggleExpanded = React.useCallback((target) => { + const idx = expandedTargets.indexOf(target); + setExpandedTargets(expandedTargets => idx >= 0 ? [...expandedTargets.slice(0, idx), ...expandedTargets.slice(idx + 1, expandedTargets.length)] : [...expandedTargets, target]); + }, [expandedTargets]); const parentRows = React.useMemo(() => { return searchedTargets.map((target, idx) => { const handleToggle = () => { if (isExpandable[idx]) { - const expandedRowId =`target-table-row-${idx}-exp`; - toggleExpanded(expandedRowId); + toggleExpanded(target); } }; @@ -294,7 +292,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent ); }); - }, [searchedTargets, isExpanded]); + }, [searchedTargets, isExpanded, tableColumns]); const tableRows = React.useMemo(() => { return parentRows.map((parentRow, idx) => { From 2b253e49a5332b9f053ba2d86cc87337008701c0 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 12 Jul 2022 20:58:52 -0400 Subject: [PATCH 44/69] Change all index-based state derivation to target-based. Use this change to hide target rows that are excluded from the search using the isHidden flag. --- .../AllTargetsArchivedRecordingsTable.tsx | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 50a3cad53..a235a3d69 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -222,15 +222,13 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return searchedTargets.map((target) => { - return expandedTargets.includes(target); - }); - }, [searchedTargets, expandedTargets]); - const isExpandable = React.useMemo(() => { - return searchedTargets.map((target, idx) => searchedCounts[idx] !== 0 || isExpanded[idx]); - }, [searchedTargets, searchedCounts, isExpanded]); + let result: Map = new Map(); + searchedTargets.map((target) => { + result.set(target, expandedTargets.includes(target) || !expandedTargets.includes(target)); + }); + return result; + }, [searchedTargets, searchedCounts, expandedTargets]); const toggleExpanded = React.useCallback((target) => { const idx = expandedTargets.indexOf(target); @@ -238,22 +236,25 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - return searchedTargets.map((target, idx) => { + let targetRows: Map = new Map(); + targets.map((target, idx) => { + let isExpanded: boolean = expandedTargets.includes(target); const handleToggle = () => { - if (isExpandable[idx]) { + if (isExpandable.get(target)) { toggleExpanded(target); } }; - return ( - + let view: JSX.Element; + view = (<> + @@ -269,20 +270,24 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - ) + ); + targetRows.set(target, view); }); - }, [searchedTargets, searchedCounts, isExpandable]); + return targetRows; + }, [searchedTargets, searchedCounts, expandedTargets]); const childRows = React.useMemo(() => { - return searchedTargets.map((target, idx) => { - return ( - + let nestedTables: Map = new Map(); + targets.map((target) => { + let view: JSX.Element; + view =(<> + - {isExpanded[idx] ? + {expandedTargets.includes(target) ? @@ -290,20 +295,11 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - ); - }); - }, [searchedTargets, isExpanded, tableColumns]); - - const tableRows = React.useMemo(() => { - return parentRows.map((parentRow, idx) => { - return ( - - {parentRow} - {childRows[idx]} - - ) + ); + nestedTables.set(target, view); }); - }, [parentRows, childRows]); + return nestedTables; + }, [targets, expandedTargets]); let view: JSX.Element; if (isLoading) { @@ -319,7 +315,12 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - { tableRows } + {targets.map((target, idx) => ( + + {parentRows.get(target)} + {childRows.get(target)} + + ))} ) } From 97162195a4d6da4fbeb7f5a182ba8b4f93cd549a Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Wed, 13 Jul 2022 16:54:50 -0400 Subject: [PATCH 45/69] Further cleanup --- .../AllTargetsArchivedRecordingsTable.tsx | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index a235a3d69..773e88e78 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -58,7 +58,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { let updatedSearchedTargets; - let updatedSearchedCounts: number[] = []; if (!search) { updatedSearchedTargets = targets; - updatedSearchedCounts = counts; } else { const searchText = search.trim().toLowerCase(); updatedSearchedTargets = targets.filter((t: Target) => t.alias.toLowerCase().includes(searchText) || t.connectUrl.toLowerCase().includes(searchText)) - - for (const t of updatedSearchedTargets) { - const idx = targets.indexOf(t); - updatedSearchedCounts.push(counts[idx]); - } } if (!_.isEqual(searchedTargetsRef.current, updatedSearchedTargets)) { setSearchedTargets([...updatedSearchedTargets]); } - setSearchedCounts([...updatedSearchedCounts]); - }, [search, targets, counts]); + }, [search, targets]); React.useEffect(() => { addSubscription( @@ -222,25 +213,17 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - let result: Map = new Map(); - searchedTargets.map((target) => { - result.set(target, expandedTargets.includes(target) || !expandedTargets.includes(target)); - }); - return result; - }, [searchedTargets, searchedCounts, expandedTargets]); - const toggleExpanded = React.useCallback((target) => { const idx = expandedTargets.indexOf(target); setExpandedTargets(expandedTargets => idx >= 0 ? [...expandedTargets.slice(0, idx), ...expandedTargets.slice(idx + 1, expandedTargets.length)] : [...expandedTargets, target]); }, [expandedTargets]); - const parentRows = React.useMemo(() => { + const targetRows = React.useMemo(() => { let targetRows: Map = new Map(); targets.map((target, idx) => { let isExpanded: boolean = expandedTargets.includes(target); const handleToggle = () => { - if (isExpandable.get(target)) { + if (counts[idx] !== 0 || isExpanded) { toggleExpanded(target); } }; @@ -266,7 +249,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - {searchedCounts[idx]} + {counts[idx]} @@ -274,20 +257,21 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - let nestedTables: Map = new Map(); - targets.map((target) => { + const recordingRows = React.useMemo(() => { + let recordingRows: Map = new Map(); + targets.map((target, idx) => { + let isExpanded: boolean = expandedTargets.includes(target); let view: JSX.Element; view =(<> - + - {expandedTargets.includes(target) ? + {isExpanded ? @@ -296,31 +280,32 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent ); - nestedTables.set(target, view); + recordingRows.set(target, view); }); - return nestedTables; - }, [targets, expandedTargets]); + return recordingRows; + }, [targets, expandedTargets, searchedTargets]); + + const tableRows = React.useMemo(() => { + return targets.map((target) => <>{targetRows.get(target)}{recordingRows.get(target)}); + }, [targets, targetRows, recordingRows]); let view: JSX.Element; if (isLoading) { view = (); } else { view = (<> - + - {tableColumns.map((key , idx) => ( + {tableColumns.map((key) => ( {key} ))} - {targets.map((target, idx) => ( - - {parentRows.get(target)} - {childRows.get(target)} - - ))} + + {tableRows} + ) } From 46c38e4536199f23878b06203220d2bdcfdcb843 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 14 Jul 2022 14:14:27 -0400 Subject: [PATCH 46/69] Further cleanup --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 773e88e78..daddc84c1 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -189,6 +189,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent old.filter(o => o.connectUrl != target.connectUrl)); + setExpandedTargets(old => old.filter(o => o != target)); setCounts(old => old.splice(idx, 1)); } }) @@ -222,6 +223,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent = new Map(); targets.map((target, idx) => { let isExpanded: boolean = expandedTargets.includes(target); + const handleToggle = () => { if (counts[idx] !== 0 || isExpanded) { toggleExpanded(target); @@ -257,7 +259,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { let recordingRows: Map = new Map(); @@ -287,7 +289,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { return targets.map((target) => <>{targetRows.get(target)}{recordingRows.get(target)}); - }, [targets, targetRows, recordingRows]); + }, [targetRows, recordingRows]); let view: JSX.Element; if (isLoading) { From b1c238a45d7ab1e21c2f24616c1c3c6872b29575 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 14 Jul 2022 20:19:02 -0400 Subject: [PATCH 47/69] Refactoring --- .../AllTargetsArchivedRecordingsTable.tsx | 44 +++++++++---------- src/app/Archives/Archives.tsx | 2 +- src/app/Recordings/Recordings.tsx | 2 +- .../ArchivedRecordingsTable.test.tsx | 20 ++++----- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index daddc84c1..01c4c6bfc 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -220,8 +220,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { - let targetRows: Map = new Map(); - targets.map((target, idx) => { + return targets.map((target, idx) => { let isExpanded: boolean = expandedTargets.includes(target); const handleToggle = () => { @@ -230,9 +229,8 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - + return ( + - - + + {counts[idx]} - + - ); - targetRows.set(target, view); + ); }); - return targetRows; }, [targets, expandedTargets, counts, searchedTargets]); const recordingRows = React.useMemo(() => { - let recordingRows: Map = new Map(); - targets.map((target, idx) => { + return targets.map((target, idx) => { let isExpanded: boolean = expandedTargets.includes(target); - let view: JSX.Element; - view =(<> + + return ( {isExpanded ? - + : null} - ); - recordingRows.set(target, view); + ); }); - return recordingRows; }, [targets, expandedTargets, searchedTargets]); - const tableRows = React.useMemo(() => { - return targets.map((target) => <>{targetRows.get(target)}{recordingRows.get(target)}); + const rowPairs = React.useMemo(() => { + let rowPairs: JSX.Element[] = []; + for (let i = 0; i < targetRows.length; i++) { + rowPairs.push(targetRows[i]); + rowPairs.push(recordingRows[i]); + } + return rowPairs; }, [targetRows, recordingRows]); let view: JSX.Element; @@ -305,8 +303,8 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - - {tableRows} + + {rowPairs} ) diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index 720fc05d0..be2493931 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -77,7 +77,7 @@ export const Archives = () => { alias: '', } return (<> - + ); },[]); diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index 7308af31a..4c8a11d22 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -59,7 +59,7 @@ export const Recordings = () => { - + ) : ( diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index f585dc365..0a0c80898 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -130,7 +130,7 @@ describe('', () => { await act(async () => { tree = renderer.create( - + ); }); @@ -140,7 +140,7 @@ describe('', () => { it('adds a recording after receiving a notification', () => { render( - + ); expect(screen.getByText('someRecording')).toBeInTheDocument(); @@ -150,7 +150,7 @@ describe('', () => { it('updates the recording labels after receiving a notification', () => { render( - + ); expect(screen.getByText('someLabel: someUpdatedValue')).toBeInTheDocument(); @@ -160,7 +160,7 @@ describe('', () => { it('removes a recording after receiving a notification', () => { render( - + ); expect(screen.queryByText('someRecording')).not.toBeInTheDocument(); @@ -169,7 +169,7 @@ describe('', () => { it('displays the toolbar buttons', () => { render( - + ); @@ -180,7 +180,7 @@ describe('', () => { it('opens the labels drawer when Edit Labels is clicked', () => { render( - + ); @@ -219,7 +219,7 @@ describe('', () => { it('deletes the recording when Delete is clicked w/o popup warning', () => { render( - + ); @@ -238,7 +238,7 @@ describe('', () => { it('downloads a recording when Download Recording is clicked', () => { render( - + ); @@ -254,7 +254,7 @@ describe('', () => { it('displays the automated analysis report when View Report is clicked', () => { render( - + ); @@ -269,7 +269,7 @@ describe('', () => { it('uploads a recording to Grafana when View in Grafana is clicked', () => { render( - + ); From 4057cbaf3a80c141f5b217ab3972481a8c959826 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Thu, 14 Jul 2022 20:34:30 -0400 Subject: [PATCH 48/69] Update Patternfly package to latest --- package.json | 8 ++--- yarn.lock | 82 ++++++++++++++++++++++++++-------------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index e0c65d66e..11a4f730a 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,10 @@ "yarn": "^1.22.13" }, "dependencies": { - "@patternfly/react-core": "^4.157.3", - "@patternfly/react-icons": "^4.11.17", - "@patternfly/react-styles": "^4.11.16", - "@patternfly/react-table": "^4.30.3", + "@patternfly/react-core": "^4.224.1", + "@patternfly/react-icons": "^4.75.1", + "@patternfly/react-styles": "^4.74.1", + "@patternfly/react-table": "^4.93.1", "@types/lodash": "^4.14.175", "express": "^4.17.1", "react": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index ba2bcb49c..f92044a35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -832,45 +832,45 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@patternfly/react-core@^4.157.3": - version "4.157.3" - resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.157.3.tgz" - integrity sha512-vP4/lZLTy0U4jmVP7ZO8I7EX1qSyVyFFbay01Pj1pVGHo74gP7yaUFwMvAvURGYmNeWdAhxgIBfYV8VimkSwLg== - dependencies: - "@patternfly/react-icons" "^4.11.17" - "@patternfly/react-styles" "^4.11.16" - "@patternfly/react-tokens" "^4.12.18" - focus-trap "6.2.2" +"@patternfly/react-core@^4.224.1": + version "4.224.1" + resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.224.1.tgz#d8d81e7cf611fd441f9fb970db8e0c3a36fa508b" + integrity sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ== + dependencies: + "@patternfly/react-icons" "^4.75.1" + "@patternfly/react-styles" "^4.74.1" + "@patternfly/react-tokens" "^4.76.1" + focus-trap "6.9.2" react-dropzone "9.0.0" tippy.js "5.1.2" tslib "^2.0.0" -"@patternfly/react-icons@^4.11.17": - version "4.11.17" - resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.11.17.tgz" - integrity sha512-T6HriEy2SgVxlQxPL0FTHQqBYdPbaMeEiK4CzIAPQvCuCT3kRUEEGNyG+VVEvc+XU8ndSiTJdOkHaq08onFvsg== - -"@patternfly/react-styles@^4.11.16": - version "4.11.16" - resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.11.16.tgz" - integrity sha512-4ZFynQuJmRF7VbZeQSs44MX6MEvW7l7ZR8lMeChd8mxnQpG8pWtVUbcHdj9FFHPZVa+sPrgrZQl8QmhbqYyOsg== - -"@patternfly/react-table@^4.30.3": - version "4.30.3" - resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.30.3.tgz" - integrity sha512-qjaXN3iJQdV6XPfv7TTK/zF6pt8IKreJOJy5k+FfFqYJfO32/04BTS1OnKGU8QM0AKl8bKyzQ6RGoXXxyMZBQQ== - dependencies: - "@patternfly/react-core" "^4.157.3" - "@patternfly/react-icons" "^4.11.17" - "@patternfly/react-styles" "^4.11.16" - "@patternfly/react-tokens" "^4.12.18" +"@patternfly/react-icons@^4.75.1": + version "4.75.1" + resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.75.1.tgz#3567b5a21a7f52c6a272f0330357fac87083867a" + integrity sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w== + +"@patternfly/react-styles@^4.74.1": + version "4.74.1" + resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.74.1.tgz#3cd19cd31dd896bfd046f79241e8a8aefbfb1152" + integrity sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A== + +"@patternfly/react-table@^4.93.1": + version "4.93.1" + resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.93.1.tgz#9b5ed60a80a27a3440ef168d93d65b588cc4b58a" + integrity sha512-N/zHkNsY3X3yUXPg6COwdZKAFmTCbWm25qCY2aHjrXlIlE2OKWaYvVag0CcTwPiQhIuCumztr9Y2Uw9uvv0Fsw== + dependencies: + "@patternfly/react-core" "^4.224.1" + "@patternfly/react-icons" "^4.75.1" + "@patternfly/react-styles" "^4.74.1" + "@patternfly/react-tokens" "^4.76.1" lodash "^4.17.19" tslib "^2.0.0" -"@patternfly/react-tokens@^4.12.18": - version "4.12.18" - resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.12.18.tgz" - integrity sha512-3bNUOSOMLmhxPku4fvopxt3StotaHGqHvlIDMxp9pGIgb0o02RyZ8JIioCCO1GkvPPIn6pKs/cGJDlB7zHV48Q== +"@patternfly/react-tokens@^4.76.1": + version "4.76.1" + resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz#ed85c3f6c6e779398579467e566d6750d01e8319" + integrity sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw== "@polka/url@^1.0.0-next.9": version "1.0.0-next.12" @@ -3476,12 +3476,12 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== -focus-trap@6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.2.2.tgz" - integrity sha512-qWovH9+LGoKqREvJaTCzJyO0hphQYGz+ap5Hc4NqXHNhZBdxCi5uBPPcaOUw66fHmzXLVwvETLvFgpwPILqKpg== +focus-trap@6.9.2: + version "6.9.2" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.9.2.tgz#a9ef72847869bd2cbf62cb930aaf8e138fef1ca9" + integrity sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw== dependencies: - tabbable "^5.1.4" + tabbable "^5.3.2" follow-redirects@^1.0.0: version "1.15.1" @@ -6917,10 +6917,10 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tabbable@^5.1.4: - version "5.2.0" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.0.tgz" - integrity sha512-0uyt8wbP0P3T4rrsfYg/5Rg3cIJ8Shl1RJ54QMqYxm1TLdWqJD1u6+RQjr2Lor3wmfT7JRHkirIwy99ydBsyPg== +tabbable@^5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== table@^6.0.9: version "6.7.1" From c16f335537f8d96d3dfcf44e4cccf05769592cc9 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Sat, 16 Jul 2022 17:16:54 -0400 Subject: [PATCH 49/69] Make the nested table small and scrollable to fix whitespace scrolling issue --- .../AllTargetsArchivedRecordingsTable.tsx | 10 +++++++--- src/app/Archives/Archives.tsx | 2 +- src/app/BreadcrumbPage/BreadcrumbPage.tsx | 2 +- src/app/Recordings/ActiveRecordingsTable.tsx | 1 + .../Recordings/ArchivedRecordingsTable.tsx | 18 ++++++++++------- src/app/Recordings/Recordings.tsx | 2 +- src/app/Recordings/RecordingsTable.tsx | 20 +++++++++++++++---- .../ArchivedRecordingsTable.test.tsx | 20 +++++++++---------- src/test/Recordings/RecordingsTable.test.tsx | 0 9 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 src/test/Recordings/RecordingsTable.test.tsx diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 01c4c6bfc..64c5bcbf5 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -41,7 +41,7 @@ import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; -import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, OuterScrollContainer, InnerScrollContainer} from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -270,7 +270,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent {isExpanded ? - + : null} @@ -289,12 +289,16 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent component + // because searching targets results in row borders disappearing if + // we instead enclose each row pair inside its own , which is + // unfortunately the proper way to handle nested tables. let view: JSX.Element; if (isLoading) { view = (); } else { view = (<> - + diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index be2493931..8a7889e8b 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -77,7 +77,7 @@ export const Archives = () => { alias: '', } return (<> - + ); },[]); diff --git a/src/app/BreadcrumbPage/BreadcrumbPage.tsx b/src/app/BreadcrumbPage/BreadcrumbPage.tsx index 2f3062855..8bffa88d0 100644 --- a/src/app/BreadcrumbPage/BreadcrumbPage.tsx +++ b/src/app/BreadcrumbPage/BreadcrumbPage.tsx @@ -51,7 +51,7 @@ export interface BreadcrumbTrail { export const BreadcrumbPage: React.FunctionComponent = (props) => { return (<> - + { (props.breadcrumbs || []).map( diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 418c5407a..d3858445a 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -569,6 +569,7 @@ export const ActiveRecordingsTable: React.FunctionComponent {recordingRows} diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 6011a7280..84c68a004 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -41,7 +41,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Button, Checkbox, Drawer, DrawerContent, DrawerContentBody, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; -import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import { Tbody, Th, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; import { PlusIcon } from '@patternfly/react-icons'; import { RecordingActions } from './RecordingActions'; import { RecordingsTable } from './RecordingsTable'; @@ -61,6 +61,7 @@ import { ArchiveUploadModal } from '@app/Archives/ArchiveUploadModal'; export interface ArchivedRecordingsTableProps { target: Observable; isUploadsTable: boolean; + isNestedTable: boolean; } export const ArchivedRecordingsTable: React.FunctionComponent = (props) => { @@ -291,7 +292,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent { return( - + - context.api.uploadArchivedRecordingToGrafana(props.recording.name)} - /> + + context.api.uploadArchivedRecordingToGrafana(props.recording.name)} + /> + ); }, [props.recording, props.recording.metadata.labels, props.recording.name, props.index, handleCheck, checkedIndices, isExpanded, handleToggle, tableColumns, context.api]); @@ -432,6 +435,7 @@ export const ArchivedRecordingsTable: React.FunctionComponent {recordingRows} diff --git a/src/app/Recordings/Recordings.tsx b/src/app/Recordings/Recordings.tsx index 4c8a11d22..03eee355a 100644 --- a/src/app/Recordings/Recordings.tsx +++ b/src/app/Recordings/Recordings.tsx @@ -59,7 +59,7 @@ export const Recordings = () => { - + ) : ( diff --git a/src/app/Recordings/RecordingsTable.tsx b/src/app/Recordings/RecordingsTable.tsx index bd2f02868..d442f21fd 100644 --- a/src/app/Recordings/RecordingsTable.tsx +++ b/src/app/Recordings/RecordingsTable.tsx @@ -38,7 +38,7 @@ import * as React from 'react'; import { Title, EmptyState, EmptyStateIcon, EmptyStateBody, Button, EmptyStateSecondaryActions } from '@patternfly/react-core'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import { TableComposable, Thead, Tr, Th } from '@patternfly/react-table'; +import { TableComposable, Thead, Tr, Th, OuterScrollContainer, InnerScrollContainer } from '@patternfly/react-table'; import { LoadingView } from '@app/LoadingView/LoadingView'; import { ErrorView } from '@app/ErrorView/ErrorView'; @@ -50,6 +50,7 @@ export interface RecordingsTableProps { isEmptyFilterResult?: boolean; isHeaderChecked: boolean; isLoading: boolean; + isNestedTable: boolean; errorMessage: string; onHeaderCheck: (event, checked: boolean) => void; clearFilters?: (filterType) => void; @@ -90,8 +91,8 @@ export const RecordingsTable: React.FunctionComponent = (p ); } else { view = (<> - - + + = (p }} /> - {props.tableColumns.map((key , idx) => ( + {props.tableColumns.map((key, idx) => ( {key} ))} @@ -112,6 +113,17 @@ export const RecordingsTable: React.FunctionComponent = (p ); } + if (props.isNestedTable) { + return (<> + + { props.toolbar } + + { view } + + + ); + } + return (<> { props.toolbar } { view } diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 0a0c80898..a64b9e020 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -130,7 +130,7 @@ describe('', () => { await act(async () => { tree = renderer.create( - + ); }); @@ -140,7 +140,7 @@ describe('', () => { it('adds a recording after receiving a notification', () => { render( - + ); expect(screen.getByText('someRecording')).toBeInTheDocument(); @@ -150,7 +150,7 @@ describe('', () => { it('updates the recording labels after receiving a notification', () => { render( - + ); expect(screen.getByText('someLabel: someUpdatedValue')).toBeInTheDocument(); @@ -160,7 +160,7 @@ describe('', () => { it('removes a recording after receiving a notification', () => { render( - + ); expect(screen.queryByText('someRecording')).not.toBeInTheDocument(); @@ -169,7 +169,7 @@ describe('', () => { it('displays the toolbar buttons', () => { render( - + ); @@ -180,7 +180,7 @@ describe('', () => { it('opens the labels drawer when Edit Labels is clicked', () => { render( - + ); @@ -219,7 +219,7 @@ describe('', () => { it('deletes the recording when Delete is clicked w/o popup warning', () => { render( - + ); @@ -238,7 +238,7 @@ describe('', () => { it('downloads a recording when Download Recording is clicked', () => { render( - + ); @@ -254,7 +254,7 @@ describe('', () => { it('displays the automated analysis report when View Report is clicked', () => { render( - + ); @@ -269,7 +269,7 @@ describe('', () => { it('uploads a recording to Grafana when View in Grafana is clicked', () => { render( - + ); diff --git a/src/test/Recordings/RecordingsTable.test.tsx b/src/test/Recordings/RecordingsTable.test.tsx new file mode 100644 index 000000000..e69de29bb From f5e04cf32aa66c2ecca9a3bf5825100eca7835d9 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Sat, 16 Jul 2022 17:37:06 -0400 Subject: [PATCH 50/69] Hide targets with zero recordings --- .../AllTargetsArchivedRecordingsTable.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 64c5bcbf5..2aaa100a5 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -40,7 +40,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge } from '@patternfly/react-core'; +import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge, Checkbox } from '@patternfly/react-core'; import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, OuterScrollContainer, InnerScrollContainer} from '@patternfly/react-table'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { of } from 'rxjs'; @@ -59,6 +59,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent + {(target.alias == target.connectUrl) || !target.alias ? `${target.connectUrl}` - : + : `${target.alias} (${target.connectUrl})`} @@ -255,14 +256,14 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent ); }); - }, [targets, expandedTargets, counts, searchedTargets]); + }, [targets, expandedTargets, counts, searchedTargets, hideEmptyTargets]); const recordingRows = React.useMemo(() => { return targets.map((target, idx) => { let isExpanded: boolean = expandedTargets.includes(target); return ( - + ); }); - }, [targets, expandedTargets, searchedTargets]); + }, [targets, expandedTargets, searchedTargets, hideEmptyTargets, counts]); const rowPairs = React.useMemo(() => { let rowPairs: JSX.Element[] = []; @@ -327,6 +328,18 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent + + + setHideEmptyTargets(old => !old)} + isChecked={hideEmptyTargets} + id={`all-archives-hide-check`} + aria-label={`all-archives-hide-check`} + /> + + {view} From e8fc5de3d55e25be713073ba241eac5550203e28 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Sat, 16 Jul 2022 18:56:40 -0400 Subject: [PATCH 51/69] fixup! Hide targets with zero recordings --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 6 +----- src/app/BreadcrumbPage/BreadcrumbPage.tsx | 2 +- src/app/Recordings/RecordingsTable.tsx | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 2aaa100a5..d016740e5 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -248,7 +248,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent - + {counts[idx]} @@ -290,10 +290,6 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent component - // because searching targets results in row borders disappearing if - // we instead enclose each row pair inside its own , which is - // unfortunately the proper way to handle nested tables. let view: JSX.Element; if (isLoading) { view = (); diff --git a/src/app/BreadcrumbPage/BreadcrumbPage.tsx b/src/app/BreadcrumbPage/BreadcrumbPage.tsx index 8bffa88d0..2f3062855 100644 --- a/src/app/BreadcrumbPage/BreadcrumbPage.tsx +++ b/src/app/BreadcrumbPage/BreadcrumbPage.tsx @@ -51,7 +51,7 @@ export interface BreadcrumbTrail { export const BreadcrumbPage: React.FunctionComponent = (props) => { return (<> - + { (props.breadcrumbs || []).map( diff --git a/src/app/Recordings/RecordingsTable.tsx b/src/app/Recordings/RecordingsTable.tsx index d442f21fd..3a14fa4cd 100644 --- a/src/app/Recordings/RecordingsTable.tsx +++ b/src/app/Recordings/RecordingsTable.tsx @@ -91,8 +91,8 @@ export const RecordingsTable: React.FunctionComponent = (p ); } else { view = (<> - - + + Date: Mon, 18 Jul 2022 14:00:33 -0400 Subject: [PATCH 52/69] Fix rebase --- .../UploadedArchivedRecordingsTable.tsx | 322 ------------------ .../ArchivedRecordingsTable.test.tsx | 2 +- 2 files changed, 1 insertion(+), 323 deletions(-) delete mode 100644 src/app/Archives/UploadedArchivedRecordingsTable.tsx diff --git a/src/app/Archives/UploadedArchivedRecordingsTable.tsx b/src/app/Archives/UploadedArchivedRecordingsTable.tsx deleted file mode 100644 index f5d8787fb..000000000 --- a/src/app/Archives/UploadedArchivedRecordingsTable.tsx +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import { ArchivedRecording } from '@app/Shared/Services/Api.service'; -import { ServiceContext } from '@app/Shared/Services/Services'; -import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; -import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Checkbox, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; -import { Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; -import { RecordingActions } from '@app/Recordings/RecordingActions'; -import { RecordingsTable } from '@app/Recordings/RecordingsTable'; -import { ReportFrame } from '@app/Recordings/ReportFrame'; -import { Observable, forkJoin, merge } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { UploadIcon } from '@patternfly/react-icons'; -import { ArchiveUploadModal } from './ArchiveUploadModal'; -import { parseLabels } from '@app/RecordingMetadata/RecordingLabel'; -import { LabelCell } from '@app/RecordingMetadata/LabelCell'; -import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; -import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; - -export interface UploadedArchivedRecordingsTableProps { } - -export const UploadedArchivedRecordingsTable: React.FunctionComponent = () => { - const context = React.useContext(ServiceContext); - - const [recordings, setRecordings] = 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 [showUploadModal, setShowUploadModal] = React.useState(false); - const [warningModalOpen, setWarningModalOpen] = React.useState(false); - const [isLoading, setIsLoading] = React.useState(false); - const addSubscription = useSubscriptions(); - - const tableColumns: string[] = [ - 'Name', - 'Labels', - ]; - - const handleHeaderCheck = React.useCallback((event, checked) => { - setHeaderChecked(checked); - setCheckedIndices(checked ? Array.from(new Array(recordings.length), (x, i) => i) : []); - }, [setHeaderChecked, setCheckedIndices, recordings]); - - const handleRowCheck = React.useCallback((checked, index) => { - if (checked) { - setCheckedIndices(ci => ([...ci, index])); - } else { - setHeaderChecked(false); - setCheckedIndices(ci => ci.filter(v => v !== index)); - } - }, [setCheckedIndices, setHeaderChecked]); - - const handleRecordings = React.useCallback((recordings) => { - setRecordings(recordings); - setIsLoading(false); - }, [setRecordings, setIsLoading]); - - const refreshRecordingList = React.useCallback(() => { - setIsLoading(true); - addSubscription( - context.api.graphql(` - query { - archivedRecordings(filter: { sourceTarget: "uploads" }) { - name - downloadUrl - reportUrl - metadata { - labels - } - } - }`) - .subscribe(v => handleRecordings(v.data.archivedRecordings as ArchivedRecording[])) - ); - }, [addSubscription, context, context.api, setIsLoading, handleRecordings]); - - React.useEffect(() => { - addSubscription( - context.target.target().subscribe(refreshRecordingList) - ); - }, [addSubscription, context, context.target, refreshRecordingList]); - - React.useEffect(() => { - addSubscription( - merge( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated), - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved) - ).subscribe(v => setRecordings(old => old.concat(v.message.recording))) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted) - .subscribe(v => setRecordings(old => old.filter(o => o.name != v.message.recording.name))) - ) - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated) - .subscribe(v => setRecordings(old => old.map( - o => o.name == v.message.recordingName ? { ...o, metadata: { labels: v.message.metadata.labels } } : o))) - ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); - - - const handleDeleteRecordings = () => { - const tasks: Observable[] = []; - recordings.forEach((r: ArchivedRecording, idx) => { - if (checkedIndices.includes(idx)) { - handleRowCheck(false, idx); - context.reports.delete(r); - tasks.push( - context.api.deleteArchivedRecording(r.name).pipe(first()) - ); - } - }); - addSubscription( - forkJoin(tasks).subscribe() - ); - }; - - 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(() => { - if (!context.settings.autoRefreshEnabled()) { - return; - } - const id = window.setInterval(() => refreshRecordingList(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); - return () => window.clearInterval(id); - }, [context, context.settings, refreshRecordingList]); - - const RecordingRow = (props) => { - const parsedLabels = React.useMemo(() => { - return parseLabels(props.recording.metadata.labels); - }, [props.recording.metadata.labels]); - - const expandedRowId =`archived-table-row-${props.index}-exp`; - const handleToggle = () => { - toggleExpanded(expandedRowId); - }; - - const isExpanded = React.useMemo(() => { - return expandedRows.includes(expandedRowId); - }, [expandedRows, expandedRowId]); - - const handleCheck = (checked) => { - handleRowCheck(checked, props.index); - }; - - const parentRow = React.useMemo(() => { - return( - - - - - - - {props.recording.name} - - - - - context.api.uploadArchivedRecordingToGrafana(props.recording.name)} - /> - - ); - }, [props.recording, props.recording.metadata.labels, props.recording.name, props.index, handleCheck, checkedIndices, isExpanded, handleToggle, tableColumns, context.api]); - - const childRow = React.useMemo(() => { - return ( - - - - - - - - ) - }, [props.recording, props.recording.name, props.index, isExpanded, tableColumns]); - - return ( - - {parentRow} - {childRow} - - ); - }; - - const handleDeleteButton = React.useCallback(() => { - if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteArchivedRecordings)) { - setWarningModalOpen(true); - } - else { - handleDeleteRecordings(); - } - }, [context, context.settings, setWarningModalOpen, handleDeleteRecordings]) - - const handleWarningModalClose = React.useCallback(() => { - setWarningModalOpen(false); - }, [setWarningModalOpen]); - - const RecordingsToolbar = () => { - const deleteArchivedWarningModal = React.useMemo(() => { - return - }, [recordings, checkedIndices]); - return ( - - - - - - - - - - - - - { deleteArchivedWarningModal } - - - ); - }; - - const recordingRows = React.useMemo(() => { - return recordings.map((r, idx) => ) - }, [recordings, expandedRows, checkedIndices]); - - const handleModalClose = React.useCallback(() => { - setShowUploadModal(false); - refreshRecordingList(); - }, [setShowUploadModal, refreshRecordingList]); - - return (<> - } - tableColumns={tableColumns} - isHeaderChecked={headerChecked} - onHeaderCheck={handleHeaderCheck} - isLoading={isLoading} - isEmpty={!recordings.length} - errorMessage='' - > - {recordingRows} - - - - ); -}; - diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index a64b9e020..7cdcd8867 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -194,7 +194,7 @@ describe('', () => { it('shows a popup when Delete is clicked and then deletes the recording after clicking confirmation Delete', () => { render( - + ); From 5da22432534a152000b1bae657a108c57c36ffb3 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Mon, 18 Jul 2022 15:47:03 -0400 Subject: [PATCH 53/69] Add missing licenses --- ...AllTargetsArchivedRecordingsTable.test.tsx | 37 +++++++++++++++++++ src/test/Archives/Archives.test.tsx | 37 +++++++++++++++++++ src/test/Recordings/RecordingsTable.test.tsx | 37 +++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index e69de29bb..7287a1a61 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ \ No newline at end of file diff --git a/src/test/Archives/Archives.test.tsx b/src/test/Archives/Archives.test.tsx index e69de29bb..7287a1a61 100644 --- a/src/test/Archives/Archives.test.tsx +++ b/src/test/Archives/Archives.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ \ No newline at end of file diff --git a/src/test/Recordings/RecordingsTable.test.tsx b/src/test/Recordings/RecordingsTable.test.tsx index e69de29bb..7287a1a61 100644 --- a/src/test/Recordings/RecordingsTable.test.tsx +++ b/src/test/Recordings/RecordingsTable.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ \ No newline at end of file From 3e272a45a48e5d11a494bcb4a6a3ac273d0a3f31 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Mon, 18 Jul 2022 17:46:28 -0400 Subject: [PATCH 54/69] Add EmptyState when recordings list is empty --- .../Archives/AllTargetsArchivedRecordingsTable.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index d016740e5..93c7c5746 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -40,8 +40,9 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge, Checkbox } from '@patternfly/react-core'; -import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, OuterScrollContainer, InnerScrollContainer} from '@patternfly/react-table'; +import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, SearchInput, Badge, Checkbox, EmptyState, EmptyStateIcon, Title } from '@patternfly/react-core'; +import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -293,6 +294,15 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent); + } else if (searchedTargets.length === 0 || (hideEmptyTargets && counts.reduce((a, b) => a + b, 0) === 0)) { + view = (<> + + + + No Targets + + + ); } else { view = (<> From 27c6fbb37a5c657befbab12d6d9e7149ab7763bd Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Mon, 18 Jul 2022 18:14:14 -0400 Subject: [PATCH 55/69] Refactoring --- src/app/Archives/AllTargetsArchivedRecordingsTable.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 93c7c5746..056910f2a 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -291,10 +291,16 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent { + return ( + searchedTargets.length === 0 || (hideEmptyTargets && counts.reduce((a, b) => a + b, 0) === 0) + ); + }, [searchedTargets, hideEmptyTargets, counts]); + let view: JSX.Element; if (isLoading) { view = (); - } else if (searchedTargets.length === 0 || (hideEmptyTargets && counts.reduce((a, b) => a + b, 0) === 0)) { + } else if (noTargets) { view = (<> From 7a50ad169733d6b3dacb0030f6ac62caea3b8e79 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 19 Jul 2022 16:23:22 -0400 Subject: [PATCH 56/69] Start updating tests --- src/test/Archives/Archives.test.tsx | 52 ++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/test/Archives/Archives.test.tsx b/src/test/Archives/Archives.test.tsx index 7287a1a61..80dd4c734 100644 --- a/src/test/Archives/Archives.test.tsx +++ b/src/test/Archives/Archives.test.tsx @@ -34,4 +34,54 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - */ \ No newline at end of file + */ + +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, within } from '@testing-library/react'; +import renderer, { act } from 'react-test-renderer'; +import '@testing-library/jest-dom'; +import { of } from 'rxjs'; + +import { Archives } from '@app/Archives/Archives'; +import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; + +jest.mock('@app/Recordings/ArchivedRecordingsTable', () => { + return { + ArchivedRecordingsTable: jest.fn((props) => { + return ( +
+ Archived Recordings Table +
+ ) + }), + }; +}); + +jest.mock('@app/Archives/AllTargetsArchivedRecordingsTable', () => { + return { + AllTargetsArchivedRecordingsTable: jest.fn(() => { + return ( +
+ All Targets Table +
+ ) + }), + } +}) + +jest.spyOn(defaultServices.api, 'isArchiveEnabled').mockReturnValue(of(true)); + +describe('', () => { + it('renders correctly', async () => { + let tree; + await act(async () => { + tree = renderer.create( + + + + ); + }); + expect(tree.toJSON()).toMatchSnapshot(); + }); +}); \ No newline at end of file From 9e7bc3d30ad019515ef7b66e1870754ceff369c7 Mon Sep 17 00:00:00 2001 From: Hareet Dhillon Date: Tue, 19 Jul 2022 17:08:38 -0400 Subject: [PATCH 57/69] Update snapshots due to the Patternfly packages update --- .../Recordings/ArchivedRecordingsTable.tsx | 12 +- src/test/Archives/Archives.test.tsx | 2 +- .../__snapshots__/Archives.test.tsx.snap | 178 ++++++++++++++++++ .../EventTemplates.test.tsx.snap | 1 - .../ActiveRecordingsTable.test.tsx.snap | 19 +- .../ArchivedRecordingsTable.test.tsx.snap | 19 +- .../RecordingLabelsPanel.test.tsx.snap | 6 +- .../__snapshots__/Recordings.test.tsx.snap | 15 +- .../__snapshots__/CreateRule.test.tsx.snap | 4 +- .../StoreJmxCredentials.test.tsx.snap | 16 +- 10 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 src/test/Archives/__snapshots__/Archives.test.tsx.snap diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 84c68a004..800803b9e 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -319,13 +319,11 @@ export const ArchivedRecordingsTable: React.FunctionComponent - - context.api.uploadArchivedRecordingToGrafana(props.recording.name)} - /> - + context.api.uploadArchivedRecordingToGrafana(props.recording.name)} + /> ); }, [props.recording, props.recording.metadata.labels, props.recording.name, props.index, handleCheck, checkedIndices, isExpanded, handleToggle, tableColumns, context.api]); diff --git a/src/test/Archives/Archives.test.tsx b/src/test/Archives/Archives.test.tsx index 80dd4c734..b009fd45d 100644 --- a/src/test/Archives/Archives.test.tsx +++ b/src/test/Archives/Archives.test.tsx @@ -51,7 +51,7 @@ jest.mock('@app/Recordings/ArchivedRecordingsTable', () => { ArchivedRecordingsTable: jest.fn((props) => { return (
- Archived Recordings Table + Uploads Table
) }), diff --git a/src/test/Archives/__snapshots__/Archives.test.tsx.snap b/src/test/Archives/__snapshots__/Archives.test.tsx.snap new file mode 100644 index 000000000..d334421c4 --- /dev/null +++ b/src/test/Archives/__snapshots__/Archives.test.tsx.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +
+ +
+
+
+
+
+ +
    +
  • + +
  • +
  • + +
  • +
+ +
+ + +
+
+
+
+
+`; diff --git a/src/test/Events/__snapshots__/EventTemplates.test.tsx.snap b/src/test/Events/__snapshots__/EventTemplates.test.tsx.snap index 594313d5e..6f1e903b9 100644 --- a/src/test/Events/__snapshots__/EventTemplates.test.tsx.snap +++ b/src/test/Events/__snapshots__/EventTemplates.test.tsx.snap @@ -331,7 +331,6 @@ Array [ style={ Object { "paddingRight": 0, - "width": "auto", } } > diff --git a/src/test/Recordings/__snapshots__/ActiveRecordingsTable.test.tsx.snap b/src/test/Recordings/__snapshots__/ActiveRecordingsTable.test.tsx.snap index 4d98a0cc3..6a7dced18 100644 --- a/src/test/Recordings/__snapshots__/ActiveRecordingsTable.test.tsx.snap +++ b/src/test/Recordings/__snapshots__/ActiveRecordingsTable.test.tsx.snap @@ -165,13 +165,15 @@ exports[` renders correctly 1`] = ` onMouseEnter={[Function]} scope={null} > - + renders correctly 1`] = ` style={ Object { "paddingRight": 0, - "width": "auto", } } > @@ -438,6 +439,7 @@ exports[` renders correctly 1`] = ` Automated Analysis:

renders correctly 1`] = ` diff --git a/src/test/Recordings/__snapshots__/ArchivedRecordingsTable.test.tsx.snap b/src/test/Recordings/__snapshots__/ArchivedRecordingsTable.test.tsx.snap index 06dfc30f4..f727d55ef 100644 --- a/src/test/Recordings/__snapshots__/ArchivedRecordingsTable.test.tsx.snap +++ b/src/test/Recordings/__snapshots__/ArchivedRecordingsTable.test.tsx.snap @@ -113,13 +113,15 @@ exports[` renders correctly 1`] = ` onMouseEnter={[Function]} scope={null} > - + renders correctly 1`] = ` style={ Object { "paddingRight": 0, - "width": "auto", } } > @@ -307,6 +308,7 @@ exports[` renders correctly 1`] = ` className="pf-c-table__expandable-row-content" > renders correctly 1`] = ` diff --git a/src/test/Recordings/__snapshots__/RecordingLabelsPanel.test.tsx.snap b/src/test/Recordings/__snapshots__/RecordingLabelsPanel.test.tsx.snap index d50445aa2..71598011d 100644 --- a/src/test/Recordings/__snapshots__/RecordingLabelsPanel.test.tsx.snap +++ b/src/test/Recordings/__snapshots__/RecordingLabelsPanel.test.tsx.snap @@ -13,12 +13,16 @@ exports[` renders correctly 1`] = `