From a2a8815de7452a069b06d65062578b1f8d031b0b Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:24:26 -0500 Subject: [PATCH 01/64] chore(mbeans): adjust MBeanMetrics GraphQL query for updated schema (#1219) --- .../AllTargetsArchivedRecordingsTable.tsx | 10 ++ src/app/Archives/ArchiveUploadModal.tsx | 6 +- src/app/Archives/Archives.tsx | 5 + src/app/Archives/utils.tsx | 5 + .../CreateRecording/CustomRecordingForm.tsx | 27 ++-- src/app/CreateRecording/types.ts | 5 +- .../mbean/MBeanMetricsChartController.tsx | 3 + .../Dashboard/JvmDetails/JvmDetailsCard.tsx | 2 +- src/app/RecordingMetadata/BulkEditLabels.tsx | 16 +-- src/app/RecordingMetadata/ClickableLabel.tsx | 6 +- src/app/RecordingMetadata/LabelCell.tsx | 10 +- .../RecordingLabelFields.tsx | 14 +- src/app/RecordingMetadata/types.tsx | 20 --- src/app/RecordingMetadata/utils.ts | 22 +--- src/app/Recordings/ActiveRecordingsTable.tsx | 8 +- .../Recordings/ArchivedRecordingsTable.tsx | 8 +- src/app/Recordings/Filters/LabelFilter.tsx | 4 +- .../MatchExpression/MatchExpressionHint.tsx | 6 +- .../Components/MatchExpression/utils.tsx | 2 +- src/app/Shared/Services/Api.service.tsx | 25 ++-- src/app/Shared/Services/api.types.ts | 31 +++-- src/app/Shared/Services/api.utils.ts | 4 +- src/app/Shared/Services/service.utils.ts | 9 +- src/app/TargetView/TargetContextSelector.tsx | 5 +- src/app/TargetView/TargetSelect.tsx | 5 +- src/app/Topology/Actions/CreateTarget.tsx | 11 +- src/app/Topology/Entity/EntityAnnotations.tsx | 5 +- src/app/Topology/Entity/EntityDetails.tsx | 41 +++--- src/app/Topology/Entity/types.ts | 6 + src/app/Topology/Entity/utils.tsx | 3 +- src/app/utils/fakeData.ts | 64 ++++++--- src/app/utils/utils.ts | 13 ++ src/mirage/factories.ts | 18 ++- src/mirage/index.ts | 121 ++++++++++-------- src/test/Agent/AgentLiveProbes.test.tsx | 8 +- .../AllArchivedRecordingsTable.test.tsx | 9 +- ...AllTargetsArchivedRecordingsTable.test.tsx | 6 + .../CustomRecordingForm.test.tsx | 14 +- .../SnapshotRecordingForm.test.tsx | 8 +- .../AutomatedAnalysisCard.test.tsx | 10 +- .../AutomatedAnalysisConfigForm.test.tsx | 8 +- .../Charts/jfr/JFRMetricsChartCard.test.tsx | 8 +- .../mbean/MBeanMetricsChartCard.test.tsx | 8 +- src/test/Dashboard/Dashboard.test.tsx | 5 +- src/test/Events/EventTemplates.test.tsx | 12 +- src/test/Events/EventTypes.test.tsx | 12 +- .../RecordingMetadata/BulkEditLabels.test.tsx | 18 ++- .../RecordingMetadata/ClickableLabel.test.tsx | 6 +- src/test/RecordingMetadata/LabelCell.test.tsx | 12 +- .../RecordingLabelFields.test.tsx | 18 +-- .../Recordings/ActiveRecordingsTable.test.tsx | 19 ++- .../ArchivedRecordingsTable.test.tsx | 31 ++++- .../Filters/DurationFilter.test.tsx | 9 +- .../Recordings/Filters/LabelFilter.test.tsx | 20 ++- .../Recordings/Filters/NameFilter.test.tsx | 9 +- .../Filters/RecordingStateFilter.test.tsx | 9 +- src/test/Recordings/RecordingFilters.test.tsx | 14 +- .../Recordings/RecordingLabelsPanel.test.tsx | 9 +- src/test/Recordings/Recordings.test.tsx | 5 +- src/test/Rules/CreateRule.test.tsx | 5 +- .../Credentials/StoreCredentials.test.tsx | 18 ++- src/test/TargetView/TargetSelect.test.tsx | 12 +- 62 files changed, 541 insertions(+), 321 deletions(-) delete mode 100644 src/app/RecordingMetadata/types.tsx diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 32a7a16b8..a52104a5e 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -114,6 +114,11 @@ export const AllTargetsArchivedRecordingsTable: React.FC = ({ onClose, const [uploading, setUploading] = React.useState(false); const [numOfFiles, setNumOfFiles] = React.useState(0); const [allOks, setAllOks] = React.useState(false); - const [labels, setLabels] = React.useState([] as RecordingLabel[]); + const [labels, setLabels] = React.useState([] as KeyValue[]); const [valid, setValid] = React.useState(ValidatedOptions.success); const getFormattedLabels = React.useCallback(() => { @@ -66,7 +66,7 @@ export const ArchiveUploadModal: React.FC = ({ onClose, const reset = React.useCallback(() => { setUploading(false); - setLabels([] as RecordingLabel[]); + setLabels([] as KeyValue[]); setValid(ValidatedOptions.success); setNumOfFiles(0); }, [setUploading, setLabels, setValid, setNumOfFiles]); diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index dfd9d9e23..b81e6954a 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -36,6 +36,11 @@ import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecording export const uploadAsTarget: Target = { connectUrl: UPLOADS_SUBDIRECTORY, alias: '', + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; enum ArchiveTab { diff --git a/src/app/Archives/utils.tsx b/src/app/Archives/utils.tsx index 377347f90..b69d72bf6 100644 --- a/src/app/Archives/utils.tsx +++ b/src/app/Archives/utils.tsx @@ -34,5 +34,10 @@ export const getTargetFromDirectory = (dir: RecordingDirectory): Target => { return { connectUrl: dir.connectUrl, alias: dir.jvmId, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; }; diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index 6b93cbc65..ee34aa0ea 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -17,10 +17,15 @@ import { DurationPicker } from '@app/DurationPicker/DurationPicker'; import { ErrorView } from '@app/ErrorView/ErrorView'; import { authFailMessage, isAuthFail } from '@app/ErrorView/types'; import { RecordingLabelFields } from '@app/RecordingMetadata/RecordingLabelFields'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { SelectTemplateSelectorForm } from '@app/Shared/Components/SelectTemplateSelectorForm'; import { LoadingProps } from '@app/Shared/Components/types'; -import { EventTemplate, RecordingAttributes, AdvancedRecordingOptions, Target } from '@app/Shared/Services/api.types'; +import { + EventTemplate, + RecordingAttributes, + AdvancedRecordingOptions, + Target, + KeyValue, +} from '@app/Shared/Services/api.types'; import { isTargetAgentHttp } from '@app/Shared/Services/api.utils'; import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; import { ServiceContext } from '@app/Shared/Services/Services'; @@ -150,18 +155,6 @@ export const CustomRecordingForm: React.FC = () => { return str; }, [formData]); - const getFormattedLabels = React.useCallback(() => { - const obj = {}; - - formData.labels.forEach((l) => { - if (l.key && l.value) { - obj[l.key] = l.value; - } - }); - - return obj; - }, [formData]); - const handleRecordingNameChange = React.useCallback( (name: string) => setFormData((old) => ({ @@ -198,7 +191,7 @@ export const CustomRecordingForm: React.FC = () => { ); const handleLabelsChange = React.useCallback( - (labels: RecordingLabel[]) => { + (labels: KeyValue[]) => { setFormData((old) => ({ ...old, labels })); }, [setFormData], @@ -266,10 +259,10 @@ export const CustomRecordingForm: React.FC = () => { maxAge: toDisk ? (continuous ? maxAge * maxAgeUnit : undefined) : undefined, maxSize: toDisk ? maxSize * maxSizeUnit : undefined, }, - metadata: { labels: getFormattedLabels() }, + metadata: { labels: formData.labels }, }; handleCreateRecording(recordingAttributes); - }, [eventSpecifierString, getFormattedLabels, formData, notifications, handleCreateRecording]); + }, [eventSpecifierString, formData, notifications, handleCreateRecording]); const refreshFormOptions = React.useCallback( (target: Target) => { diff --git a/src/app/CreateRecording/types.ts b/src/app/CreateRecording/types.ts index 1e5766949..e11288c17 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RecordingLabel } from '@app/RecordingMetadata/types'; -import { EventTemplate } from '@app/Shared/Services/api.types'; +import { EventTemplate, KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; export type EventTemplateIdentifier = Pick; @@ -22,7 +21,7 @@ export type EventTemplateIdentifier = Pick; interface _FormBaseData { name: string; template?: EventTemplateIdentifier; - labels: RecordingLabel[]; + labels: KeyValue[]; continuous: boolean; archiveOnStop: boolean; restart: boolean; diff --git a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx index 151a9398f..142831c4b 100644 --- a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx +++ b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx @@ -147,6 +147,9 @@ export class MBeanMetricsChartController { l += '}'; q.push(l); }); + if (q.length === 0) { + return of({}); + } return this._api.getTargetMBeanMetrics(target, q); } } diff --git a/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx b/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx index 0adc1ebc4..1bd73c01c 100644 --- a/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx +++ b/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx @@ -48,7 +48,7 @@ export const JvmDetailsCard: DashboardCardFC = (props) => { name: target.alias, target, nodeType: NodeType.JVM, - labels: {}, + labels: [], }), }; }, [target]); diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index e47f92a3f..b88a78a73 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -24,6 +24,7 @@ import { UPLOADS_SUBDIRECTORY, NotificationCategory, Target, + KeyValue, } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; @@ -33,8 +34,7 @@ import { HelpIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { combineLatest, concatMap, filter, first, forkJoin, map, Observable, of } from 'rxjs'; import { RecordingLabelFields } from './RecordingLabelFields'; -import { RecordingLabel } from './types'; -import { includesLabel, parseLabels } from './utils'; +import { includesLabel } from './utils'; export interface BulkEditLabelsProps { isTargetRecording: boolean; @@ -54,8 +54,8 @@ export const BulkEditLabels: React.FC = ({ const context = React.useContext(ServiceContext); const [recordings, setRecordings] = React.useState([] as Recording[]); const [editing, setEditing] = React.useState(false); - const [commonLabels, setCommonLabels] = React.useState([] as RecordingLabel[]); - const [savedCommonLabels, setSavedCommonLabels] = React.useState([] as RecordingLabel[]); + const [commonLabels, setCommonLabels] = React.useState([] as KeyValue[]); + const [savedCommonLabels, setSavedCommonLabels] = React.useState([] as KeyValue[]); const [valid, setValid] = React.useState(ValidatedOptions.default); const [loading, setLoading] = React.useState(false); const addSubscription = useSubscriptions(); @@ -78,7 +78,7 @@ export const BulkEditLabels: React.FC = ({ recordings.forEach((r: Recording) => { const idx = getIdxFromRecording(r); if (checkedIndices.includes(idx)) { - let updatedLabels = [...parseLabels(r.metadata.labels), ...commonLabels]; + let updatedLabels = [...r.metadata.labels, ...commonLabels]; updatedLabels = updatedLabels.filter((label) => { return !includesLabel(toDelete, label); }); @@ -124,13 +124,13 @@ export const BulkEditLabels: React.FC = ({ }, [setEditing, setCommonLabels, savedCommonLabels]); const updateCommonLabels = React.useCallback( - (setLabels: (l: RecordingLabel[]) => void) => { - const allRecordingLabels = [] as RecordingLabel[][]; + (setLabels: (l: KeyValue[]) => void) => { + const allRecordingLabels = [] as KeyValue[][]; recordings.forEach((r: Recording) => { const idx = getIdxFromRecording(r); if (checkedIndices.includes(idx)) { - allRecordingLabels.push(parseLabels(r.metadata.labels)); + allRecordingLabels.push(r.metadata.labels); } }); diff --git a/src/app/RecordingMetadata/ClickableLabel.tsx b/src/app/RecordingMetadata/ClickableLabel.tsx index f49e4210f..72113c72f 100644 --- a/src/app/RecordingMetadata/ClickableLabel.tsx +++ b/src/app/RecordingMetadata/ClickableLabel.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { Label } from '@patternfly/react-core'; import * as React from 'react'; -import { RecordingLabel } from './types'; export interface ClickableLabelCellProps { - label: RecordingLabel; + label: KeyValue; isSelected: boolean; - onLabelClick: (label: RecordingLabel) => void; + onLabelClick: (label: KeyValue) => void; } export const ClickableLabel: React.FC = ({ label, isSelected, onLabelClick }) => { diff --git a/src/app/RecordingMetadata/LabelCell.tsx b/src/app/RecordingMetadata/LabelCell.tsx index dab08476d..7e5a2a42a 100644 --- a/src/app/RecordingMetadata/LabelCell.tsx +++ b/src/app/RecordingMetadata/LabelCell.tsx @@ -15,15 +15,15 @@ */ import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { Label, Text } from '@patternfly/react-core'; import * as React from 'react'; import { ClickableLabel } from './ClickableLabel'; -import { RecordingLabel } from './types'; import { getLabelDisplay } from './utils'; export interface LabelCellProps { target: string; - labels: RecordingLabel[]; + labels: KeyValue[]; // If undefined, labels are not clickable (i.e. display only) and only displayed in grey. clickableOptions?: { labelFilters: string[]; @@ -33,7 +33,7 @@ export interface LabelCellProps { export const LabelCell: React.FC = ({ target, labels, clickableOptions }) => { const isLabelSelected = React.useCallback( - (label: RecordingLabel) => { + (label: KeyValue) => { if (clickableOptions) { const labelFilterSet = new Set(clickableOptions.labelFilters); return labelFilterSet.has(getLabelDisplay(label)); @@ -44,11 +44,11 @@ export const LabelCell: React.FC = ({ target, labels, clickableO ); const getLabelColor = React.useCallback( - (label: RecordingLabel) => (isLabelSelected(label) ? 'blue' : 'grey'), + (label: KeyValue) => (isLabelSelected(label) ? 'blue' : 'grey'), [isLabelSelected], ); const onLabelSelectToggle = React.useCallback( - (clickedLabel: RecordingLabel) => { + (clickedLabel: KeyValue) => { if (clickableOptions) { clickableOptions.updateFilters(target, { filterKey: 'Label', diff --git a/src/app/RecordingMetadata/RecordingLabelFields.tsx b/src/app/RecordingMetadata/RecordingLabelFields.tsx index f891bb238..bb241e0c5 100644 --- a/src/app/RecordingMetadata/RecordingLabelFields.tsx +++ b/src/app/RecordingMetadata/RecordingLabelFields.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ import { LoadingView } from '@app/Shared/Components/LoadingView'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { portalRoot } from '@app/utils/utils'; import { @@ -34,12 +35,11 @@ import { CloseIcon, ExclamationCircleIcon, FileIcon, PlusCircleIcon, UploadIcon import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { catchError, Observable, of, zip } from 'rxjs'; -import { RecordingLabel } from './types'; import { matchesLabelSyntax, getValidatedOption, LabelPattern, parseLabelsFromFile } from './utils'; export interface RecordingLabelFieldsProps { - labels: RecordingLabel[]; - setLabels: (labels: RecordingLabel[]) => void; + labels: KeyValue[]; + setLabels: (labels: KeyValue[]) => void; setValid: (isValid: ValidatedOptions) => void; isUploadable?: boolean; isDisabled?: boolean; @@ -78,7 +78,7 @@ export const RecordingLabelFields: React.FC = ({ ); const handleAddLabelButtonClick = React.useCallback(() => { - setLabels([...labels, { key: '', value: '' } as RecordingLabel]); + setLabels([...labels, { key: '', value: '' } as KeyValue]); }, [labels, setLabels]); const handleDeleteLabelButtonClick = React.useCallback( @@ -91,7 +91,7 @@ export const RecordingLabelFields: React.FC = ({ ); const isDuplicateKey = React.useCallback( - (key: string, labels: RecordingLabel[]) => labels.filter((label) => label.key === key).length > 1, + (key: string, labels: KeyValue[]) => labels.filter((label) => label.key === key).length > 1, [], ); @@ -127,7 +127,7 @@ export const RecordingLabelFields: React.FC = ({ (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length) { - const tasks: Observable[] = []; + const tasks: Observable[] = []; setLoading(true); for (const labelFile of Array.from(files)) { tasks.push( @@ -140,7 +140,7 @@ export const RecordingLabelFields: React.FC = ({ ); } addSubscription( - zip(tasks).subscribe((labelArrays: RecordingLabel[][]) => { + zip(tasks).subscribe((labelArrays: KeyValue[][]) => { setLoading(false); const newLabels = labelArrays.reduce((acc, next) => acc.concat(next), []); setLabels([...labels, ...newLabels]); diff --git a/src/app/RecordingMetadata/types.tsx b/src/app/RecordingMetadata/types.tsx deleted file mode 100644 index 810222ff1..000000000 --- a/src/app/RecordingMetadata/types.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright The Cryostat Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface RecordingLabel { - key: string; - value: string; -} diff --git a/src/app/RecordingMetadata/utils.ts b/src/app/RecordingMetadata/utils.ts index a5b8c3b17..d66f8cf2b 100644 --- a/src/app/RecordingMetadata/utils.ts +++ b/src/app/RecordingMetadata/utils.ts @@ -14,33 +14,25 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; import { Observable, from } from 'rxjs'; -import { RecordingLabel } from './types'; -export const parseLabels = (jsonLabels?: { [key: string]: string }) => { - if (!jsonLabels) return []; - - return Object.entries(jsonLabels).map(([k, v]) => { - return { key: k, value: v } as RecordingLabel; - }); -}; - -export const isEqualLabel = (a: RecordingLabel, b: RecordingLabel) => { +export const isEqualLabel = (a: KeyValue, b: KeyValue) => { return a.key === b.key && a.value === b.value; }; -export const includesLabel = (arr: RecordingLabel[], searchLabel: RecordingLabel) => { +export const includesLabel = (arr: KeyValue[], searchLabel: KeyValue) => { return arr.some((l) => isEqualLabel(searchLabel, l)); }; -export const parseLabelsFromFile = (file: File): Observable => { +export const parseLabelsFromFile = (file: File): Observable => { return from( file .text() .then(JSON.parse) .then((obj) => { - const labels: RecordingLabel[] = []; + const labels: KeyValue[] = []; const labelObj = obj['labels']; if (labelObj) { Object.keys(labelObj).forEach((key) => { @@ -56,7 +48,7 @@ export const parseLabelsFromFile = (file: File): Observable => ); }; -export const getLabelDisplay = (label: RecordingLabel) => `${label.key}:${label.value}`; +export const getLabelDisplay = (label: KeyValue) => `${label.key}:${label.value}`; export const LabelPattern = /^\S+$/; @@ -64,6 +56,6 @@ export const getValidatedOption = (isValid: boolean) => { return isValid ? ValidatedOptions.success : ValidatedOptions.error; }; -export const matchesLabelSyntax = (l: RecordingLabel) => { +export const matchesLabelSyntax = (l: KeyValue) => { return l && LabelPattern.test(l.key) && LabelPattern.test(l.value); }; diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index eb473920e..5a6369d47 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -20,7 +20,6 @@ import { } from '@app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel'; import { authFailMessage } from '@app/ErrorView/types'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { parseLabels } from '@app/RecordingMetadata/utils'; import { LoadingProps } from '@app/Shared/Components/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; import { emptyActiveRecordingFilters, TargetRecordingFilters } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; @@ -861,10 +860,6 @@ export const ActiveRecordingRow: React.FC = ({ return expandedRows.includes(expandedRowId); }, [expandedRowId, expandedRows]); - const parsedLabels = React.useMemo(() => { - return parseLabels(recording.metadata.labels); - }, [recording]); - const handleToggle = React.useCallback(() => toggleExpanded(expandedRowId), [expandedRowId, toggleExpanded]); const handleCheck = React.useCallback( @@ -974,7 +969,7 @@ export const ActiveRecordingRow: React.FC = ({ updateFilters: updateFilters, labelFilters: labelFilters, }} - labels={parsedLabels} + labels={recording.metadata.labels} /> = ({ recording, labelFilters, currentSelectedTargetURL, - parsedLabels, context.api, handleCheck, handleToggle, diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index c2f9a470d..3d23450f8 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -21,7 +21,6 @@ import { } from '@app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { parseLabels } from '@app/RecordingMetadata/utils'; import { LoadingProps } from '@app/Shared/Components/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; import { emptyArchivedRecordingFilters, TargetRecordingFilters } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; @@ -772,10 +771,6 @@ export const ArchivedRecordingRow: React.FC = ({ const [loadingAnalysis, setLoadingAnalysis] = React.useState(false); const [analyses, setAnalyses] = React.useState([]); - const parsedLabels = React.useMemo(() => { - return parseLabels(recording.metadata.labels); - }, [recording]); - const expandedRowId = React.useMemo(() => `archived-table-row-${index}-exp`, [index]); const handleToggle = React.useCallback(() => { @@ -849,7 +844,7 @@ export const ArchivedRecordingRow: React.FC = ({ updateFilters: updateFilters, labelFilters: labelFilters, }} - labels={parsedLabels} + labels={recording.metadata.labels} /> @@ -874,7 +869,6 @@ export const ArchivedRecordingRow: React.FC = ({ index, checkedIndices, isExpanded, - parsedLabels, labelFilters, currentSelectedTargetURL, sourceTarget, diff --git a/src/app/Recordings/Filters/LabelFilter.tsx b/src/app/Recordings/Filters/LabelFilter.tsx index 2db3c30bc..67fc3a7e2 100644 --- a/src/app/Recordings/Filters/LabelFilter.tsx +++ b/src/app/Recordings/Filters/LabelFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { parseLabels, getLabelDisplay } from '@app/RecordingMetadata/utils'; +import { getLabelDisplay } from '@app/RecordingMetadata/utils'; import { Recording } from '@app/Shared/Services/api.types'; import { Label, Select, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; @@ -42,7 +42,7 @@ export const LabelFilter: React.FC = ({ recordings, filteredLa const labels = new Set(); recordings.forEach((r) => { if (!r || !r.metadata || !r.metadata.labels) return; - parseLabels(r.metadata.labels).map((label) => labels.add(getLabelDisplay(label))); + r.metadata.labels.map((label) => labels.add(getLabelDisplay(label))); }); return Array.from(labels) .filter((l) => !filteredLabels.includes(l)) diff --git a/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx b/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx index 5efff8e5b..d219fbab2 100644 --- a/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx +++ b/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx @@ -15,6 +15,7 @@ */ import { Target } from '@app/Shared/Services/api.types'; +import { getAnnotation } from '@app/utils/utils'; import { ClipboardCopyButton, CodeBlock, CodeBlockAction, CodeBlockCode } from '@patternfly/react-core'; import * as React from 'react'; @@ -30,7 +31,10 @@ export const MatchExpressionHint: React.FC = ({ target if (!target || !target.alias || !target.connectUrl) { body = 'true'; } else { - body = `target.alias == '${target.alias}' || target.annotations.cryostat['PORT'] == ${target.annotations?.cryostat['PORT']}`; + body = `target.alias == '${target.alias}' || target.annotations.cryostat['PORT'] == ${getAnnotation( + target.annotations.cryostat, + 'PORT', + )}`; } body = JSON.stringify(body, null, 2); body = body.substring(1, body.length - 1); diff --git a/src/app/Shared/Components/MatchExpression/utils.tsx b/src/app/Shared/Components/MatchExpression/utils.tsx index 0ff572697..e1590757a 100644 --- a/src/app/Shared/Components/MatchExpression/utils.tsx +++ b/src/app/Shared/Components/MatchExpression/utils.tsx @@ -72,7 +72,7 @@ export const createTargetNode = (target: Target): TargetNode => { id: hashCode(JSON.stringify(target)), name: target.connectUrl, nodeType: NodeType.TARGET, - labels: {}, + labels: [], target: target, }; }; diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 0714a8e2f..2bded2658 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -15,7 +15,6 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { LayoutTemplate, SerialLayoutTemplate } from '@app/Dashboard/types'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { createBlobURL } from '@app/utils/utils'; import { ValidatedOptions } from '@patternfly/react-core'; import { EMPTY, forkJoin, from, Observable, ObservableInput, of, ReplaySubject, shareReplay, throwError } from 'rxjs'; @@ -59,6 +58,8 @@ import { XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, + KeyValue, + CustomTargetStub, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -170,7 +171,7 @@ export class ApiService { } createTarget( - target: Target, + target: CustomTargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -522,7 +523,7 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: RecordingLabel[]) { + transformAndStringifyToRawLabels(labels: KeyValue[]) { const rawLabels = {}; for (const label of labels) { rawLabels[label.key] = label.value; @@ -530,7 +531,7 @@ export class ApiService { return JSON.stringify(rawLabels); } - postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: RecordingLabel[]): Observable { + postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: KeyValue[]): Observable { return this.sendRequest( 'beta', `fs/recordings/${encodeURIComponent(jvmId)}/${encodeURIComponent(recordingName)}/metadata/labels`, @@ -888,7 +889,7 @@ export class ApiService { ); } - postRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.target.target().pipe( filter((target: Target) => !!target), first(), @@ -917,7 +918,7 @@ export class ApiService { ); } - postUploadedRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postUploadedRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.graphql( ` query PostUploadedRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String){ @@ -935,7 +936,7 @@ export class ApiService { ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); } - postTargetRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postTargetRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.target.target().pipe( filter((target) => !!target), first(), @@ -1206,8 +1207,10 @@ export class ApiService { ` query MBeanMXMetricsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - mbeanMetrics { - ${queries.join('\n')} + target { + mbeanMetrics { + ${queries.join('\n')} + } } } }`, @@ -1218,7 +1221,7 @@ export class ApiService { if (!nodes || nodes.length === 0) { return {}; } - return nodes[0]?.mbeanMetrics; + return nodes[0]?.target.mbeanMetrics; }), catchError((_) => of({})), ); @@ -1306,7 +1309,7 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: RecordingLabel[]): string { + stringifyRecordingLabels(labels: KeyValue[]): string { return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); } diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 86cd9f6d7..2bd3b0ba3 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -23,12 +23,13 @@ export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'beta // Common Resources // ====================================== export interface KeyValue { - readonly [key: string]: string; + key: string; + value: string; } export interface Metadata { - labels: KeyValue; - annotations?: KeyValue; + labels: KeyValue[]; + annotations?: KeyValue[]; } export interface ApiV2Response { @@ -87,6 +88,8 @@ export class XMLHttpError extends Error { } } +export type CustomTargetStub = Omit; + // ====================================== // Health Resources // ====================================== @@ -122,7 +125,7 @@ export interface AuthV2Response extends ApiV2Response { // ====================================== // MBean metric resources // ====================================== -export interface MemoryUsage { +export interface MemoryUtilization { init: number; used: number; committed: number; @@ -147,8 +150,8 @@ export interface MBeanMetrics { totalSwapSpaceSize?: number; }; memory?: { - heapMemoryUsage?: MemoryUsage; - nonHeapMemoryUsage?: MemoryUsage; + heapMemoryUsage?: MemoryUtilization; + nonHeapMemoryUsage?: MemoryUtilization; heapMemoryUsagePercent?: number; }; runtime?: { @@ -161,7 +164,7 @@ export interface MBeanMetrics { specName?: string; specVendor?: string; startTime?: number; - systemProperties?: object; + systemProperties?: KeyValue[]; uptime?: number; vmName?: string; vmVendor?: string; @@ -173,7 +176,9 @@ export interface MBeanMetrics { export interface MBeanMetricsResponse { data: { targetNodes: { - mbeanMetrics: MBeanMetrics; + target: { + mbeanMetrics: MBeanMetrics; + }; }[]; }; } @@ -444,10 +449,10 @@ export interface Target { jvmId?: string; // present in responses, but we do not need to provide it in requests connectUrl: string; alias: string; - labels?: KeyValue; - annotations?: { - cryostat: KeyValue; - platform: KeyValue; + labels: KeyValue[]; + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; }; } @@ -483,7 +488,7 @@ interface _AbstractNode { readonly id: number; readonly name: string; readonly nodeType: NodeType; - readonly labels: KeyValue; + readonly labels: KeyValue[]; } export interface EnvironmentNode extends _AbstractNode { diff --git a/src/app/Shared/Services/api.utils.ts b/src/app/Shared/Services/api.utils.ts index 5eb3caf73..af376af02 100644 --- a/src/app/Shared/Services/api.utils.ts +++ b/src/app/Shared/Services/api.utils.ts @@ -112,7 +112,7 @@ export const DEFAULT_EMPTY_UNIVERSE: EnvironmentNode = { id: 0, name: 'Universe', nodeType: NodeType.UNIVERSE, - labels: {}, + labels: [], children: [], }; @@ -140,7 +140,7 @@ export const getTargetRepresentation = (t: Target) => export const isTargetAgentHttp = (t: Target) => t.connectUrl.startsWith('http'); export const isTargetNode = (node: EnvironmentNode | TargetNode): node is TargetNode => { - return node['target'] !== undefined && node['children'] === undefined; + return node['target'] !== undefined; }; export const getAllLeaves = (root: EnvironmentNode | TargetNode): TargetNode[] => { diff --git a/src/app/Shared/Services/service.utils.ts b/src/app/Shared/Services/service.utils.ts index a43f0171d..40fa88ce1 100644 --- a/src/app/Shared/Services/service.utils.ts +++ b/src/app/Shared/Services/service.utils.ts @@ -48,9 +48,12 @@ export const automatedAnalysisConfigToRecordingAttributes = ( maxSize: config.maxSize, }, metadata: { - labels: { - origin: automatedAnalysisRecordingName, - }, + labels: [ + { + key: 'origin', + value: automatedAnalysisRecordingName, + }, + ], }, }; }; diff --git a/src/app/TargetView/TargetContextSelector.tsx b/src/app/TargetView/TargetContextSelector.tsx index 4ab01089c..b27ac69a6 100644 --- a/src/app/TargetView/TargetContextSelector.tsx +++ b/src/app/TargetView/TargetContextSelector.tsx @@ -19,6 +19,7 @@ import { isEqualTarget, getTargetRepresentation } from '@app/Shared/Services/api import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getFromLocalStorage, removeFromLocalStorage, saveToLocalStorage } from '@app/utils/LocalStorage'; +import { getAnnotation } from '@app/utils/utils'; import { Button, Divider, Select, SelectGroup, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; import { Link } from 'react-router-dom'; @@ -113,13 +114,13 @@ export const TargetContextSelector: React.FC = ({ cl const favSet = new Set(favorites); const groupNames = new Set(); - targets.forEach((t) => groupNames.add(t.annotations?.cryostat['REALM'] || 'Others')); + targets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others')); const options = Array.from(groupNames) .map((name) => ( {targets - .filter((t) => (t.annotations?.cryostat['REALM'] || 'Others') === name) + .filter((t) => getAnnotation(t.annotations.cryostat, 'REALM') === name) .map((t: Target) => ( = ({ onSelect, simple, .. let options = [] as JSX.Element[]; const groupNames = new Set(); - targets.forEach((t) => groupNames.add(t.annotations?.cryostat['REALM'] || 'Others')); + targets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others')); options = options.concat( Array.from(groupNames) .map((name) => ( {targets - .filter((t) => (t.annotations?.cryostat['REALM'] || 'Others') === name) + .filter((t) => (getAnnotation(t.annotations.cryostat, 'REALM') || 'Others') === name) .map((t: Target) => ( {!t.alias || t.alias === t.connectUrl ? `${t.connectUrl}` : `${t.alias} (${t.connectUrl})`} diff --git a/src/app/Topology/Actions/CreateTarget.tsx b/src/app/Topology/Actions/CreateTarget.tsx index e4e8645e0..cc62f41fd 100644 --- a/src/app/Topology/Actions/CreateTarget.tsx +++ b/src/app/Topology/Actions/CreateTarget.tsx @@ -25,7 +25,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import '@app/Topology/styles/base.css'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getFromLocalStorage } from '@app/utils/LocalStorage'; -import { portalRoot } from '@app/utils/utils'; +import { getAnnotation, portalRoot } from '@app/utils/utils'; import { Accordion, AccordionContent, @@ -241,7 +241,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { React.useEffect(() => { addSubscription( context.targets.targets().subscribe((ts) => { - const discoveredTargets = ts.filter((t) => t.annotations?.cryostat['REALM'] !== 'Custom Targets'); + const discoveredTargets = ts.filter((t) => getAnnotation(t.annotations.cryostat, 'REALM') !== 'Custom Targets'); if (discoveredTargets.length) { setExample(discoveredTargets[0].connectUrl); } @@ -400,7 +400,12 @@ export const CreateTarget: React.FC = ({ prefilled }) => { - + diff --git a/src/app/Topology/Entity/EntityAnnotations.tsx b/src/app/Topology/Entity/EntityAnnotations.tsx index 916762317..416804732 100644 --- a/src/app/Topology/Entity/EntityAnnotations.tsx +++ b/src/app/Topology/Entity/EntityAnnotations.tsx @@ -16,8 +16,9 @@ import { Label, LabelGroup } from '@patternfly/react-core'; import * as React from 'react'; import { EmptyText } from '../../Shared/Components/EmptyText'; +import { Annotations } from './types'; -export const EntityAnnotations: React.FC<{ annotations?: object; maxDisplay?: number }> = ({ +export const EntityAnnotations: React.FC<{ annotations?: Annotations; maxDisplay?: number }> = ({ annotations, maxDisplay, ...props @@ -26,7 +27,7 @@ export const EntityAnnotations: React.FC<{ annotations?: object; maxDisplay?: nu return annotations ? Object.keys(annotations).map((groupK) => ({ groupLabel: groupK, - annotations: Object.keys(annotations[groupK]).map((k) => `${k}=${annotations[groupK][k]}`), + annotations: annotations[groupK].map((kv) => `${kv.key}=${kv.value}`), })) : []; }, [annotations]); diff --git a/src/app/Topology/Entity/EntityDetails.tsx b/src/app/Topology/Entity/EntityDetails.tsx index 8fd419964..01814c76e 100644 --- a/src/app/Topology/Entity/EntityDetails.tsx +++ b/src/app/Topology/Entity/EntityDetails.tsx @@ -271,23 +271,28 @@ const MBeanDetails: React.FC<{ ` query MBeanMXMetricsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - mbeanMetrics { - runtime { - startTime - vmVendor - vmVersion - classPath - libraryPath - inputArguments - systemProperties - } - os { - name - version - arch - availableProcessors - totalPhysicalMemorySize - totalSwapSpaceSize + target { + mbeanMetrics { + runtime { + startTime + vmVendor + vmVersion + classPath + libraryPath + inputArguments + systemProperties { + key + value + } + } + os { + name + version + arch + availableProcessors + totalPhysicalMemorySize + totalSwapSpaceSize + } } } } @@ -295,7 +300,7 @@ const MBeanDetails: React.FC<{ { connectUrl }, ) .pipe( - map((resp) => resp.data.targetNodes[0].mbeanMetrics || {}), + map((resp) => resp.data.targetNodes[0].target.mbeanMetrics || {}), catchError((_) => of({})), ) .subscribe(setMbeanMetrics), diff --git a/src/app/Topology/Entity/types.ts b/src/app/Topology/Entity/types.ts index 5dc1cea00..631c06436 100644 --- a/src/app/Topology/Entity/types.ts +++ b/src/app/Topology/Entity/types.ts @@ -17,6 +17,7 @@ import type { EventProbe, EventTemplate, EventType, + KeyValue, NotificationMessage, Recording, Rule, @@ -48,6 +49,11 @@ export const TargetOwnedResourceTypeAsArray = [ 'agentProbes', ] as const; +export type Annotations = { + cryostat: KeyValue[]; + platform: KeyValue[]; +}; + export const TargetRelatedResourceTypeAsArray = ['automatedRules', 'credentials'] as const; export type TargetOwnedResourceType = (typeof TargetOwnedResourceTypeAsArray)[number]; diff --git a/src/app/Topology/Entity/utils.tsx b/src/app/Topology/Entity/utils.tsx index 4df3a52ff..9e25102a1 100644 --- a/src/app/Topology/Entity/utils.tsx +++ b/src/app/Topology/Entity/utils.tsx @@ -53,7 +53,8 @@ import { import { ActiveRecDetail, Nothing } from './ResourceDetails'; import { DescriptionConfig, TargetOwnedResourceType, TargetRelatedResourceType, ResourceTypes, PatchFn } from './types'; -export const keyValueEntryTransformer = (kv: object): string[] => Object.entries(kv).map(([k, v]) => `${k}=${v}`); +export const keyValueEntryTransformer = (kv: { key: string; value: string }[]): string[] => + kv.map((k) => `${k.key}=${k.value}`); export const valuesEntryTransformer: (kv: string[] | object) => string[] = Object.values; diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index f1d5544fd..0075d0fa0 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -49,19 +49,40 @@ export const fakeTarget: Target = { jvmId: 'rpZeYNB9wM_TEnXoJvAFuR0jdcUBXZgvkXiKhjQGFvY=', connectUrl: 'service:jmx:rmi:///jndi/rmi://10-128-2-25.my-namespace.pod:9097/jmxrmi', alias: 'quarkus-test-77f556586c-25bkv', - labels: { - 'pod-template-hash': '77f556586c', - deployment: 'quarkus-test', - }, - annotations: { - cryostat: { - HOST: '10.128.2.25', - PORT: '9097', - POD_NAME: 'quarkus-test-77f556586c-25bkv', - REALM: 'KubernetesApi', - NAMESPACE: 'my-namespace', + labels: [ + { + key: 'pod-template-hash', + value: '77f556586c', }, - platform: {}, + { + key: 'deployment', + value: 'quarkus-test', + }, + ], + annotations: { + cryostat: [ + { + key: 'HOST', + value: '10.128.2.25', + }, + { + key: 'PORT', + value: '9097', + }, + { + key: 'POD_NAME', + value: 'quarkus-test-77f556586c-25bkv', + }, + { + key: 'REALM', + value: 'KubernetesApi', + }, + { + key: 'NAMESPACE', + value: 'my-namespace', + }, + ], + platform: [], }, }; @@ -72,11 +93,20 @@ export const fakeAARecording: ActiveRecording = { reportUrl: 'https://clustercryostat-sample-default.apps.ci-ln-25fg5f2-76ef8.origin-ci-int-aws.dev.rhcloud.com:443/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2F10-128-2-27.my-namespace.pod:9097%2Fjmxrmi/reports/automated-analysis', metadata: { - labels: { - 'template.name': 'Profiling', - 'template.type': 'TARGET', - origin: 'automated-analysis', - }, + labels: [ + { + key: 'template.name', + value: 'Profiling', + }, + { + key: 'template.type', + value: 'TARGET', + }, + { + key: 'origin', + value: 'automated-analysis', + }, + ], }, startTime: 1680732807, id: 0, diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index a684c48fe..dd80a8811 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { ISortBy, SortByDirection } from '@patternfly/react-table'; import _ from 'lodash'; import { NavigateFunction } from 'react-router-dom'; @@ -181,6 +182,18 @@ export interface TableColumn { width?: number; } +export const getAnnotation = (kv: KeyValue[], key: string, def?: string): string | undefined => { + if (!kv) { + return def; + } + for (const k of kv) { + if (k.key === key) { + return k.value; + } + } + return def; +}; + const mapper = (tableColumns: TableColumn[], index?: number) => { if (index === undefined) { return undefined; diff --git a/src/mirage/factories.ts b/src/mirage/factories.ts index 687ab86a5..939791695 100644 --- a/src/mirage/factories.ts +++ b/src/mirage/factories.ts @@ -22,8 +22,22 @@ export const targetFactory: FactoryDefinition = Factory.extend({ connectUrl: 'http://fake-target.local:1234', jvmId: '1234', annotations: { - platform: { 'io.cryostat.demo': 'this-is-not-real' }, - cryostat: { hello: 'world', REALM: 'Some Realm' }, + platform: [ + { + key: 'io.cryostat.demo', + value: 'this-is-not-real', + }, + ], + cryostat: [ + { + key: 'hello', + value: 'world', + }, + { + key: 'REALM', + value: 'Some Realm', + }, + ], }, }); diff --git a/src/mirage/index.ts b/src/mirage/index.ts index 5de3574e1..d42a8b0f1 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -101,10 +101,13 @@ export const startMirage = ({ environment = 'development' } = {}) => { alias: attrs.get('alias'), connectUrl: attrs.get('connectUrl'), annotations: { - platform: {}, - cryostat: { - REALM: 'Custom Targets', - }, + platform: [], + cryostat: [ + { + key: 'REALM', + value: 'Custom Targets', + }, + ], }, }); websocket.send( @@ -135,11 +138,11 @@ export const startMirage = ({ environment = 'development' } = {}) => { result: { name: 'Universe', nodeType: 'Universe', - labels: {}, + labels: [], children: realmTypes.map((r: string) => ({ name: r, nodeType: 'Realm', - labels: {}, + labels: [], id: r, children: models .filter((t) => t.annotations.cryostat['REALM'] === r) @@ -211,11 +214,17 @@ export const startMirage = ({ environment = 'development' } = {}) => { maxSize: attrs.get('maxSize') || 0, maxAge: attrs.get('maxAge') || 0, metadata: { - labels: { - ...(attrs.labels || {}), - 'template.type': 'TARGET', - 'template.name': 'Demo_Template', - }, + labels: [ + ...(attrs.labels || []), + { + key: 'template.type', + value: 'TARGET', + }, + { + key: 'template.name', + value: 'Demo_Template', + }, + ], }, }); websocket.send( @@ -630,52 +639,54 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - mbeanMetrics: { - thread: { - threadCount: Math.ceil(Math.random() * 5), - daemonThreadCount: Math.ceil(Math.random() * 5), - }, - os: { - arch: 'x86_64', - availableProcessors: Math.ceil(Math.random() * 8), - version: '10.0.1', - systemCpuLoad: Math.random(), - systemLoadAverage: Math.random(), - processCpuLoad: Math.random(), - totalPhysicalMemorySize: Math.ceil(Math.random() * 64), - freePhysicalMemorySize: Math.ceil(Math.random() * 64), - }, - memory: { - heapMemoryUsage: { - init: Math.ceil(Math.random() * 64), - used: Math.ceil(Math.random() * 64), - committed: Math.ceil(Math.random() * 64), - max: Math.ceil(Math.random() * 64), + target: { + mbeanMetrics: { + thread: { + threadCount: Math.ceil(Math.random() * 5), + daemonThreadCount: Math.ceil(Math.random() * 5), }, - nonHeapMemoryUsage: { - init: Math.ceil(Math.random() * 64), - used: Math.ceil(Math.random() * 64), - committed: Math.ceil(Math.random() * 64), - max: Math.ceil(Math.random() * 64), + os: { + arch: 'x86_64', + availableProcessors: Math.ceil(Math.random() * 8), + version: '10.0.1', + systemCpuLoad: Math.random(), + systemLoadAverage: Math.random(), + processCpuLoad: Math.random(), + totalPhysicalMemorySize: Math.ceil(Math.random() * 64), + freePhysicalMemorySize: Math.ceil(Math.random() * 64), + }, + memory: { + heapMemoryUsage: { + init: Math.ceil(Math.random() * 64), + used: Math.ceil(Math.random() * 64), + committed: Math.ceil(Math.random() * 64), + max: Math.ceil(Math.random() * 64), + }, + nonHeapMemoryUsage: { + init: Math.ceil(Math.random() * 64), + used: Math.ceil(Math.random() * 64), + committed: Math.ceil(Math.random() * 64), + max: Math.ceil(Math.random() * 64), + }, + heapMemoryUsagePercent: Math.random(), + }, + runtime: { + bootClassPath: '/path/to/boot/classpath', + classPath: '/path/to/classpath', + inputArguments: ['-Xmx1g', '-Djava.security.policy=...'], + libraryPath: '/path/to/library/path', + managementSpecVersion: '1.0', + name: 'Java Virtual Machine', + specName: 'Java Virtual Machine Specification', + specVendor: 'Oracle Corporation', + startTime: Date.now(), + // systemProperties: {...} + uptime: Date.now(), + vmName: 'Java HotSpot(TM) 64-Bit Server VM', + vmVendor: 'Oracle Corporation', + vmVersion: '25.131-b11', + bootClassPathSupported: true, }, - heapMemoryUsagePercent: Math.random(), - }, - runtime: { - bootClassPath: '/path/to/boot/classpath', - classPath: '/path/to/classpath', - inputArguments: ['-Xmx1g', '-Djava.security.policy=...'], - libraryPath: '/path/to/library/path', - managementSpecVersion: '1.0', - name: 'Java Virtual Machine', - specName: 'Java Virtual Machine Specification', - specVendor: 'Oracle Corporation', - startTime: Date.now(), - // systemProperties: {...} - uptime: Date.now(), - vmName: 'Java HotSpot(TM) 64-Bit Server VM', - vmVendor: 'Oracle Corporation', - vmVersion: '25.131-b11', - bootClassPathSupported: true, }, }, }, diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index eb4f9197c..b366d05f5 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -30,7 +30,13 @@ import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; diff --git a/src/test/Archives/AllArchivedRecordingsTable.test.tsx b/src/test/Archives/AllArchivedRecordingsTable.test.tsx index 5acbb2939..d6ddc0d33 100644 --- a/src/test/Archives/AllArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllArchivedRecordingsTable.test.tsx @@ -43,9 +43,12 @@ const mockRecordingDeletedNotification = { }, } as NotificationMessage; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ArchivedRecording = { name: 'someRecording', diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 1c12b36da..6af7d3070 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -26,6 +26,9 @@ const mockAlias1 = 'fooTarget1'; const mockTarget1: Target = { connectUrl: mockConnectUrl1, alias: mockAlias1, + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockConnectUrl2 = 'service:jmx:rmi://someUrl2'; const mockAlias2 = 'fooTarget2'; @@ -36,6 +39,9 @@ const mockNewAlias = 'newTarget'; const mockNewTarget: Target = { connectUrl: mockNewConnectUrl, alias: mockNewAlias, + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockCount1 = 1; const mockCount2 = 3; diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bb28193ba..bdc185501 100644 --- a/src/test/CreateRecording/CustomRecordingForm.test.tsx +++ b/src/test/CreateRecording/CustomRecordingForm.test.tsx @@ -15,7 +15,7 @@ */ import { CustomRecordingForm } from '@app/CreateRecording/CustomRecordingForm'; import { authFailMessage } from '@app/ErrorView/types'; -import { EventTemplate, AdvancedRecordingOptions, RecordingAttributes } from '@app/Shared/Services/api.types'; +import { EventTemplate, AdvancedRecordingOptions, RecordingAttributes, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, Services, defaultServices } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import { screen, cleanup, act as doAct } from '@testing-library/react'; @@ -36,7 +36,13 @@ jest.mock('react-router-dom', () => ({ })); const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockCustomEventTemplate: EventTemplate = { name: 'someEventTemplate', @@ -118,7 +124,7 @@ describe('', () => { maxSize: 0, toDisk: true, }, - metadata: { labels: {} }, + metadata: { labels: [] }, } as RecordingAttributes); expect(mockNavigate).toHaveBeenCalledWith('..', { relative: 'path' }); }); @@ -150,7 +156,7 @@ describe('', () => { it('should show error view if failing to retrieve templates or recording options', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/CreateRecording/SnapshotRecordingForm.test.tsx b/src/test/CreateRecording/SnapshotRecordingForm.test.tsx index 8b8914bfc..1c8b6cf4f 100644 --- a/src/test/CreateRecording/SnapshotRecordingForm.test.tsx +++ b/src/test/CreateRecording/SnapshotRecordingForm.test.tsx @@ -23,7 +23,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index e9dcd0c10..8f6d9a313 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -40,7 +40,13 @@ jest.mock('@app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList', () => { }; }); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockEmptyCachedReport: CachedReportValue = { report: [], @@ -136,7 +142,7 @@ const mockArchivedRecording: ArchivedRecording = { name: 'someArchivedRecording', downloadUrl: '', reportUrl: '', - metadata: { labels: {} }, + metadata: { labels: [] }, size: 0, archivedTime: 1663027200000, // 2022-09-13T00:00:00.000Z in milliseconds }; diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx index 76f56848c..fcfed6b1f 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx @@ -22,7 +22,13 @@ import { cleanup, screen } from '@testing-library/react'; import { of } from 'rxjs'; import { render, testT } from '../../utils'; -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockTemplate1: EventTemplate = { name: 'template1', diff --git a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx index 5dec865e2..e0b8ef384 100644 --- a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx +++ b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx @@ -29,7 +29,13 @@ import { mockMediaQueryList, render, renderSnapshot } from '../../../utils'; const mockDashboardUrl = 'http://localhost:3000'; jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of(mockDashboardUrl)); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.settings, 'themeSetting').mockReturnValue(of(ThemeSetting.LIGHT)); diff --git a/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx b/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx index 8a8acf9c7..fe55eb5f1 100644 --- a/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx +++ b/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx @@ -34,7 +34,13 @@ jest.spyOn(defaultServices.settings, 'datetimeFormat').mockReturnValue(of(defaul jest.spyOn(defaultServices.settings, 'themeSetting').mockReturnValue(of(ThemeSetting.DARK)); jest.spyOn(defaultServices.settings, 'media').mockReturnValue(of(mockMediaQueryList)); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); const mockJfrController = new JFRMetricsChartController( diff --git a/src/test/Dashboard/Dashboard.test.tsx b/src/test/Dashboard/Dashboard.test.tsx index 6ee344f75..15dd83c2a 100644 --- a/src/test/Dashboard/Dashboard.test.tsx +++ b/src/test/Dashboard/Dashboard.test.tsx @@ -30,9 +30,10 @@ const mockFooConnectUrl = 'service:jmx:rmi://someFooUrl'; const mockFooTarget: Target = { connectUrl: mockFooConnectUrl, alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; diff --git a/src/test/Events/EventTemplates.test.tsx b/src/test/Events/EventTemplates.test.tsx index 736f28e8b..915705da6 100644 --- a/src/test/Events/EventTemplates.test.tsx +++ b/src/test/Events/EventTemplates.test.tsx @@ -16,7 +16,7 @@ import { authFailMessage } from '@app/ErrorView/types'; import { EventTemplates } from '@app/Events/EventTemplates'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { MessageType, EventTemplate, MessageMeta, NotificationMessage } from '@app/Shared/Services/api.types'; +import { MessageType, EventTemplate, MessageMeta, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, defaultServices, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import '@testing-library/jest-dom'; @@ -25,7 +25,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; @@ -255,7 +261,7 @@ describe('', () => { it('should show error view if failing to retrieve event templates', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/Events/EventTypes.test.tsx b/src/test/Events/EventTypes.test.tsx index df8cd0518..8016f6709 100644 --- a/src/test/Events/EventTypes.test.tsx +++ b/src/test/Events/EventTypes.test.tsx @@ -15,7 +15,7 @@ */ import { authFailMessage } from '@app/ErrorView/types'; import { EventTypes } from '@app/Events/EventTypes'; -import { EventType } from '@app/Shared/Services/api.types'; +import { EventType, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, defaultServices, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import { act as doAct, cleanup, screen } from '@testing-library/react'; @@ -24,7 +24,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockEventType: EventType = { name: 'Some Event', @@ -58,7 +64,7 @@ describe('', () => { it('should show error view if failing to retrieve event types', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index 7354520c1..69d73eb08 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -19,6 +19,7 @@ import { ActiveRecording, RecordingState, NotificationMessage, + Target, } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; @@ -33,12 +34,21 @@ jest.mock('@patternfly/react-core', () => ({ const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; - -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget: Target = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; + const mockArchivedRecording: ArchivedRecording = { name: 'someArchivedRecording_some_random', downloadUrl: 'http://downloadUrl', diff --git a/src/test/RecordingMetadata/ClickableLabel.test.tsx b/src/test/RecordingMetadata/ClickableLabel.test.tsx index e38cb4f12..beb9c64a6 100644 --- a/src/test/RecordingMetadata/ClickableLabel.test.tsx +++ b/src/test/RecordingMetadata/ClickableLabel.test.tsx @@ -14,18 +14,18 @@ * limitations under the License. */ import { ClickableLabel } from '@app/RecordingMetadata/ClickableLabel'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import '@testing-library/jest-dom'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { cleanup, screen } from '@testing-library/react'; import { render, renderSnapshot } from '../utils'; const mockLabel = { key: 'someLabel', value: 'someValue', -} as RecordingLabel; +} as KeyValue; const mockLabelAsString = 'someLabel: someValue'; -const onLabelClick = jest.fn((_label: RecordingLabel) => { +const onLabelClick = jest.fn((_label: KeyValue) => { /**Do nothing. Used for checking renders */ }); diff --git a/src/test/RecordingMetadata/LabelCell.test.tsx b/src/test/RecordingMetadata/LabelCell.test.tsx index 572772be9..67fb6f9fa 100644 --- a/src/test/RecordingMetadata/LabelCell.test.tsx +++ b/src/test/RecordingMetadata/LabelCell.test.tsx @@ -15,9 +15,8 @@ */ import { LabelCell } from '@app/RecordingMetadata/LabelCell'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; -import { Target } from '@app/Shared/Services/api.types'; +import { KeyValue, Target } from '@app/Shared/Services/api.types'; import '@testing-library/jest-dom'; import { cleanup, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -26,14 +25,15 @@ import { render, renderSnapshot } from '../utils'; const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; -const mockLabel = { key: 'someLabel', value: 'someValue' } as RecordingLabel; -const mockAnotherLabel = { key: 'anotherLabel', value: 'anotherValue' } as RecordingLabel; +const mockLabel = { key: 'someLabel', value: 'someValue' } as KeyValue; +const mockAnotherLabel = { key: 'anotherLabel', value: 'anotherValue' } as KeyValue; const mockLabelList = [mockLabel, mockAnotherLabel]; // For display diff --git a/src/test/RecordingMetadata/RecordingLabelFields.test.tsx b/src/test/RecordingMetadata/RecordingLabelFields.test.tsx index 9449dfa60..13a48d7e3 100644 --- a/src/test/RecordingMetadata/RecordingLabelFields.test.tsx +++ b/src/test/RecordingMetadata/RecordingLabelFields.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { RecordingLabelFields, RecordingLabelFieldsProps } from '@app/RecordingMetadata/RecordingLabelFields'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; import '@testing-library/jest-dom'; import * as tlr from '@testing-library/react'; @@ -37,12 +37,12 @@ mockMetadataFile.text = jest.fn( describe('', () => { // RecordingLabelFields component modifies labels in-place, so we need to reinitialize mocks // after every tests. - let mockLabels: RecordingLabel[]; + let mockLabels: KeyValue[]; let mockValid: ValidatedOptions; let mockProps: RecordingLabelFieldsProps; - let mockLabel1: RecordingLabel; - let mockLabel2: RecordingLabel; - let mockEmptyLabel: RecordingLabel; + let mockLabel1: KeyValue; + let mockLabel2: KeyValue; + let mockEmptyLabel: KeyValue; afterEach(cleanup); @@ -50,20 +50,20 @@ describe('', () => { mockLabel1 = { key: 'someLabel', value: 'someValue', - } as RecordingLabel; + } as KeyValue; mockLabel2 = { key: 'anotherLabel', value: 'anotherValue', - } as RecordingLabel; + } as KeyValue; mockEmptyLabel = { key: '', value: '', - } as RecordingLabel; + } as KeyValue; mockLabels = [mockLabel1, mockLabel2]; mockValid = ValidatedOptions.default; mockProps = { labels: mockLabels, - setLabels: jest.fn((labels: RecordingLabel[]) => (mockLabels = labels.slice())), + setLabels: jest.fn((labels: KeyValue[]) => (mockLabels = labels.slice())), setValid: jest.fn((state: ValidatedOptions) => (mockValid = state)), }; }); diff --git a/src/test/Recordings/ActiveRecordingsTable.test.tsx b/src/test/Recordings/ActiveRecordingsTable.test.tsx index 38e98fb0c..865cefc71 100644 --- a/src/test/Recordings/ActiveRecordingsTable.test.tsx +++ b/src/test/Recordings/ActiveRecordingsTable.test.tsx @@ -23,7 +23,7 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { ActiveRecording, RecordingState, NotificationMessage } from '@app/Shared/Services/api.types'; +import { ActiveRecording, RecordingState, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { defaultServices, ServiceContext, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import dayjs, { defaultDatetimeFormat } from '@i18n/datetime'; @@ -33,10 +33,19 @@ import { basePreloadedState, DEFAULT_DIMENSIONS, render, resize } from '../utils const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', @@ -527,7 +536,7 @@ describe('', () => { it('should show error view if failing to retrieve recordings', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 4a492c6ab..b8d028bf9 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -21,7 +21,7 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage } from '@app/Shared/Services/api.types'; +import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import { Text } from '@patternfly/react-core'; import '@testing-library/jest-dom'; @@ -32,14 +32,31 @@ import { basePreloadedState, DEFAULT_DIMENSIONS, render, resize } from '../utils const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; -const mockUploadsTarget = { connectUrl: UPLOADS_SUBDIRECTORY, alias: '' }; -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget: Target = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; -const mockUploadedRecordingLabels = { - someUploaded: 'someUploadedValue', +const mockUploadsTarget = { + connectUrl: UPLOADS_SUBDIRECTORY, + alias: '', + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; +const mockUploadedRecordingLabels = [ + { + key: 'someUploaded', + value: 'someUpdatedValue', + }, +]; const mockMetadataFileName = 'mock.metadata.json'; const mockMetadataFile = new File( [JSON.stringify({ labels: { ...mockUploadedRecordingLabels } })], diff --git a/src/test/Recordings/Filters/DurationFilter.test.tsx b/src/test/Recordings/Filters/DurationFilter.test.tsx index a6eb14b2b..a9ab62117 100644 --- a/src/test/Recordings/Filters/DurationFilter.test.tsx +++ b/src/test/Recordings/Filters/DurationFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/Filters/LabelFilter.test.tsx b/src/test/Recordings/Filters/LabelFilter.test.tsx index 1bcb48391..117a0960f 100644 --- a/src/test/Recordings/Filters/LabelFilter.test.tsx +++ b/src/test/Recordings/Filters/LabelFilter.test.tsx @@ -19,12 +19,18 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; -const mockAnotherRecordingLabels = { - anotherLabel: 'anotherValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; +const mockAnotherRecordingLabels = [ + { + key: 'anotherLabel', + value: 'anotherValue', + }, +]; const mockRecordingLabelList = ['someLabel:someValue', 'anotherLabel:anotherValue']; const mockRecording: ActiveRecording = { @@ -49,7 +55,7 @@ const mockAnotherRecording = { const mockRecordingWithoutLabel = { ...mockRecording, name: 'noLabelRecording', - metadata: { labels: {} }, + metadata: { labels: [] }, } as ActiveRecording; const mockRecordingList = [mockRecording, mockAnotherRecording, mockRecordingWithoutLabel]; diff --git a/src/test/Recordings/Filters/NameFilter.test.tsx b/src/test/Recordings/Filters/NameFilter.test.tsx index b32477b31..2d04b37b5 100644 --- a/src/test/Recordings/Filters/NameFilter.test.tsx +++ b/src/test/Recordings/Filters/NameFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/Filters/RecordingStateFilter.test.tsx b/src/test/Recordings/Filters/RecordingStateFilter.test.tsx index bf94a3aea..d576b621c 100644 --- a/src/test/Recordings/Filters/RecordingStateFilter.test.tsx +++ b/src/test/Recordings/Filters/RecordingStateFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/RecordingFilters.test.tsx b/src/test/Recordings/RecordingFilters.test.tsx index 547d932bc..2d4370602 100644 --- a/src/test/Recordings/RecordingFilters.test.tsx +++ b/src/test/Recordings/RecordingFilters.test.tsx @@ -37,15 +37,19 @@ import { basePreloadedState, render } from '../utils'; const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockActiveRecording: ActiveRecording = { name: 'someRecording', diff --git a/src/test/Recordings/RecordingLabelsPanel.test.tsx b/src/test/Recordings/RecordingLabelsPanel.test.tsx index 8b845e2fc..3478435aa 100644 --- a/src/test/Recordings/RecordingLabelsPanel.test.tsx +++ b/src/test/Recordings/RecordingLabelsPanel.test.tsx @@ -33,9 +33,12 @@ jest.mock('@app/RecordingMetadata/BulkEditLabels', () => { }; }); -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ArchivedRecording = { name: 'someRecording', diff --git a/src/test/Recordings/Recordings.test.tsx b/src/test/Recordings/Recordings.test.tsx index bdf062a7f..4c21e6b8c 100644 --- a/src/test/Recordings/Recordings.test.tsx +++ b/src/test/Recordings/Recordings.test.tsx @@ -53,9 +53,10 @@ jest.mock('@app/TargetView/TargetView', () => { const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; diff --git a/src/test/Rules/CreateRule.test.tsx b/src/test/Rules/CreateRule.test.tsx index 634a215e4..5bb566850 100644 --- a/src/test/Rules/CreateRule.test.tsx +++ b/src/test/Rules/CreateRule.test.tsx @@ -30,9 +30,10 @@ const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockTarget: Target = { connectUrl: mockConnectUrl, alias: 'io.cryostat.Cryostat', + labels: [], annotations: { - cryostat: { PORT: '9091' }, - platform: {}, + cryostat: [{ key: 'PORT', value: '9091' }], + platform: [], }, }; const mockEventTemplate: EventTemplate = { diff --git a/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx b/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx index 741451f8a..ce7bfe824 100644 --- a/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx +++ b/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx @@ -35,15 +35,29 @@ const mockAnotherCredential: StoredCredential = { numMatchingTargets: 2, }; -const mockTarget: Target = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'someAlias' }; -const mockAnotherTarget: Target = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherAlias' }; +const mockTarget: Target = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'someAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; +const mockAnotherTarget: Target = { + connectUrl: 'service:jmx:rmi://anotherUrl', + alias: 'anotherAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockAnotherMatchingTarget: Target = { connectUrl: 'service:jmx:rmi://anotherMatchUrl', alias: 'anotherMatchAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockYetAnotherMatchingTarget: Target = { connectUrl: 'service:jmx:rmi://yetAnotherMatchUrl', alias: 'yetAnotherMatchAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockMatchedCredentialResponse: MatchedCredential = { diff --git a/src/test/TargetView/TargetSelect.test.tsx b/src/test/TargetView/TargetSelect.test.tsx index c31a1b4d7..f30c19ab0 100644 --- a/src/test/TargetView/TargetSelect.test.tsx +++ b/src/test/TargetView/TargetSelect.test.tsx @@ -26,16 +26,20 @@ const mockBarConnectUrl = 'service:jmx:rmi://someBarUrl'; const CUSTOM_TARGET_REALM = 'Custom Targets'; -const cryostatAnnotation = { - REALM: CUSTOM_TARGET_REALM, -}; +const cryostatAnnotation = [ + { + key: 'REALM', + value: CUSTOM_TARGET_REALM, + }, +]; const mockFooTarget: Target = { jvmId: 'abcd', connectUrl: mockFooConnectUrl, alias: 'fooTarget', + labels: [], annotations: { cryostat: cryostatAnnotation, - platform: {}, + platform: [], }, }; const mockBarTarget: Target = { ...mockFooTarget, jvmId: 'efgh', connectUrl: mockBarConnectUrl, alias: 'barTarget' }; From c6eacbe10b836e75ba40600f3e446d446dd9375c Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:48:23 -0500 Subject: [PATCH 02/64] correct tests --- .../RecordingMetadata/BulkEditLabels.test.tsx | 14 +++++++-- .../Recordings/ActiveRecordingsTable.test.tsx | 6 ++-- .../ArchivedRecordingsTable.test.tsx | 29 ++++++++++++++----- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index 69d73eb08..cec202ce1 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -78,7 +78,12 @@ const mockActiveLabelsNotification = { target: mockConnectUrl, recordingName: 'someActiveRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, }, } as NotificationMessage; @@ -89,7 +94,12 @@ const mockArchivedLabelsNotification = { target: mockConnectUrl, recordingName: 'someArchivedRecording_some_random', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, }, } as NotificationMessage; diff --git a/src/test/Recordings/ActiveRecordingsTable.test.tsx b/src/test/Recordings/ActiveRecordingsTable.test.tsx index 865cefc71..1b01ac846 100644 --- a/src/test/Recordings/ActiveRecordingsTable.test.tsx +++ b/src/test/Recordings/ActiveRecordingsTable.test.tsx @@ -69,7 +69,7 @@ const mockLabelsNotification = { target: mockConnectUrl, recordingName: 'someRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, }, } as NotificationMessage; const mockStopNotification = { @@ -242,8 +242,8 @@ describe('', () => { expect(state).toBeInTheDocument(); expect(state).toBeVisible(); - Object.keys(mockRecordingLabels).forEach((key) => { - const label = screen.getByText(`${key}: ${mockRecordingLabels[key]}`); + mockRecordingLabels.forEach((entry) => { + const label = screen.getByText(`${entry.key}: ${entry.value}`); expect(label).toBeInTheDocument(); expect(label).toBeVisible(); }); diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index b8d028bf9..56dc27a9c 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -21,7 +21,13 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage, Target } from '@app/Shared/Services/api.types'; +import { + UPLOADS_SUBDIRECTORY, + ArchivedRecording, + NotificationMessage, + Target, + KeyValue, +} from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import { Text } from '@patternfly/react-core'; import '@testing-library/jest-dom'; @@ -57,13 +63,22 @@ const mockUploadedRecordingLabels = [ value: 'someUpdatedValue', }, ]; +export const convertLabels = (kv: KeyValue[]): object => { + const out = {}; + for (let e of kv) { + out[e.key] = e.value; + } + return out; +}; const mockMetadataFileName = 'mock.metadata.json'; const mockMetadataFile = new File( - [JSON.stringify({ labels: { ...mockUploadedRecordingLabels } })], + [JSON.stringify({ labels: convertLabels(mockUploadedRecordingLabels) })], mockMetadataFileName, { type: 'json' }, ); -mockMetadataFile.text = jest.fn(() => Promise.resolve(JSON.stringify({ labels: { ...mockUploadedRecordingLabels } }))); +mockMetadataFile.text = jest.fn(() => + Promise.resolve(JSON.stringify({ labels: convertLabels(mockUploadedRecordingLabels) })), +); const mockRecording: ArchivedRecording = { name: 'someRecording', @@ -91,7 +106,7 @@ const mockLabelsNotification = { target: mockConnectUrl, recordingName: 'someRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, }, } as NotificationMessage; const mockDeleteNotification = { @@ -237,8 +252,8 @@ describe('', () => { expect(size).toBeInTheDocument(); expect(size).toBeVisible(); - Object.keys(mockRecordingLabels).forEach((key) => { - const label = screen.getByText(`${key}: ${mockRecordingLabels[key]}`); + mockRecordingLabels.forEach((entry) => { + const label = screen.getByText(`${entry.key}: ${entry.value}`); expect(label).toBeInTheDocument(); expect(label).toBeVisible(); }); @@ -742,7 +757,7 @@ describe('', () => { expect(uploadSpy).toHaveBeenCalled(); expect(uploadSpy).toHaveBeenCalledWith( mockFileUpload, - mockUploadedRecordingLabels, + convertLabels(mockUploadedRecordingLabels), expect.any(Function), expect.any(Subject), ); From c29d250d759798465398b8e9149630425ca3f7f8 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:51:05 -0500 Subject: [PATCH 03/64] fixup! correct tests --- src/test/Recordings/ArchivedRecordingsTable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 56dc27a9c..e167c84c1 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -65,7 +65,7 @@ const mockUploadedRecordingLabels = [ ]; export const convertLabels = (kv: KeyValue[]): object => { const out = {}; - for (let e of kv) { + for (const e of kv) { out[e.key] = e.value; } return out; From 817e32dbc7cdf1599bd677918ccdff99e77ff4c8 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 13:01:05 -0500 Subject: [PATCH 04/64] fix(graphql): adjustments for Dashboard Automated Analysis with new API schema (#1220) * fix(graphql): adjustments for Dashboard Automated Analysis with new API schema --- .../AutomatedAnalysisCard.tsx | 36 +++++++++++-------- src/app/Shared/Services/Api.service.tsx | 10 +++++- .../AutomatedAnalysisCard.test.tsx | 10 +++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 901257860..4755d9578 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -179,18 +179,23 @@ export const AutomatedAnalysisCard: DashboardCardFC ` query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: { - name: "${automatedAnalysisRecordingName}", - labels: ["origin=${automatedAnalysisRecordingName}"], - }) { - data { - state - name - downloadUrl - reportUrl - metadata { - labels + target { + recordings { + active (filter: { + name: "${automatedAnalysisRecordingName}", + labels: ["origin=${automatedAnalysisRecordingName}"], + }) { + data { + state + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } } } } @@ -214,7 +219,10 @@ export const AutomatedAnalysisCard: DashboardCardFC downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size archivedTime @@ -358,7 +366,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].recordings.active.data[0] as Recording), + map((v) => v.data.targetNodes[0].target.recordings.active.data[0] as Recording), tap((recording) => { if (recording === null || recording === undefined) { throw new Error(NO_RECORDINGS_MESSAGE); diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 2bded2658..4f8b3b7a4 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -310,8 +310,16 @@ export class ApiService { if (archiveOnStop != undefined) { form.append('archiveOnStop', String(archiveOnStop)); } + const transformedMetadata = { + labels: {}, + annotations: { + cryostat: {}, + platform: {}, + }, + }; + metadata?.labels.forEach((label) => (transformedMetadata.labels[label.key] = label.value)); if (metadata) { - form.append('metadata', JSON.stringify(metadata)); + form.append('metadata', JSON.stringify(transformedMetadata)); } if (restart != undefined) { form.append('restart', String(restart)); diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 8f6d9a313..32b6bd382 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -162,7 +162,7 @@ const mockTargetNode = { const mockActiveRecordingsResponse = { data: { - targetNodes: [mockTargetNode], + targetNodes: [{ target: mockTargetNode }], }, }; @@ -170,9 +170,11 @@ const mockEmptyActiveRecordingsResponse = { data: { targetNodes: [ { - recordings: { - active: { - data: [], + target: { + recordings: { + active: { + data: [], + }, }, }, }, From 885487b5851ee8384ff96c40e68a431fb6687c1b Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 16:08:58 -0500 Subject: [PATCH 05/64] fix(archives): adjust queries and response handling for updated data schema (#1221) * fix(archives): adjust queries and response handling for updated data schema --- .../AllTargetsArchivedRecordingsTable.tsx | 28 ++++----- src/app/RecordingMetadata/BulkEditLabels.tsx | 2 +- .../Recordings/ArchivedRecordingsTable.tsx | 12 +++- ...AllTargetsArchivedRecordingsTable.test.tsx | 58 ++++++++++--------- 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index a52104a5e..6ddb90d8a 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -112,7 +112,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC { const target: Target = { - connectUrl: node.target.serviceUri, + connectUrl: node.target.connectUrl, alias: node.target.alias, labels: [], annotations: { @@ -123,7 +123,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC = ({ observable = isUploadsTable ? context.api .graphql( - `query GetUploadedRecordings($filter: ArchivedRecordingFilterInput) { + `query GetUploadedRecordings($filter: ArchivedRecordingsFilterInput) { archivedRecordings(filter: $filter) { data { name diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 3d23450f8..9c16de6e2 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -198,7 +198,10 @@ export const ArchivedRecordingsTable: React.FC = ( downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size } @@ -213,14 +216,17 @@ export const ArchivedRecordingsTable: React.FC = ( const queryUploadedRecordings = React.useCallback(() => { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ return context.api.graphql( - `query UploadedRecordings($filter: ArchivedRecordingFilterInput){ + `query UploadedRecordings($filter: ArchivedRecordingsFilterInput) { archivedRecordings(filter: $filter) { data { name downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size } diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 6af7d3070..0fdd8e029 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -76,42 +76,42 @@ const mockTargetsAndCountsResponse = { data: { targetNodes: [ { - recordings: { - archived: { - aggregate: { - count: mockCount1, - }, - }, - }, target: { alias: mockAlias1, - serviceUri: mockConnectUrl1, - }, - }, - { - recordings: { - archived: { - aggregate: { - count: mockCount2, + connectUrl: mockConnectUrl1, + recordings: { + archived: { + aggregate: { + count: mockCount1, + }, }, }, }, - target: { - alias: mockAlias2, - serviceUri: mockConnectUrl2, - }, }, { - recordings: { - archived: { - aggregate: { - count: mockCount3, + target: { + alias: mockAlias2, + connectUrl: mockConnectUrl2, + recordings: { + archived: { + aggregate: { + count: mockCount2, + }, }, }, }, + }, + { target: { alias: mockAlias3, - serviceUri: mockConnectUrl3, + connectUrl: mockConnectUrl3, + recordings: { + archived: { + aggregate: { + count: mockCount3, + }, + }, + }, }, }, ], @@ -122,10 +122,12 @@ const mockNewTargetCountResponse = { data: { targetNodes: [ { - recordings: { - archived: { - aggregate: { - count: mockNewCount, + target: { + recordings: { + archived: { + aggregate: { + count: mockNewCount, + }, }, }, }, From 811bd991001adb43508e8110926e8fbd49c4eda2 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 14:10:27 -0500 Subject: [PATCH 06/64] fix(topology): update for new GraphQL schema --- .../CreateRecording/CustomRecordingForm.tsx | 2 +- src/app/CreateRecording/types.ts | 2 + src/app/RecordingMetadata/BulkEditLabels.tsx | 2 +- src/app/Shared/Services/Api.service.tsx | 132 ++++++++++++------ src/app/Shared/Services/api.types.ts | 43 ++++-- src/app/Topology/Actions/utils.tsx | 63 +++++---- src/app/utils/fakeData.ts | 4 +- .../CustomRecordingForm.test.tsx | 2 +- .../RecordingMetadata/BulkEditLabels.test.tsx | 8 +- 9 files changed, 160 insertions(+), 98 deletions(-) diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index ee34aa0ea..dfe0a1688 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -253,7 +253,7 @@ export const CustomRecordingForm: React.FC = () => { events: eventSpecifierString, duration: continuous ? undefined : duration * (durationUnit / 1000), archiveOnStop: archiveOnStop && !continuous, - restart: restart, + replace: restart ? 'ALWAYS' : 'NEVER', advancedOptions: { toDisk: toDisk, maxAge: toDisk ? (continuous ? maxAge * maxAgeUnit : undefined) : undefined, diff --git a/src/app/CreateRecording/types.ts b/src/app/CreateRecording/types.ts index e11288c17..00d9492b7 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -18,6 +18,8 @@ import { ValidatedOptions } from '@patternfly/react-core'; export type EventTemplateIdentifier = Pick; +export type RecordingReplace = 'ALWAYS' | 'NEVER' | 'STOPPED'; + interface _FormBaseData { name: string; template?: EventTemplateIdentifier; diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 3ecb392ec..cd5b931be 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -205,7 +205,7 @@ export const BulkEditLabels: React.FC = ({ { connectUrl: target.connectUrl }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.recordings.archived.data as ArchivedRecording[]), first(), ); } diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 4f8b3b7a4..faf531d50 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -45,21 +45,23 @@ import { CredentialsResponse, RulesResponse, EnvironmentNode, - DiscoveryResponse, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, RecordingCountResponse, MBeanMetrics, MBeanMetricsResponse, EventType, NotificationCategory, - NullableTarget, HttpError, SimpleResponse, XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, KeyValue, - CustomTargetStub, + TargetStub, + TargetForTest, + Metadata, + TargetMetadata, + isTargetMetadata, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -171,7 +173,7 @@ export class ApiService { } createTarget( - target: CustomTargetStub, + target: TargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -207,7 +209,7 @@ export class ApiService { ); } - deleteTarget(target: Target): Observable { + deleteTarget(target: TargetStub): Observable { return this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}`, { method: 'DELETE', }).pipe( @@ -296,7 +298,7 @@ export class ApiService { name, events, duration, - restart, + replace, archiveOnStop, metadata, advancedOptions, @@ -310,19 +312,11 @@ export class ApiService { if (archiveOnStop != undefined) { form.append('archiveOnStop', String(archiveOnStop)); } - const transformedMetadata = { - labels: {}, - annotations: { - cryostat: {}, - platform: {}, - }, - }; - metadata?.labels.forEach((label) => (transformedMetadata.labels[label.key] = label.value)); if (metadata) { - form.append('metadata', JSON.stringify(transformedMetadata)); + form.append('metadata', JSON.stringify(this.transformMetadataToObject(metadata))); } - if (restart != undefined) { - form.append('restart', String(restart)); + if (replace != undefined) { + form.append('replace', String(replace)); } if (advancedOptions) { if (advancedOptions.toDisk != undefined) { @@ -489,7 +483,7 @@ export class ApiService { } uploadArchivedRecordingToGrafana( - sourceTarget: Observable, + sourceTarget: Observable, recordingName: string, ): Observable { return sourceTarget.pipe( @@ -531,7 +525,13 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: KeyValue[]) { + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + stringifyRecordingLabels(labels: KeyValue | KeyValue[]): string { + return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + } + + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + transformAndStringifyToRawLabels(labels: KeyValue[]): string { const rawLabels = {}; for (const label of labels) { rawLabels[label.key] = label.value; @@ -539,6 +539,30 @@ export class ApiService { return JSON.stringify(rawLabels); } + transformMetadataToObject(metadata: Metadata | TargetMetadata): object { + if (isTargetMetadata(metadata)) { + return { + labels: this.transformLabelsToObject(metadata.labels), + annotations: { + cryostat: this.transformLabelsToObject(metadata?.annotations?.cryostat), + platform: this.transformLabelsToObject(metadata?.annotations?.platform), + }, + }; + } else { + return { + labels: this.transformLabelsToObject(metadata.labels), + }; + } + } + + transformLabelsToObject(labels: KeyValue[]): object { + const out = {}; + for (const label of labels) { + out[label.key] = label.value; + } + return out; + } + postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: KeyValue[]): Observable { return this.sendRequest( 'beta', @@ -734,7 +758,7 @@ export class ApiService { } getActiveProbesForTarget( - target: Target, + target: TargetStub, suppressNotifications = false, skipStatusCheck = false, ): Observable { @@ -922,7 +946,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.recordings.archived as ArchivedRecording[]), ); } @@ -969,7 +993,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].recordings.active as ActiveRecording[]), + map((v) => v.data.targetNodes[0].target.recordings.active as ActiveRecording[]), ); } @@ -1043,11 +1067,10 @@ export class ApiService { } getDiscoveryTree(): Observable { - return this.sendRequest('v2.1', 'discovery', { + return this.sendRequest('v3', 'discovery', { method: 'GET', }).pipe( concatMap((resp) => resp.json()), - map((body: DiscoveryResponse) => body.data.result), first(), ); } @@ -1056,7 +1079,7 @@ export class ApiService { matchTargetsWithExpr(matchExpression: string, targets: Target[]): Observable { const body = JSON.stringify({ matchExpression, - targets, + targets: targets.map((t) => this.transformTarget(t)), }); const headers = new Headers(); headers.set('Content-Type', 'application/json'); @@ -1087,20 +1110,22 @@ export class ApiService { ); } - groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingFilterInput): Observable { + groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingsFilterInput): Observable { return this.graphql( ` - query GetRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query GetRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - name - } + data { + name + } } + } } } } @@ -1114,7 +1139,7 @@ export class ApiService { first(), map((body) => body.data.environmentNodes[0].descendantTargets.reduce( - (acc: Partial[], curr) => acc.concat(curr.recordings?.active?.data || []), + (acc: Partial[], curr) => acc.concat(curr.target.recordings?.active?.data || []), [] as Partial[], ), ), @@ -1123,10 +1148,10 @@ export class ApiService { ); } - targetHasRecording(target: Target, filter: ActiveRecordingFilterInput = {}): Observable { + targetHasRecording(target: TargetStub, filter: ActiveRecordingsFilterInput = {}): Observable { return this.graphql( ` - query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingFilterInput) { + query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingsFilterInput) { targetNodes(filter: { name: $connectUrl }) { recordings { active (filter: $recordingFilter) { @@ -1149,7 +1174,7 @@ export class ApiService { if (nodes.length === 0) { return false; } - const count = nodes[0].recordings.active.aggregate.count; + const count = nodes[0].target.recordings.active.aggregate.count; return count > 0; }), catchError((_) => of(false)), @@ -1157,7 +1182,7 @@ export class ApiService { } checkCredentialForTarget( - target: Target, + target: TargetStub, credentials: { username: string; password: string }, ): Observable< | { @@ -1210,7 +1235,7 @@ export class ApiService { ); } - getTargetMBeanMetrics(target: Target, queries: string[]): Observable { + getTargetMBeanMetrics(target: TargetStub, queries: string[]): Observable { return this.graphql( ` query MBeanMXMetricsForTarget($connectUrl: String) { @@ -1235,7 +1260,7 @@ export class ApiService { ); } - getTargetArchivedRecordings(target: Target): Observable { + getTargetArchivedRecordings(target: TargetStub): Observable { return this.graphql( ` query ArchivedRecordingsForTarget($connectUrl: String) { @@ -1245,7 +1270,10 @@ export class ApiService { downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size archivedTime @@ -1258,7 +1286,7 @@ export class ApiService { ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); } - getTargetActiveRecordings(target: Target): Observable { + getTargetActiveRecordings(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/recordings`, 'v1', @@ -1268,7 +1296,7 @@ export class ApiService { ); } - getTargetEventTemplates(target: Target): Observable { + getTargetEventTemplates(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/templates`, 'v1', @@ -1278,7 +1306,7 @@ export class ApiService { ); } - getTargetEventTypes(target: Target): Observable { + getTargetEventTypes(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/events`, 'v1', @@ -1317,8 +1345,22 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: KeyValue[]): string { - return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + private transformTarget(target: Target): TargetForTest { + const out: TargetForTest = { + alias: target.alias, + connectUrl: target.connectUrl, + labels: {}, + annotations: { cryostat: {}, platform: {} }, + }; + for (const l of target.labels) { + out.labels[l.key] = l.value; + } + for (const s of ['cryostat', 'platform']) { + for (const e of out.annotations[s]) { + target.annotations[s][e.key] = e.value; + } + } + return out; } private sendRequest( diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 2bd3b0ba3..b7a92003b 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { RecordingReplace } from '@app/CreateRecording/types'; import { AlertVariant } from '@patternfly/react-core'; import { Observable } from 'rxjs'; -export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'beta'; +export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'v3' | 'beta'; // ====================================== // Common Resources @@ -29,7 +30,17 @@ export interface KeyValue { export interface Metadata { labels: KeyValue[]; - annotations?: KeyValue[]; +} + +export type TargetMetadata = Metadata & { + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; + }; +}; + +export function isTargetMetadata(metadata: Metadata | TargetMetadata): metadata is TargetMetadata { + return (metadata as TargetMetadata).annotations !== undefined; } export interface ApiV2Response { @@ -88,7 +99,12 @@ export class XMLHttpError extends Error { } } -export type CustomTargetStub = Omit; +export type TargetStub = Omit; + +export type TargetForTest = Pick & { + labels: object; + annotations: { cryostat: object; platform: object }; +}; // ====================================== // Health Resources @@ -210,7 +226,7 @@ export interface RecordingAttributes { events: string; duration?: number; archiveOnStop?: boolean; - restart?: boolean; + replace?: RecordingReplace; advancedOptions?: AdvancedRecordingOptions; metadata?: Metadata; } @@ -238,7 +254,7 @@ export interface ActiveRecording extends Recording { maxAge: number; } -export interface ActiveRecordingFilterInput { +export interface ActiveRecordingsFilterInput { name?: string; state?: string; continuous?: boolean; @@ -265,10 +281,12 @@ export interface RecordingResponse extends ApiV2Response { export interface RecordingCountResponse { data: { targetNodes: { - recordings: { - active: { - aggregate: { - count: number; + target: { + recordings: { + active: { + aggregate: { + count: number; + }; }; }; }; @@ -446,6 +464,7 @@ export const TEMPLATE_UNSUPPORTED_MESSAGE = 'The template type used in this reco // Discovery/Target resources // ====================================== export interface Target { + id?: number; // present in responses but we must not include it in requests to create targets jvmId?: string; // present in responses, but we do not need to provide it in requests connectUrl: string; alias: string; @@ -499,12 +518,6 @@ export interface TargetNode extends _AbstractNode { readonly target: Target; } -export interface DiscoveryResponse extends ApiV2Response { - data: { - result: EnvironmentNode; - }; -} - // ====================================== // Notification resources // ====================================== diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index aee7c9300..dd108d2f9 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -122,23 +122,23 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StartRecordingForGroup($filter: EnvironmentNodeFilterInput, $recordingName: String!, $labels: String) { + query StartRecordingForGroup($filter: EnvironmentNodesFilterInput!, $recordingName: String!, $metadata: RecordingMetadataInput!) { environmentNodes(filter: $filter) { name descendantTargets { name - doStartRecording(recording: { - name: $recordingName, - template: "Continuous", - templateType: "TARGET", - duration: 0, - restart: true, - metadata: { - labels: $labels - }, - }) { - name - state + target { + doStartRecording(recording: { + name: $recordingName, + template: "Continuous", + templateType: "TARGET", + duration: 0, + replace: "STOPPED", + metadata: $metadata, + }) { + name + state + } } } } @@ -147,12 +147,9 @@ export const nodeActions: NodeAction[] = [ { filter: { id: group.id }, recordingName: QUICK_RECORDING_NAME, - labels: services.api.stringifyRecordingLabels([ - { - key: QUICK_RECORDING_LABEL_KEY, - value: group.name.replace(/[\s+-]/g, '_'), - }, - ]), + metadata: { + labels: [{ key: QUICK_RECORDING_LABEL_KEY, value: group.name.replace(/[\s+-]/g, '_') }], + }, }, false, true, @@ -171,12 +168,13 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { data { doArchive { @@ -184,6 +182,7 @@ export const nodeActions: NodeAction[] = [ } } } + } } } } @@ -213,20 +212,22 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StopRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query StopRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doStop { - name - state - } + data { + doStop { + name + state } + } } + } } } } @@ -257,12 +258,13 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { data { doDelete { @@ -271,6 +273,7 @@ export const nodeActions: NodeAction[] = [ } } } + } } } } diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index 0075d0fa0..f6a6443d6 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -23,7 +23,7 @@ import { RecordingState, Recording, MBeanMetrics, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, ArchivedRecording, EventTemplate, EventProbe, @@ -301,7 +301,7 @@ class FakeApiService extends ApiService { } // JFR Metrics card - targetHasRecording(_target: Target, _filter?: ActiveRecordingFilterInput): Observable { + targetHasRecording(_target: Target, _filter?: ActiveRecordingsFilterInput): Observable { return of(true); } diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bdc185501..9392a3094 100644 --- a/src/test/CreateRecording/CustomRecordingForm.test.tsx +++ b/src/test/CreateRecording/CustomRecordingForm.test.tsx @@ -118,7 +118,7 @@ describe('', () => { events: 'template=someEventTemplate,type=CUSTOM', duration: 30, archiveOnStop: true, - restart: false, + replace: 'NEVER', advancedOptions: { maxAge: undefined, maxSize: 0, diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index cec202ce1..458eef774 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -107,9 +107,11 @@ const mockArchivedRecordingsResponse = { data: { targetNodes: [ { - recordings: { - archived: { - data: [mockArchivedRecording] as ArchivedRecording[], + target: { + recordings: { + archived: { + data: [mockArchivedRecording] as ArchivedRecording[], + }, }, }, }, From cae8d54239a4c6f3518338998f0b4e29d5dbcce0 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 10:35:31 -0500 Subject: [PATCH 07/64] adjust queries to avoid 'recordings' struct field and use new direct activeRecordings/archivedRecordings accessors --- .../AllTargetsArchivedRecordingsTable.tsx | 20 ++--- .../AutomatedAnalysisCard.tsx | 60 ++++++------- src/app/RecordingMetadata/BulkEditLabels.tsx | 6 +- .../Recordings/ArchivedRecordingsTable.tsx | 26 +++--- src/app/Shared/Services/Api.service.tsx | 86 +++++++++---------- src/app/Shared/Services/api.types.ts | 8 +- 6 files changed, 101 insertions(+), 105 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 6ddb90d8a..260599425 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -123,7 +123,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { target { - recordings { - active (filter: { - name: "${automatedAnalysisRecordingName}", - labels: ["origin=${automatedAnalysisRecordingName}"], - }) { - data { - state - name - downloadUrl - reportUrl - metadata { - labels { - key - value - } + activeRecordings(filter: { + name: "${automatedAnalysisRecordingName}", + labels: ["origin=${automatedAnalysisRecordingName}"], + }) { + data { + state + name + downloadUrl + reportUrl + metadata { + labels { + key + value } } } @@ -213,21 +211,25 @@ export const AutomatedAnalysisCard: DashboardCardFC /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ return context.api.graphql( `query ArchivedRecordingsForAutomatedAnalysis($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } } } - size - archivedTime } - } }`, { connectUrl }, ); @@ -311,7 +313,7 @@ export const AutomatedAnalysisCard: DashboardCardFC queryArchivedRecordings(connectUrl) .pipe( first(), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: (recordings) => { @@ -366,7 +368,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].target.recordings.active.data[0] as Recording), + map((v) => v.data.targetNodes[0].target.activeRecordings.data[0] as Recording), tap((recording) => { if (recording === null || recording === undefined) { throw new Error(NO_RECORDINGS_MESSAGE); diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index cd5b931be..47ec48833 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -188,8 +188,8 @@ export const BulkEditLabels: React.FC = ({ context.api.graphql( `query ArchivedRecordingsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived { + target { + archivedRecordings { data { name downloadUrl @@ -205,7 +205,7 @@ export const BulkEditLabels: React.FC = ({ { connectUrl: target.connectUrl }, ), ), - map((v) => v.data.targetNodes[0].target.recordings.archived.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), first(), ); } diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 9c16de6e2..88ac40dcd 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -192,18 +192,22 @@ export const ArchivedRecordingsTable: React.FC = ( return context.api.graphql( ` query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size } } - size } } }`, @@ -256,7 +260,7 @@ export const ArchivedRecordingsTable: React.FC = ( filter((target) => !!target), first(), concatMap((target: Target) => queryTargetRecordings(target.connectUrl)), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: handleRecordings, diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index faf531d50..8b6df45bc 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -930,13 +930,11 @@ export class ApiService { ` query PostRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived(filter: { name: $recordingName }) { - data { - doPutMetadata(metadata: { labels: $labels }) { - metadata { - labels - } + archivedRecordings(filter: { name: $recordingName }) { + data { + doPutMetadata(metadata: { labels: $labels }) { + metadata { + labels } } } @@ -946,7 +944,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].target.recordings.archived as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings as ArchivedRecording[]), ); } @@ -977,13 +975,11 @@ export class ApiService { ` query PostActiveRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active(filter: { name: $recordingName }) { - data { - doPutMetadata(metadata: { labels: $labels }) { - metadata { - labels - } + activeRecordings(filter: { name: $recordingName }) { + data { + doPutMetadata(metadata: { labels: $labels }) { + metadata { + labels } } } @@ -993,7 +989,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].target.recordings.active as ActiveRecording[]), + map((v) => v.data.targetNodes[0].target.activeRecordings as ActiveRecording[]), ); } @@ -1113,17 +1109,15 @@ export class ApiService { groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingsFilterInput): Observable { return this.graphql( ` - query GetRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ + query GroupHasRecording ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ environmentNodes(filter: $groupFilter) { name descendantTargets { name target { - recordings { - active(filter: $recordingFilter) { - data { - name - } + activeRecordings(filter: $recordingFilter) { + aggregate { + count } } } @@ -1139,12 +1133,12 @@ export class ApiService { first(), map((body) => body.data.environmentNodes[0].descendantTargets.reduce( - (acc: Partial[], curr) => acc.concat(curr.target.recordings?.active?.data || []), - [] as Partial[], + (acc: number, curr) => acc + curr.target.activeRecordings.aggregate.count, + 0, ), ), catchError((_) => of([])), - map((recs: Partial[]) => recs.length > 0), // At least one + map((acc) => acc > 0), // At least one ); } @@ -1153,11 +1147,9 @@ export class ApiService { ` query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingsFilterInput) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: $recordingFilter) { - aggregate { - count - } + activeRecordings(filter: $recordingFilter) { + aggregate { + count } } } @@ -1174,7 +1166,7 @@ export class ApiService { if (nodes.length === 0) { return false; } - const count = nodes[0].target.recordings.active.aggregate.count; + const count = nodes[0].target.activeRecordings.aggregate.count; return count > 0; }), catchError((_) => of(false)), @@ -1263,27 +1255,31 @@ export class ApiService { getTargetArchivedRecordings(target: TargetStub): Observable { return this.graphql( ` - query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + query ArchivedRecordingsForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } } + size + archivedTime } - size - archivedTime } } - }`, + } + }`, { connectUrl: target.connectUrl }, true, true, - ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); + ).pipe(map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[])); } getTargetActiveRecordings(target: TargetStub): Observable { diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index b7a92003b..1f124d35c 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -282,11 +282,9 @@ export interface RecordingCountResponse { data: { targetNodes: { target: { - recordings: { - active: { - aggregate: { - count: number; - }; + activeRecordings: { + aggregate: { + count: number; }; }; }; From 95e18cb6d56fc87c94196f2198856ad7dae63f84 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 7 Mar 2024 11:27:04 -0500 Subject: [PATCH 08/64] use new filter input type name --- src/app/Shared/Services/Api.service.tsx | 2 +- src/app/Topology/Actions/utils.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 8b6df45bc..adab2d348 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -1109,7 +1109,7 @@ export class ApiService { groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingsFilterInput): Observable { return this.graphql( ` - query GroupHasRecording ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ + query GroupHasRecording ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ environmentNodes(filter: $groupFilter) { name descendantTargets { diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index dd108d2f9..fc04d8901 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -122,7 +122,7 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StartRecordingForGroup($filter: EnvironmentNodesFilterInput!, $recordingName: String!, $metadata: RecordingMetadataInput!) { + query StartRecordingForGroup($filter: DiscoveryNodeFilterInput!, $recordingName: String!, $metadata: RecordingMetadataInput!) { environmentNodes(filter: $filter) { name descendantTargets { @@ -168,7 +168,7 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { @@ -212,7 +212,7 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StopRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { + query StopRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { @@ -258,7 +258,7 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodesFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { From 08aca67f3d6d6a8897573778b53a2be3f94b9063 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 10:46:17 -0400 Subject: [PATCH 09/64] indentation --- src/app/Topology/Actions/utils.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index fc04d8901..f7f358a16 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -176,11 +176,11 @@ export const nodeActions: NodeAction[] = [ target { recordings { active(filter: $recordingFilter) { - data { - doArchive { - name - } + data { + doArchive { + name } + } } } } @@ -266,12 +266,12 @@ export const nodeActions: NodeAction[] = [ target { recordings { active(filter: $recordingFilter) { - data { - doDelete { - name - state - } + data { + doDelete { + name + state } + } } } } From 4de75119af9d176152529090c42d4a82352ccf68 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 12:06:32 -0400 Subject: [PATCH 10/64] update fake graphql query response format --- src/app/utils/fakeData.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index f6a6443d6..bc88cf54e 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -347,8 +347,8 @@ class FakeApiService extends ApiService { return of([]); } - // Automatic Analysis Card - // This fakes the fetch for Automatic Analysis recording to return available. + // Automated Analysis Card + // This fakes the fetch for Automated Analysis recording to return available. // Then subsequent graphql call for archived recording is ignored graphql( _query: string, @@ -360,10 +360,8 @@ class FakeApiService extends ApiService { data: { targetNodes: [ { - recordings: { - active: { - data: [fakeAARecording], - }, + activeRecordings: { + data: [fakeAARecording], }, }, ], From 4bbda01b0c16dd5e3a27897aa3d6b10c3f00a4f7 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 12:06:50 -0400 Subject: [PATCH 11/64] use dynamic mock timestamp --- src/app/utils/fakeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index bc88cf54e..66d5f0f9f 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -208,7 +208,7 @@ export const fakeEvaluations: AnalysisResult[] = [ export const fakeCachedReport: CachedReportValue = { report: fakeEvaluations, - timestamp: 1663027200000, + timestamp: Date.now() - (1000 * 60 * 60), }; class FakeTargetService extends TargetService { From 387edd6809fd8a1310706e0e6b622eb45b39376a Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 14:09:56 -0400 Subject: [PATCH 12/64] apply formatting --- src/app/utils/fakeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index 66d5f0f9f..faffa68c8 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -208,7 +208,7 @@ export const fakeEvaluations: AnalysisResult[] = [ export const fakeCachedReport: CachedReportValue = { report: fakeEvaluations, - timestamp: Date.now() - (1000 * 60 * 60), + timestamp: Date.now() - 1000 * 60 * 60, }; class FakeTargetService extends TargetService { From daa01bcf769bd0b733887f95a5ef05fc81a9c84e Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 14:10:12 -0400 Subject: [PATCH 13/64] schema adjustments for Mirage preview --- src/mirage/index.ts | 82 ++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/mirage/index.ts b/src/mirage/index.ts index d42a8b0f1..d78719320 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -126,35 +126,27 @@ export const startMirage = ({ environment = 'development' } = {}) => { }; }); this.get('api/v1/targets', (schema) => schema.all(Resource.TARGET).models); - this.get('api/v2.1/discovery', (schema) => { + this.get('api/v3/discovery', (schema) => { const models = schema.all(Resource.TARGET).models; const realmTypes = models.map((t) => t.annotations.cryostat['REALM']); return { - meta: { - status: 'OK', - type: 'application/json', - }, - data: { - result: { - name: 'Universe', - nodeType: 'Universe', - labels: [], - children: realmTypes.map((r: string) => ({ - name: r, - nodeType: 'Realm', - labels: [], - id: r, - children: models - .filter((t) => t.annotations.cryostat['REALM'] === r) - .map((t) => ({ - id: t.alias, - name: t.alias, - nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', - target: t, - })), + name: 'Universe', + nodeType: 'Universe', + labels: [], + children: realmTypes.map((r: string) => ({ + name: r, + nodeType: 'Realm', + labels: [], + id: r, + children: models + .filter((t) => t.annotations.cryostat['REALM'] === r) + .map((t) => ({ + id: t.alias, + name: t.alias, + nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', + target: t, })), - }, - }, + })), }; }); this.get('api/v1/recordings', (schema) => schema.all(Resource.ARCHIVE).models); @@ -505,17 +497,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { case 'ArchivedRecordingsForTarget': case 'UploadedRecordings': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForTarget': data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: schema.all(Resource.ARCHIVE).models, }, }, @@ -525,17 +523,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { break; case 'ArchivedRecordingsForAutomatedAnalysis': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForAutomatedAnalysis': data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: schema.all(Resource.RECORDING).models, }, }, @@ -556,8 +560,8 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [ { doPutMetadata: { @@ -602,8 +606,8 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [ { doPutMetadata: { From 3849ab47b4dafa1eaa46b7fb4ac106ccbcdca08f Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 14:48:52 -0400 Subject: [PATCH 14/64] fixup --- src/app/Shared/Services/Api.service.tsx | 8 +++++--- src/app/utils/fakeData.ts | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index adab2d348..388a1d107 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -1147,9 +1147,11 @@ export class ApiService { ` query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingsFilterInput) { targetNodes(filter: { name: $connectUrl }) { - activeRecordings(filter: $recordingFilter) { - aggregate { - count + target { + activeRecordings(filter: $recordingFilter) { + aggregate { + count + } } } } diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index faffa68c8..1723e190c 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -360,8 +360,10 @@ class FakeApiService extends ApiService { data: { targetNodes: [ { - activeRecordings: { - data: [fakeAARecording], + target: { + activeRecordings: { + data: [fakeAARecording], + }, }, }, ], From f122a6eff0988bb4687b9ea34d9e5547e23340fc Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 15:07:11 -0400 Subject: [PATCH 15/64] indentation --- .../AllTargetsArchivedRecordingsTable.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 260599425..e9396ed54 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -146,18 +146,18 @@ export const AllTargetsArchivedRecordingsTable: React.FC( `query AllTargetsArchives { - targetNodes { - target { - connectUrl - alias - archivedRecordings { - aggregate { - count - } + targetNodes { + target { + connectUrl + alias + archivedRecordings { + aggregate { + count } } } - }`, + } + }`, ) .pipe(map((v) => v.data.targetNodes)) .subscribe({ @@ -173,18 +173,17 @@ export const AllTargetsArchivedRecordingsTable: React.FC( - ` - query ArchiveCountForTarget($connectUrl: String) { - targetNodes(filter: { name: $connectUrl }) { - target { - archivedRecordings { - aggregate { - count + `query ArchiveCountForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + aggregate { + count + } + } } } - } - } - }`, + }`, { connectUrl: target.connectUrl }, ) .subscribe((v) => { From 1f5f62da030f634003c0e978cf579cf810ee3058 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 11 Mar 2024 15:09:41 -0400 Subject: [PATCH 16/64] test fixup --- ...AllTargetsArchivedRecordingsTable.test.tsx | 32 +++++++---------- .../AutomatedAnalysisCard.test.tsx | 36 +++++++++++-------- .../RecordingMetadata/BulkEditLabels.test.tsx | 6 ++-- .../ArchivedRecordingsTable.test.tsx | 28 +++++++++++++-- 4 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 0fdd8e029..8a021df10 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -79,11 +79,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias1, connectUrl: mockConnectUrl1, - recordings: { - archived: { - aggregate: { - count: mockCount1, - }, + archivedRecordings: { + aggregate: { + count: mockCount1, }, }, }, @@ -92,11 +90,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias2, connectUrl: mockConnectUrl2, - recordings: { - archived: { - aggregate: { - count: mockCount2, - }, + archivedRecordings: { + aggregate: { + count: mockCount2, }, }, }, @@ -105,11 +101,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias3, connectUrl: mockConnectUrl3, - recordings: { - archived: { - aggregate: { - count: mockCount3, - }, + archivedRecordings: { + aggregate: { + count: mockCount3, }, }, }, @@ -123,11 +117,9 @@ const mockNewTargetCountResponse = { targetNodes: [ { target: { - recordings: { - archived: { - aggregate: { - count: mockNewCount, - }, + archivedRecordings: { + aggregate: { + count: mockNewCount, }, }, }, diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 32b6bd382..6b27417d6 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -153,10 +153,8 @@ const mockCachedReport: CachedReportValue = { }; const mockTargetNode = { - recordings: { - active: { - data: [mockRecording], - }, + activeRecordings: { + data: [mockRecording], }, }; @@ -171,10 +169,8 @@ const mockEmptyActiveRecordingsResponse = { targetNodes: [ { target: { - recordings: { - active: { - data: [], - }, + activeRecordings: { + data: [], }, }, }, @@ -184,17 +180,29 @@ const mockEmptyActiveRecordingsResponse = { const mockArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [mockArchivedRecording], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockArchivedRecording], + }, + }, + }, + ], }, }; const mockEmptyArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [], + }, + }, + }, + ], }, }; diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index 458eef774..4745612ec 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -108,10 +108,8 @@ const mockArchivedRecordingsResponse = { targetNodes: [ { target: { - recordings: { - archived: { - data: [mockArchivedRecording] as ArchivedRecording[], - }, + archivedRecordings: { + data: [mockArchivedRecording] as ArchivedRecording[], }, }, }, diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index e167c84c1..59a9509b9 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -90,9 +90,27 @@ const mockRecording: ArchivedRecording = { }; const mockArchivedRecordingsResponse = { + data: { + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockRecording], + }, + }, + }, + ], + }, +}; + +const mockAllArchivedRecordingsResponse = { data: { archivedRecordings: { - data: [mockRecording] as ArchivedRecording[], + data: [mockRecording], + aggregate: { + count: 1, + size: mockRecording.size, + }, }, }, }; @@ -135,7 +153,13 @@ jest.spyOn(defaultServices.api, 'deleteArchivedRecording').mockReturnValue(of(tr jest.spyOn(defaultServices.api, 'downloadRecording').mockReturnValue(); jest.spyOn(defaultServices.api, 'grafanaDatasourceUrl').mockReturnValue(of('/datasource')); jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of('/grafanaUrl')); -jest.spyOn(defaultServices.api, 'graphql').mockReturnValue(of(mockArchivedRecordingsResponse)); +jest.spyOn(defaultServices.api, 'graphql').mockImplementation((query: string) => { + if (query.includes('ArchivedRecordingsForTarget')) { + return of(mockArchivedRecordingsResponse); + } else { + return of(mockAllArchivedRecordingsResponse); + } +}); jest.spyOn(defaultServices.api, 'uploadArchivedRecordingToGrafana').mockReturnValue(of(true)); jest From 87d8e3b2182dd8cb8689e2a1c4d880707d3cf05d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:24:26 -0500 Subject: [PATCH 17/64] chore(mbeans): adjust MBeanMetrics GraphQL query for updated schema (#1219) --- .../AllTargetsArchivedRecordingsTable.tsx | 10 ++ src/app/Archives/ArchiveUploadModal.tsx | 6 +- src/app/Archives/Archives.tsx | 5 + src/app/Archives/utils.tsx | 5 + .../CreateRecording/CustomRecordingForm.tsx | 27 ++-- src/app/CreateRecording/types.ts | 5 +- .../mbean/MBeanMetricsChartController.tsx | 3 + .../Dashboard/JvmDetails/JvmDetailsCard.tsx | 2 +- src/app/RecordingMetadata/BulkEditLabels.tsx | 16 +-- src/app/RecordingMetadata/ClickableLabel.tsx | 6 +- src/app/RecordingMetadata/LabelCell.tsx | 10 +- .../RecordingLabelFields.tsx | 14 +- src/app/RecordingMetadata/types.tsx | 20 --- src/app/RecordingMetadata/utils.ts | 22 +--- src/app/Recordings/ActiveRecordingsTable.tsx | 8 +- .../Recordings/ArchivedRecordingsTable.tsx | 8 +- src/app/Recordings/Filters/LabelFilter.tsx | 4 +- .../MatchExpression/MatchExpressionHint.tsx | 6 +- .../Components/MatchExpression/utils.tsx | 2 +- src/app/Shared/Services/Api.service.tsx | 25 ++-- src/app/Shared/Services/api.types.ts | 31 +++-- src/app/Shared/Services/api.utils.ts | 4 +- src/app/Shared/Services/service.utils.ts | 9 +- src/app/TargetView/TargetContextSelector.tsx | 5 +- src/app/TargetView/TargetSelect.tsx | 5 +- src/app/Topology/Actions/CreateTarget.tsx | 11 +- src/app/Topology/Entity/EntityAnnotations.tsx | 5 +- src/app/Topology/Entity/EntityDetails.tsx | 41 +++--- src/app/Topology/Entity/types.ts | 6 + src/app/Topology/Entity/utils.tsx | 3 +- src/app/utils/fakeData.ts | 64 ++++++--- src/app/utils/utils.ts | 13 ++ src/mirage/factories.ts | 18 ++- src/mirage/index.ts | 121 ++++++++++-------- src/test/Agent/AgentLiveProbes.test.tsx | 8 +- .../AllArchivedRecordingsTable.test.tsx | 9 +- ...AllTargetsArchivedRecordingsTable.test.tsx | 6 + .../CustomRecordingForm.test.tsx | 14 +- .../SnapshotRecordingForm.test.tsx | 8 +- .../AutomatedAnalysisCard.test.tsx | 10 +- .../AutomatedAnalysisConfigForm.test.tsx | 8 +- .../Charts/jfr/JFRMetricsChartCard.test.tsx | 8 +- .../mbean/MBeanMetricsChartCard.test.tsx | 8 +- src/test/Dashboard/Dashboard.test.tsx | 5 +- src/test/Events/EventTemplates.test.tsx | 12 +- src/test/Events/EventTypes.test.tsx | 12 +- .../RecordingMetadata/BulkEditLabels.test.tsx | 18 ++- .../RecordingMetadata/ClickableLabel.test.tsx | 6 +- src/test/RecordingMetadata/LabelCell.test.tsx | 12 +- .../RecordingLabelFields.test.tsx | 18 +-- .../Recordings/ActiveRecordingsTable.test.tsx | 19 ++- .../ArchivedRecordingsTable.test.tsx | 31 ++++- .../Filters/DurationFilter.test.tsx | 9 +- .../Recordings/Filters/LabelFilter.test.tsx | 20 ++- .../Recordings/Filters/NameFilter.test.tsx | 9 +- .../Filters/RecordingStateFilter.test.tsx | 9 +- src/test/Recordings/RecordingFilters.test.tsx | 14 +- .../Recordings/RecordingLabelsPanel.test.tsx | 9 +- src/test/Recordings/Recordings.test.tsx | 5 +- src/test/Rules/CreateRule.test.tsx | 5 +- .../Credentials/StoreCredentials.test.tsx | 18 ++- src/test/TargetView/TargetSelect.test.tsx | 12 +- 62 files changed, 541 insertions(+), 321 deletions(-) delete mode 100644 src/app/RecordingMetadata/types.tsx diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 32a7a16b8..a52104a5e 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -114,6 +114,11 @@ export const AllTargetsArchivedRecordingsTable: React.FC = ({ onClose, const [uploading, setUploading] = React.useState(false); const [numOfFiles, setNumOfFiles] = React.useState(0); const [allOks, setAllOks] = React.useState(false); - const [labels, setLabels] = React.useState([] as RecordingLabel[]); + const [labels, setLabels] = React.useState([] as KeyValue[]); const [valid, setValid] = React.useState(ValidatedOptions.success); const getFormattedLabels = React.useCallback(() => { @@ -66,7 +66,7 @@ export const ArchiveUploadModal: React.FC = ({ onClose, const reset = React.useCallback(() => { setUploading(false); - setLabels([] as RecordingLabel[]); + setLabels([] as KeyValue[]); setValid(ValidatedOptions.success); setNumOfFiles(0); }, [setUploading, setLabels, setValid, setNumOfFiles]); diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index dfd9d9e23..b81e6954a 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -36,6 +36,11 @@ import { AllTargetsArchivedRecordingsTable } from './AllTargetsArchivedRecording export const uploadAsTarget: Target = { connectUrl: UPLOADS_SUBDIRECTORY, alias: '', + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; enum ArchiveTab { diff --git a/src/app/Archives/utils.tsx b/src/app/Archives/utils.tsx index 377347f90..b69d72bf6 100644 --- a/src/app/Archives/utils.tsx +++ b/src/app/Archives/utils.tsx @@ -34,5 +34,10 @@ export const getTargetFromDirectory = (dir: RecordingDirectory): Target => { return { connectUrl: dir.connectUrl, alias: dir.jvmId, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; }; diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index 6b93cbc65..ee34aa0ea 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -17,10 +17,15 @@ import { DurationPicker } from '@app/DurationPicker/DurationPicker'; import { ErrorView } from '@app/ErrorView/ErrorView'; import { authFailMessage, isAuthFail } from '@app/ErrorView/types'; import { RecordingLabelFields } from '@app/RecordingMetadata/RecordingLabelFields'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { SelectTemplateSelectorForm } from '@app/Shared/Components/SelectTemplateSelectorForm'; import { LoadingProps } from '@app/Shared/Components/types'; -import { EventTemplate, RecordingAttributes, AdvancedRecordingOptions, Target } from '@app/Shared/Services/api.types'; +import { + EventTemplate, + RecordingAttributes, + AdvancedRecordingOptions, + Target, + KeyValue, +} from '@app/Shared/Services/api.types'; import { isTargetAgentHttp } from '@app/Shared/Services/api.utils'; import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; import { ServiceContext } from '@app/Shared/Services/Services'; @@ -150,18 +155,6 @@ export const CustomRecordingForm: React.FC = () => { return str; }, [formData]); - const getFormattedLabels = React.useCallback(() => { - const obj = {}; - - formData.labels.forEach((l) => { - if (l.key && l.value) { - obj[l.key] = l.value; - } - }); - - return obj; - }, [formData]); - const handleRecordingNameChange = React.useCallback( (name: string) => setFormData((old) => ({ @@ -198,7 +191,7 @@ export const CustomRecordingForm: React.FC = () => { ); const handleLabelsChange = React.useCallback( - (labels: RecordingLabel[]) => { + (labels: KeyValue[]) => { setFormData((old) => ({ ...old, labels })); }, [setFormData], @@ -266,10 +259,10 @@ export const CustomRecordingForm: React.FC = () => { maxAge: toDisk ? (continuous ? maxAge * maxAgeUnit : undefined) : undefined, maxSize: toDisk ? maxSize * maxSizeUnit : undefined, }, - metadata: { labels: getFormattedLabels() }, + metadata: { labels: formData.labels }, }; handleCreateRecording(recordingAttributes); - }, [eventSpecifierString, getFormattedLabels, formData, notifications, handleCreateRecording]); + }, [eventSpecifierString, formData, notifications, handleCreateRecording]); const refreshFormOptions = React.useCallback( (target: Target) => { diff --git a/src/app/CreateRecording/types.ts b/src/app/CreateRecording/types.ts index 1e5766949..e11288c17 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RecordingLabel } from '@app/RecordingMetadata/types'; -import { EventTemplate } from '@app/Shared/Services/api.types'; +import { EventTemplate, KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; export type EventTemplateIdentifier = Pick; @@ -22,7 +21,7 @@ export type EventTemplateIdentifier = Pick; interface _FormBaseData { name: string; template?: EventTemplateIdentifier; - labels: RecordingLabel[]; + labels: KeyValue[]; continuous: boolean; archiveOnStop: boolean; restart: boolean; diff --git a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx index 151a9398f..142831c4b 100644 --- a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx +++ b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartController.tsx @@ -147,6 +147,9 @@ export class MBeanMetricsChartController { l += '}'; q.push(l); }); + if (q.length === 0) { + return of({}); + } return this._api.getTargetMBeanMetrics(target, q); } } diff --git a/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx b/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx index 0adc1ebc4..1bd73c01c 100644 --- a/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx +++ b/src/app/Dashboard/JvmDetails/JvmDetailsCard.tsx @@ -48,7 +48,7 @@ export const JvmDetailsCard: DashboardCardFC = (props) => { name: target.alias, target, nodeType: NodeType.JVM, - labels: {}, + labels: [], }), }; }, [target]); diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index e47f92a3f..b88a78a73 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -24,6 +24,7 @@ import { UPLOADS_SUBDIRECTORY, NotificationCategory, Target, + KeyValue, } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; @@ -33,8 +34,7 @@ import { HelpIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { combineLatest, concatMap, filter, first, forkJoin, map, Observable, of } from 'rxjs'; import { RecordingLabelFields } from './RecordingLabelFields'; -import { RecordingLabel } from './types'; -import { includesLabel, parseLabels } from './utils'; +import { includesLabel } from './utils'; export interface BulkEditLabelsProps { isTargetRecording: boolean; @@ -54,8 +54,8 @@ export const BulkEditLabels: React.FC = ({ const context = React.useContext(ServiceContext); const [recordings, setRecordings] = React.useState([] as Recording[]); const [editing, setEditing] = React.useState(false); - const [commonLabels, setCommonLabels] = React.useState([] as RecordingLabel[]); - const [savedCommonLabels, setSavedCommonLabels] = React.useState([] as RecordingLabel[]); + const [commonLabels, setCommonLabels] = React.useState([] as KeyValue[]); + const [savedCommonLabels, setSavedCommonLabels] = React.useState([] as KeyValue[]); const [valid, setValid] = React.useState(ValidatedOptions.default); const [loading, setLoading] = React.useState(false); const addSubscription = useSubscriptions(); @@ -78,7 +78,7 @@ export const BulkEditLabels: React.FC = ({ recordings.forEach((r: Recording) => { const idx = getIdxFromRecording(r); if (checkedIndices.includes(idx)) { - let updatedLabels = [...parseLabels(r.metadata.labels), ...commonLabels]; + let updatedLabels = [...r.metadata.labels, ...commonLabels]; updatedLabels = updatedLabels.filter((label) => { return !includesLabel(toDelete, label); }); @@ -124,13 +124,13 @@ export const BulkEditLabels: React.FC = ({ }, [setEditing, setCommonLabels, savedCommonLabels]); const updateCommonLabels = React.useCallback( - (setLabels: (l: RecordingLabel[]) => void) => { - const allRecordingLabels = [] as RecordingLabel[][]; + (setLabels: (l: KeyValue[]) => void) => { + const allRecordingLabels = [] as KeyValue[][]; recordings.forEach((r: Recording) => { const idx = getIdxFromRecording(r); if (checkedIndices.includes(idx)) { - allRecordingLabels.push(parseLabels(r.metadata.labels)); + allRecordingLabels.push(r.metadata.labels); } }); diff --git a/src/app/RecordingMetadata/ClickableLabel.tsx b/src/app/RecordingMetadata/ClickableLabel.tsx index f49e4210f..72113c72f 100644 --- a/src/app/RecordingMetadata/ClickableLabel.tsx +++ b/src/app/RecordingMetadata/ClickableLabel.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { Label } from '@patternfly/react-core'; import * as React from 'react'; -import { RecordingLabel } from './types'; export interface ClickableLabelCellProps { - label: RecordingLabel; + label: KeyValue; isSelected: boolean; - onLabelClick: (label: RecordingLabel) => void; + onLabelClick: (label: KeyValue) => void; } export const ClickableLabel: React.FC = ({ label, isSelected, onLabelClick }) => { diff --git a/src/app/RecordingMetadata/LabelCell.tsx b/src/app/RecordingMetadata/LabelCell.tsx index dab08476d..7e5a2a42a 100644 --- a/src/app/RecordingMetadata/LabelCell.tsx +++ b/src/app/RecordingMetadata/LabelCell.tsx @@ -15,15 +15,15 @@ */ import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { Label, Text } from '@patternfly/react-core'; import * as React from 'react'; import { ClickableLabel } from './ClickableLabel'; -import { RecordingLabel } from './types'; import { getLabelDisplay } from './utils'; export interface LabelCellProps { target: string; - labels: RecordingLabel[]; + labels: KeyValue[]; // If undefined, labels are not clickable (i.e. display only) and only displayed in grey. clickableOptions?: { labelFilters: string[]; @@ -33,7 +33,7 @@ export interface LabelCellProps { export const LabelCell: React.FC = ({ target, labels, clickableOptions }) => { const isLabelSelected = React.useCallback( - (label: RecordingLabel) => { + (label: KeyValue) => { if (clickableOptions) { const labelFilterSet = new Set(clickableOptions.labelFilters); return labelFilterSet.has(getLabelDisplay(label)); @@ -44,11 +44,11 @@ export const LabelCell: React.FC = ({ target, labels, clickableO ); const getLabelColor = React.useCallback( - (label: RecordingLabel) => (isLabelSelected(label) ? 'blue' : 'grey'), + (label: KeyValue) => (isLabelSelected(label) ? 'blue' : 'grey'), [isLabelSelected], ); const onLabelSelectToggle = React.useCallback( - (clickedLabel: RecordingLabel) => { + (clickedLabel: KeyValue) => { if (clickableOptions) { clickableOptions.updateFilters(target, { filterKey: 'Label', diff --git a/src/app/RecordingMetadata/RecordingLabelFields.tsx b/src/app/RecordingMetadata/RecordingLabelFields.tsx index f891bb238..bb241e0c5 100644 --- a/src/app/RecordingMetadata/RecordingLabelFields.tsx +++ b/src/app/RecordingMetadata/RecordingLabelFields.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ import { LoadingView } from '@app/Shared/Components/LoadingView'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { portalRoot } from '@app/utils/utils'; import { @@ -34,12 +35,11 @@ import { CloseIcon, ExclamationCircleIcon, FileIcon, PlusCircleIcon, UploadIcon import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { catchError, Observable, of, zip } from 'rxjs'; -import { RecordingLabel } from './types'; import { matchesLabelSyntax, getValidatedOption, LabelPattern, parseLabelsFromFile } from './utils'; export interface RecordingLabelFieldsProps { - labels: RecordingLabel[]; - setLabels: (labels: RecordingLabel[]) => void; + labels: KeyValue[]; + setLabels: (labels: KeyValue[]) => void; setValid: (isValid: ValidatedOptions) => void; isUploadable?: boolean; isDisabled?: boolean; @@ -78,7 +78,7 @@ export const RecordingLabelFields: React.FC = ({ ); const handleAddLabelButtonClick = React.useCallback(() => { - setLabels([...labels, { key: '', value: '' } as RecordingLabel]); + setLabels([...labels, { key: '', value: '' } as KeyValue]); }, [labels, setLabels]); const handleDeleteLabelButtonClick = React.useCallback( @@ -91,7 +91,7 @@ export const RecordingLabelFields: React.FC = ({ ); const isDuplicateKey = React.useCallback( - (key: string, labels: RecordingLabel[]) => labels.filter((label) => label.key === key).length > 1, + (key: string, labels: KeyValue[]) => labels.filter((label) => label.key === key).length > 1, [], ); @@ -127,7 +127,7 @@ export const RecordingLabelFields: React.FC = ({ (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length) { - const tasks: Observable[] = []; + const tasks: Observable[] = []; setLoading(true); for (const labelFile of Array.from(files)) { tasks.push( @@ -140,7 +140,7 @@ export const RecordingLabelFields: React.FC = ({ ); } addSubscription( - zip(tasks).subscribe((labelArrays: RecordingLabel[][]) => { + zip(tasks).subscribe((labelArrays: KeyValue[][]) => { setLoading(false); const newLabels = labelArrays.reduce((acc, next) => acc.concat(next), []); setLabels([...labels, ...newLabels]); diff --git a/src/app/RecordingMetadata/types.tsx b/src/app/RecordingMetadata/types.tsx deleted file mode 100644 index 810222ff1..000000000 --- a/src/app/RecordingMetadata/types.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright The Cryostat Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface RecordingLabel { - key: string; - value: string; -} diff --git a/src/app/RecordingMetadata/utils.ts b/src/app/RecordingMetadata/utils.ts index a5b8c3b17..d66f8cf2b 100644 --- a/src/app/RecordingMetadata/utils.ts +++ b/src/app/RecordingMetadata/utils.ts @@ -14,33 +14,25 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; import { Observable, from } from 'rxjs'; -import { RecordingLabel } from './types'; -export const parseLabels = (jsonLabels?: { [key: string]: string }) => { - if (!jsonLabels) return []; - - return Object.entries(jsonLabels).map(([k, v]) => { - return { key: k, value: v } as RecordingLabel; - }); -}; - -export const isEqualLabel = (a: RecordingLabel, b: RecordingLabel) => { +export const isEqualLabel = (a: KeyValue, b: KeyValue) => { return a.key === b.key && a.value === b.value; }; -export const includesLabel = (arr: RecordingLabel[], searchLabel: RecordingLabel) => { +export const includesLabel = (arr: KeyValue[], searchLabel: KeyValue) => { return arr.some((l) => isEqualLabel(searchLabel, l)); }; -export const parseLabelsFromFile = (file: File): Observable => { +export const parseLabelsFromFile = (file: File): Observable => { return from( file .text() .then(JSON.parse) .then((obj) => { - const labels: RecordingLabel[] = []; + const labels: KeyValue[] = []; const labelObj = obj['labels']; if (labelObj) { Object.keys(labelObj).forEach((key) => { @@ -56,7 +48,7 @@ export const parseLabelsFromFile = (file: File): Observable => ); }; -export const getLabelDisplay = (label: RecordingLabel) => `${label.key}:${label.value}`; +export const getLabelDisplay = (label: KeyValue) => `${label.key}:${label.value}`; export const LabelPattern = /^\S+$/; @@ -64,6 +56,6 @@ export const getValidatedOption = (isValid: boolean) => { return isValid ? ValidatedOptions.success : ValidatedOptions.error; }; -export const matchesLabelSyntax = (l: RecordingLabel) => { +export const matchesLabelSyntax = (l: KeyValue) => { return l && LabelPattern.test(l.key) && LabelPattern.test(l.value); }; diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index eb473920e..5a6369d47 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -20,7 +20,6 @@ import { } from '@app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel'; import { authFailMessage } from '@app/ErrorView/types'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { parseLabels } from '@app/RecordingMetadata/utils'; import { LoadingProps } from '@app/Shared/Components/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; import { emptyActiveRecordingFilters, TargetRecordingFilters } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; @@ -861,10 +860,6 @@ export const ActiveRecordingRow: React.FC = ({ return expandedRows.includes(expandedRowId); }, [expandedRowId, expandedRows]); - const parsedLabels = React.useMemo(() => { - return parseLabels(recording.metadata.labels); - }, [recording]); - const handleToggle = React.useCallback(() => toggleExpanded(expandedRowId), [expandedRowId, toggleExpanded]); const handleCheck = React.useCallback( @@ -974,7 +969,7 @@ export const ActiveRecordingRow: React.FC = ({ updateFilters: updateFilters, labelFilters: labelFilters, }} - labels={parsedLabels} + labels={recording.metadata.labels} /> = ({ recording, labelFilters, currentSelectedTargetURL, - parsedLabels, context.api, handleCheck, handleToggle, diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index c2f9a470d..3d23450f8 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -21,7 +21,6 @@ import { } from '@app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { parseLabels } from '@app/RecordingMetadata/utils'; import { LoadingProps } from '@app/Shared/Components/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; import { emptyArchivedRecordingFilters, TargetRecordingFilters } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; @@ -772,10 +771,6 @@ export const ArchivedRecordingRow: React.FC = ({ const [loadingAnalysis, setLoadingAnalysis] = React.useState(false); const [analyses, setAnalyses] = React.useState([]); - const parsedLabels = React.useMemo(() => { - return parseLabels(recording.metadata.labels); - }, [recording]); - const expandedRowId = React.useMemo(() => `archived-table-row-${index}-exp`, [index]); const handleToggle = React.useCallback(() => { @@ -849,7 +844,7 @@ export const ArchivedRecordingRow: React.FC = ({ updateFilters: updateFilters, labelFilters: labelFilters, }} - labels={parsedLabels} + labels={recording.metadata.labels} /> @@ -874,7 +869,6 @@ export const ArchivedRecordingRow: React.FC = ({ index, checkedIndices, isExpanded, - parsedLabels, labelFilters, currentSelectedTargetURL, sourceTarget, diff --git a/src/app/Recordings/Filters/LabelFilter.tsx b/src/app/Recordings/Filters/LabelFilter.tsx index 2db3c30bc..67fc3a7e2 100644 --- a/src/app/Recordings/Filters/LabelFilter.tsx +++ b/src/app/Recordings/Filters/LabelFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { parseLabels, getLabelDisplay } from '@app/RecordingMetadata/utils'; +import { getLabelDisplay } from '@app/RecordingMetadata/utils'; import { Recording } from '@app/Shared/Services/api.types'; import { Label, Select, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; @@ -42,7 +42,7 @@ export const LabelFilter: React.FC = ({ recordings, filteredLa const labels = new Set(); recordings.forEach((r) => { if (!r || !r.metadata || !r.metadata.labels) return; - parseLabels(r.metadata.labels).map((label) => labels.add(getLabelDisplay(label))); + r.metadata.labels.map((label) => labels.add(getLabelDisplay(label))); }); return Array.from(labels) .filter((l) => !filteredLabels.includes(l)) diff --git a/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx b/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx index 5efff8e5b..d219fbab2 100644 --- a/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx +++ b/src/app/Shared/Components/MatchExpression/MatchExpressionHint.tsx @@ -15,6 +15,7 @@ */ import { Target } from '@app/Shared/Services/api.types'; +import { getAnnotation } from '@app/utils/utils'; import { ClipboardCopyButton, CodeBlock, CodeBlockAction, CodeBlockCode } from '@patternfly/react-core'; import * as React from 'react'; @@ -30,7 +31,10 @@ export const MatchExpressionHint: React.FC = ({ target if (!target || !target.alias || !target.connectUrl) { body = 'true'; } else { - body = `target.alias == '${target.alias}' || target.annotations.cryostat['PORT'] == ${target.annotations?.cryostat['PORT']}`; + body = `target.alias == '${target.alias}' || target.annotations.cryostat['PORT'] == ${getAnnotation( + target.annotations.cryostat, + 'PORT', + )}`; } body = JSON.stringify(body, null, 2); body = body.substring(1, body.length - 1); diff --git a/src/app/Shared/Components/MatchExpression/utils.tsx b/src/app/Shared/Components/MatchExpression/utils.tsx index 0ff572697..e1590757a 100644 --- a/src/app/Shared/Components/MatchExpression/utils.tsx +++ b/src/app/Shared/Components/MatchExpression/utils.tsx @@ -72,7 +72,7 @@ export const createTargetNode = (target: Target): TargetNode => { id: hashCode(JSON.stringify(target)), name: target.connectUrl, nodeType: NodeType.TARGET, - labels: {}, + labels: [], target: target, }; }; diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 0714a8e2f..2bded2658 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -15,7 +15,6 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { LayoutTemplate, SerialLayoutTemplate } from '@app/Dashboard/types'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { createBlobURL } from '@app/utils/utils'; import { ValidatedOptions } from '@patternfly/react-core'; import { EMPTY, forkJoin, from, Observable, ObservableInput, of, ReplaySubject, shareReplay, throwError } from 'rxjs'; @@ -59,6 +58,8 @@ import { XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, + KeyValue, + CustomTargetStub, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -170,7 +171,7 @@ export class ApiService { } createTarget( - target: Target, + target: CustomTargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -522,7 +523,7 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: RecordingLabel[]) { + transformAndStringifyToRawLabels(labels: KeyValue[]) { const rawLabels = {}; for (const label of labels) { rawLabels[label.key] = label.value; @@ -530,7 +531,7 @@ export class ApiService { return JSON.stringify(rawLabels); } - postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: RecordingLabel[]): Observable { + postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: KeyValue[]): Observable { return this.sendRequest( 'beta', `fs/recordings/${encodeURIComponent(jvmId)}/${encodeURIComponent(recordingName)}/metadata/labels`, @@ -888,7 +889,7 @@ export class ApiService { ); } - postRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.target.target().pipe( filter((target: Target) => !!target), first(), @@ -917,7 +918,7 @@ export class ApiService { ); } - postUploadedRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postUploadedRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.graphql( ` query PostUploadedRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String){ @@ -935,7 +936,7 @@ export class ApiService { ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); } - postTargetRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postTargetRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.target.target().pipe( filter((target) => !!target), first(), @@ -1206,8 +1207,10 @@ export class ApiService { ` query MBeanMXMetricsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - mbeanMetrics { - ${queries.join('\n')} + target { + mbeanMetrics { + ${queries.join('\n')} + } } } }`, @@ -1218,7 +1221,7 @@ export class ApiService { if (!nodes || nodes.length === 0) { return {}; } - return nodes[0]?.mbeanMetrics; + return nodes[0]?.target.mbeanMetrics; }), catchError((_) => of({})), ); @@ -1306,7 +1309,7 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: RecordingLabel[]): string { + stringifyRecordingLabels(labels: KeyValue[]): string { return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); } diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 86cd9f6d7..2bd3b0ba3 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -23,12 +23,13 @@ export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'beta // Common Resources // ====================================== export interface KeyValue { - readonly [key: string]: string; + key: string; + value: string; } export interface Metadata { - labels: KeyValue; - annotations?: KeyValue; + labels: KeyValue[]; + annotations?: KeyValue[]; } export interface ApiV2Response { @@ -87,6 +88,8 @@ export class XMLHttpError extends Error { } } +export type CustomTargetStub = Omit; + // ====================================== // Health Resources // ====================================== @@ -122,7 +125,7 @@ export interface AuthV2Response extends ApiV2Response { // ====================================== // MBean metric resources // ====================================== -export interface MemoryUsage { +export interface MemoryUtilization { init: number; used: number; committed: number; @@ -147,8 +150,8 @@ export interface MBeanMetrics { totalSwapSpaceSize?: number; }; memory?: { - heapMemoryUsage?: MemoryUsage; - nonHeapMemoryUsage?: MemoryUsage; + heapMemoryUsage?: MemoryUtilization; + nonHeapMemoryUsage?: MemoryUtilization; heapMemoryUsagePercent?: number; }; runtime?: { @@ -161,7 +164,7 @@ export interface MBeanMetrics { specName?: string; specVendor?: string; startTime?: number; - systemProperties?: object; + systemProperties?: KeyValue[]; uptime?: number; vmName?: string; vmVendor?: string; @@ -173,7 +176,9 @@ export interface MBeanMetrics { export interface MBeanMetricsResponse { data: { targetNodes: { - mbeanMetrics: MBeanMetrics; + target: { + mbeanMetrics: MBeanMetrics; + }; }[]; }; } @@ -444,10 +449,10 @@ export interface Target { jvmId?: string; // present in responses, but we do not need to provide it in requests connectUrl: string; alias: string; - labels?: KeyValue; - annotations?: { - cryostat: KeyValue; - platform: KeyValue; + labels: KeyValue[]; + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; }; } @@ -483,7 +488,7 @@ interface _AbstractNode { readonly id: number; readonly name: string; readonly nodeType: NodeType; - readonly labels: KeyValue; + readonly labels: KeyValue[]; } export interface EnvironmentNode extends _AbstractNode { diff --git a/src/app/Shared/Services/api.utils.ts b/src/app/Shared/Services/api.utils.ts index 5eb3caf73..af376af02 100644 --- a/src/app/Shared/Services/api.utils.ts +++ b/src/app/Shared/Services/api.utils.ts @@ -112,7 +112,7 @@ export const DEFAULT_EMPTY_UNIVERSE: EnvironmentNode = { id: 0, name: 'Universe', nodeType: NodeType.UNIVERSE, - labels: {}, + labels: [], children: [], }; @@ -140,7 +140,7 @@ export const getTargetRepresentation = (t: Target) => export const isTargetAgentHttp = (t: Target) => t.connectUrl.startsWith('http'); export const isTargetNode = (node: EnvironmentNode | TargetNode): node is TargetNode => { - return node['target'] !== undefined && node['children'] === undefined; + return node['target'] !== undefined; }; export const getAllLeaves = (root: EnvironmentNode | TargetNode): TargetNode[] => { diff --git a/src/app/Shared/Services/service.utils.ts b/src/app/Shared/Services/service.utils.ts index a43f0171d..40fa88ce1 100644 --- a/src/app/Shared/Services/service.utils.ts +++ b/src/app/Shared/Services/service.utils.ts @@ -48,9 +48,12 @@ export const automatedAnalysisConfigToRecordingAttributes = ( maxSize: config.maxSize, }, metadata: { - labels: { - origin: automatedAnalysisRecordingName, - }, + labels: [ + { + key: 'origin', + value: automatedAnalysisRecordingName, + }, + ], }, }; }; diff --git a/src/app/TargetView/TargetContextSelector.tsx b/src/app/TargetView/TargetContextSelector.tsx index 4ab01089c..b27ac69a6 100644 --- a/src/app/TargetView/TargetContextSelector.tsx +++ b/src/app/TargetView/TargetContextSelector.tsx @@ -19,6 +19,7 @@ import { isEqualTarget, getTargetRepresentation } from '@app/Shared/Services/api import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getFromLocalStorage, removeFromLocalStorage, saveToLocalStorage } from '@app/utils/LocalStorage'; +import { getAnnotation } from '@app/utils/utils'; import { Button, Divider, Select, SelectGroup, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; import { Link } from 'react-router-dom'; @@ -113,13 +114,13 @@ export const TargetContextSelector: React.FC = ({ cl const favSet = new Set(favorites); const groupNames = new Set(); - targets.forEach((t) => groupNames.add(t.annotations?.cryostat['REALM'] || 'Others')); + targets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others')); const options = Array.from(groupNames) .map((name) => ( {targets - .filter((t) => (t.annotations?.cryostat['REALM'] || 'Others') === name) + .filter((t) => getAnnotation(t.annotations.cryostat, 'REALM') === name) .map((t: Target) => ( = ({ onSelect, simple, .. let options = [] as JSX.Element[]; const groupNames = new Set(); - targets.forEach((t) => groupNames.add(t.annotations?.cryostat['REALM'] || 'Others')); + targets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others')); options = options.concat( Array.from(groupNames) .map((name) => ( {targets - .filter((t) => (t.annotations?.cryostat['REALM'] || 'Others') === name) + .filter((t) => (getAnnotation(t.annotations.cryostat, 'REALM') || 'Others') === name) .map((t: Target) => ( {!t.alias || t.alias === t.connectUrl ? `${t.connectUrl}` : `${t.alias} (${t.connectUrl})`} diff --git a/src/app/Topology/Actions/CreateTarget.tsx b/src/app/Topology/Actions/CreateTarget.tsx index e4e8645e0..cc62f41fd 100644 --- a/src/app/Topology/Actions/CreateTarget.tsx +++ b/src/app/Topology/Actions/CreateTarget.tsx @@ -25,7 +25,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import '@app/Topology/styles/base.css'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getFromLocalStorage } from '@app/utils/LocalStorage'; -import { portalRoot } from '@app/utils/utils'; +import { getAnnotation, portalRoot } from '@app/utils/utils'; import { Accordion, AccordionContent, @@ -241,7 +241,7 @@ export const CreateTarget: React.FC = ({ prefilled }) => { React.useEffect(() => { addSubscription( context.targets.targets().subscribe((ts) => { - const discoveredTargets = ts.filter((t) => t.annotations?.cryostat['REALM'] !== 'Custom Targets'); + const discoveredTargets = ts.filter((t) => getAnnotation(t.annotations.cryostat, 'REALM') !== 'Custom Targets'); if (discoveredTargets.length) { setExample(discoveredTargets[0].connectUrl); } @@ -400,7 +400,12 @@ export const CreateTarget: React.FC = ({ prefilled }) => { - + diff --git a/src/app/Topology/Entity/EntityAnnotations.tsx b/src/app/Topology/Entity/EntityAnnotations.tsx index 916762317..416804732 100644 --- a/src/app/Topology/Entity/EntityAnnotations.tsx +++ b/src/app/Topology/Entity/EntityAnnotations.tsx @@ -16,8 +16,9 @@ import { Label, LabelGroup } from '@patternfly/react-core'; import * as React from 'react'; import { EmptyText } from '../../Shared/Components/EmptyText'; +import { Annotations } from './types'; -export const EntityAnnotations: React.FC<{ annotations?: object; maxDisplay?: number }> = ({ +export const EntityAnnotations: React.FC<{ annotations?: Annotations; maxDisplay?: number }> = ({ annotations, maxDisplay, ...props @@ -26,7 +27,7 @@ export const EntityAnnotations: React.FC<{ annotations?: object; maxDisplay?: nu return annotations ? Object.keys(annotations).map((groupK) => ({ groupLabel: groupK, - annotations: Object.keys(annotations[groupK]).map((k) => `${k}=${annotations[groupK][k]}`), + annotations: annotations[groupK].map((kv) => `${kv.key}=${kv.value}`), })) : []; }, [annotations]); diff --git a/src/app/Topology/Entity/EntityDetails.tsx b/src/app/Topology/Entity/EntityDetails.tsx index 8fd419964..01814c76e 100644 --- a/src/app/Topology/Entity/EntityDetails.tsx +++ b/src/app/Topology/Entity/EntityDetails.tsx @@ -271,23 +271,28 @@ const MBeanDetails: React.FC<{ ` query MBeanMXMetricsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - mbeanMetrics { - runtime { - startTime - vmVendor - vmVersion - classPath - libraryPath - inputArguments - systemProperties - } - os { - name - version - arch - availableProcessors - totalPhysicalMemorySize - totalSwapSpaceSize + target { + mbeanMetrics { + runtime { + startTime + vmVendor + vmVersion + classPath + libraryPath + inputArguments + systemProperties { + key + value + } + } + os { + name + version + arch + availableProcessors + totalPhysicalMemorySize + totalSwapSpaceSize + } } } } @@ -295,7 +300,7 @@ const MBeanDetails: React.FC<{ { connectUrl }, ) .pipe( - map((resp) => resp.data.targetNodes[0].mbeanMetrics || {}), + map((resp) => resp.data.targetNodes[0].target.mbeanMetrics || {}), catchError((_) => of({})), ) .subscribe(setMbeanMetrics), diff --git a/src/app/Topology/Entity/types.ts b/src/app/Topology/Entity/types.ts index 5dc1cea00..631c06436 100644 --- a/src/app/Topology/Entity/types.ts +++ b/src/app/Topology/Entity/types.ts @@ -17,6 +17,7 @@ import type { EventProbe, EventTemplate, EventType, + KeyValue, NotificationMessage, Recording, Rule, @@ -48,6 +49,11 @@ export const TargetOwnedResourceTypeAsArray = [ 'agentProbes', ] as const; +export type Annotations = { + cryostat: KeyValue[]; + platform: KeyValue[]; +}; + export const TargetRelatedResourceTypeAsArray = ['automatedRules', 'credentials'] as const; export type TargetOwnedResourceType = (typeof TargetOwnedResourceTypeAsArray)[number]; diff --git a/src/app/Topology/Entity/utils.tsx b/src/app/Topology/Entity/utils.tsx index 4df3a52ff..9e25102a1 100644 --- a/src/app/Topology/Entity/utils.tsx +++ b/src/app/Topology/Entity/utils.tsx @@ -53,7 +53,8 @@ import { import { ActiveRecDetail, Nothing } from './ResourceDetails'; import { DescriptionConfig, TargetOwnedResourceType, TargetRelatedResourceType, ResourceTypes, PatchFn } from './types'; -export const keyValueEntryTransformer = (kv: object): string[] => Object.entries(kv).map(([k, v]) => `${k}=${v}`); +export const keyValueEntryTransformer = (kv: { key: string; value: string }[]): string[] => + kv.map((k) => `${k.key}=${k.value}`); export const valuesEntryTransformer: (kv: string[] | object) => string[] = Object.values; diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index f1d5544fd..0075d0fa0 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -49,19 +49,40 @@ export const fakeTarget: Target = { jvmId: 'rpZeYNB9wM_TEnXoJvAFuR0jdcUBXZgvkXiKhjQGFvY=', connectUrl: 'service:jmx:rmi:///jndi/rmi://10-128-2-25.my-namespace.pod:9097/jmxrmi', alias: 'quarkus-test-77f556586c-25bkv', - labels: { - 'pod-template-hash': '77f556586c', - deployment: 'quarkus-test', - }, - annotations: { - cryostat: { - HOST: '10.128.2.25', - PORT: '9097', - POD_NAME: 'quarkus-test-77f556586c-25bkv', - REALM: 'KubernetesApi', - NAMESPACE: 'my-namespace', + labels: [ + { + key: 'pod-template-hash', + value: '77f556586c', }, - platform: {}, + { + key: 'deployment', + value: 'quarkus-test', + }, + ], + annotations: { + cryostat: [ + { + key: 'HOST', + value: '10.128.2.25', + }, + { + key: 'PORT', + value: '9097', + }, + { + key: 'POD_NAME', + value: 'quarkus-test-77f556586c-25bkv', + }, + { + key: 'REALM', + value: 'KubernetesApi', + }, + { + key: 'NAMESPACE', + value: 'my-namespace', + }, + ], + platform: [], }, }; @@ -72,11 +93,20 @@ export const fakeAARecording: ActiveRecording = { reportUrl: 'https://clustercryostat-sample-default.apps.ci-ln-25fg5f2-76ef8.origin-ci-int-aws.dev.rhcloud.com:443/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2F10-128-2-27.my-namespace.pod:9097%2Fjmxrmi/reports/automated-analysis', metadata: { - labels: { - 'template.name': 'Profiling', - 'template.type': 'TARGET', - origin: 'automated-analysis', - }, + labels: [ + { + key: 'template.name', + value: 'Profiling', + }, + { + key: 'template.type', + value: 'TARGET', + }, + { + key: 'origin', + value: 'automated-analysis', + }, + ], }, startTime: 1680732807, id: 0, diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index a684c48fe..dd80a8811 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { KeyValue } from '@app/Shared/Services/api.types'; import { ISortBy, SortByDirection } from '@patternfly/react-table'; import _ from 'lodash'; import { NavigateFunction } from 'react-router-dom'; @@ -181,6 +182,18 @@ export interface TableColumn { width?: number; } +export const getAnnotation = (kv: KeyValue[], key: string, def?: string): string | undefined => { + if (!kv) { + return def; + } + for (const k of kv) { + if (k.key === key) { + return k.value; + } + } + return def; +}; + const mapper = (tableColumns: TableColumn[], index?: number) => { if (index === undefined) { return undefined; diff --git a/src/mirage/factories.ts b/src/mirage/factories.ts index 687ab86a5..939791695 100644 --- a/src/mirage/factories.ts +++ b/src/mirage/factories.ts @@ -22,8 +22,22 @@ export const targetFactory: FactoryDefinition = Factory.extend({ connectUrl: 'http://fake-target.local:1234', jvmId: '1234', annotations: { - platform: { 'io.cryostat.demo': 'this-is-not-real' }, - cryostat: { hello: 'world', REALM: 'Some Realm' }, + platform: [ + { + key: 'io.cryostat.demo', + value: 'this-is-not-real', + }, + ], + cryostat: [ + { + key: 'hello', + value: 'world', + }, + { + key: 'REALM', + value: 'Some Realm', + }, + ], }, }); diff --git a/src/mirage/index.ts b/src/mirage/index.ts index 5de3574e1..d42a8b0f1 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -101,10 +101,13 @@ export const startMirage = ({ environment = 'development' } = {}) => { alias: attrs.get('alias'), connectUrl: attrs.get('connectUrl'), annotations: { - platform: {}, - cryostat: { - REALM: 'Custom Targets', - }, + platform: [], + cryostat: [ + { + key: 'REALM', + value: 'Custom Targets', + }, + ], }, }); websocket.send( @@ -135,11 +138,11 @@ export const startMirage = ({ environment = 'development' } = {}) => { result: { name: 'Universe', nodeType: 'Universe', - labels: {}, + labels: [], children: realmTypes.map((r: string) => ({ name: r, nodeType: 'Realm', - labels: {}, + labels: [], id: r, children: models .filter((t) => t.annotations.cryostat['REALM'] === r) @@ -211,11 +214,17 @@ export const startMirage = ({ environment = 'development' } = {}) => { maxSize: attrs.get('maxSize') || 0, maxAge: attrs.get('maxAge') || 0, metadata: { - labels: { - ...(attrs.labels || {}), - 'template.type': 'TARGET', - 'template.name': 'Demo_Template', - }, + labels: [ + ...(attrs.labels || []), + { + key: 'template.type', + value: 'TARGET', + }, + { + key: 'template.name', + value: 'Demo_Template', + }, + ], }, }); websocket.send( @@ -630,52 +639,54 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - mbeanMetrics: { - thread: { - threadCount: Math.ceil(Math.random() * 5), - daemonThreadCount: Math.ceil(Math.random() * 5), - }, - os: { - arch: 'x86_64', - availableProcessors: Math.ceil(Math.random() * 8), - version: '10.0.1', - systemCpuLoad: Math.random(), - systemLoadAverage: Math.random(), - processCpuLoad: Math.random(), - totalPhysicalMemorySize: Math.ceil(Math.random() * 64), - freePhysicalMemorySize: Math.ceil(Math.random() * 64), - }, - memory: { - heapMemoryUsage: { - init: Math.ceil(Math.random() * 64), - used: Math.ceil(Math.random() * 64), - committed: Math.ceil(Math.random() * 64), - max: Math.ceil(Math.random() * 64), + target: { + mbeanMetrics: { + thread: { + threadCount: Math.ceil(Math.random() * 5), + daemonThreadCount: Math.ceil(Math.random() * 5), }, - nonHeapMemoryUsage: { - init: Math.ceil(Math.random() * 64), - used: Math.ceil(Math.random() * 64), - committed: Math.ceil(Math.random() * 64), - max: Math.ceil(Math.random() * 64), + os: { + arch: 'x86_64', + availableProcessors: Math.ceil(Math.random() * 8), + version: '10.0.1', + systemCpuLoad: Math.random(), + systemLoadAverage: Math.random(), + processCpuLoad: Math.random(), + totalPhysicalMemorySize: Math.ceil(Math.random() * 64), + freePhysicalMemorySize: Math.ceil(Math.random() * 64), + }, + memory: { + heapMemoryUsage: { + init: Math.ceil(Math.random() * 64), + used: Math.ceil(Math.random() * 64), + committed: Math.ceil(Math.random() * 64), + max: Math.ceil(Math.random() * 64), + }, + nonHeapMemoryUsage: { + init: Math.ceil(Math.random() * 64), + used: Math.ceil(Math.random() * 64), + committed: Math.ceil(Math.random() * 64), + max: Math.ceil(Math.random() * 64), + }, + heapMemoryUsagePercent: Math.random(), + }, + runtime: { + bootClassPath: '/path/to/boot/classpath', + classPath: '/path/to/classpath', + inputArguments: ['-Xmx1g', '-Djava.security.policy=...'], + libraryPath: '/path/to/library/path', + managementSpecVersion: '1.0', + name: 'Java Virtual Machine', + specName: 'Java Virtual Machine Specification', + specVendor: 'Oracle Corporation', + startTime: Date.now(), + // systemProperties: {...} + uptime: Date.now(), + vmName: 'Java HotSpot(TM) 64-Bit Server VM', + vmVendor: 'Oracle Corporation', + vmVersion: '25.131-b11', + bootClassPathSupported: true, }, - heapMemoryUsagePercent: Math.random(), - }, - runtime: { - bootClassPath: '/path/to/boot/classpath', - classPath: '/path/to/classpath', - inputArguments: ['-Xmx1g', '-Djava.security.policy=...'], - libraryPath: '/path/to/library/path', - managementSpecVersion: '1.0', - name: 'Java Virtual Machine', - specName: 'Java Virtual Machine Specification', - specVendor: 'Oracle Corporation', - startTime: Date.now(), - // systemProperties: {...} - uptime: Date.now(), - vmName: 'Java HotSpot(TM) 64-Bit Server VM', - vmVendor: 'Oracle Corporation', - vmVersion: '25.131-b11', - bootClassPathSupported: true, }, }, }, diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index eb4f9197c..b366d05f5 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -30,7 +30,13 @@ import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; diff --git a/src/test/Archives/AllArchivedRecordingsTable.test.tsx b/src/test/Archives/AllArchivedRecordingsTable.test.tsx index 5acbb2939..d6ddc0d33 100644 --- a/src/test/Archives/AllArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllArchivedRecordingsTable.test.tsx @@ -43,9 +43,12 @@ const mockRecordingDeletedNotification = { }, } as NotificationMessage; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ArchivedRecording = { name: 'someRecording', diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 1c12b36da..6af7d3070 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -26,6 +26,9 @@ const mockAlias1 = 'fooTarget1'; const mockTarget1: Target = { connectUrl: mockConnectUrl1, alias: mockAlias1, + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockConnectUrl2 = 'service:jmx:rmi://someUrl2'; const mockAlias2 = 'fooTarget2'; @@ -36,6 +39,9 @@ const mockNewAlias = 'newTarget'; const mockNewTarget: Target = { connectUrl: mockNewConnectUrl, alias: mockNewAlias, + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockCount1 = 1; const mockCount2 = 3; diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bb28193ba..bdc185501 100644 --- a/src/test/CreateRecording/CustomRecordingForm.test.tsx +++ b/src/test/CreateRecording/CustomRecordingForm.test.tsx @@ -15,7 +15,7 @@ */ import { CustomRecordingForm } from '@app/CreateRecording/CustomRecordingForm'; import { authFailMessage } from '@app/ErrorView/types'; -import { EventTemplate, AdvancedRecordingOptions, RecordingAttributes } from '@app/Shared/Services/api.types'; +import { EventTemplate, AdvancedRecordingOptions, RecordingAttributes, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, Services, defaultServices } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import { screen, cleanup, act as doAct } from '@testing-library/react'; @@ -36,7 +36,13 @@ jest.mock('react-router-dom', () => ({ })); const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockCustomEventTemplate: EventTemplate = { name: 'someEventTemplate', @@ -118,7 +124,7 @@ describe('', () => { maxSize: 0, toDisk: true, }, - metadata: { labels: {} }, + metadata: { labels: [] }, } as RecordingAttributes); expect(mockNavigate).toHaveBeenCalledWith('..', { relative: 'path' }); }); @@ -150,7 +156,7 @@ describe('', () => { it('should show error view if failing to retrieve templates or recording options', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/CreateRecording/SnapshotRecordingForm.test.tsx b/src/test/CreateRecording/SnapshotRecordingForm.test.tsx index 8b8914bfc..1c8b6cf4f 100644 --- a/src/test/CreateRecording/SnapshotRecordingForm.test.tsx +++ b/src/test/CreateRecording/SnapshotRecordingForm.test.tsx @@ -23,7 +23,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index e9dcd0c10..8f6d9a313 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -40,7 +40,13 @@ jest.mock('@app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCardList', () => { }; }); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockEmptyCachedReport: CachedReportValue = { report: [], @@ -136,7 +142,7 @@ const mockArchivedRecording: ArchivedRecording = { name: 'someArchivedRecording', downloadUrl: '', reportUrl: '', - metadata: { labels: {} }, + metadata: { labels: [] }, size: 0, archivedTime: 1663027200000, // 2022-09-13T00:00:00.000Z in milliseconds }; diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx index 76f56848c..fcfed6b1f 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.test.tsx @@ -22,7 +22,13 @@ import { cleanup, screen } from '@testing-library/react'; import { of } from 'rxjs'; import { render, testT } from '../../utils'; -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockTemplate1: EventTemplate = { name: 'template1', diff --git a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx index 5dec865e2..e0b8ef384 100644 --- a/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx +++ b/src/test/Dashboard/Charts/jfr/JFRMetricsChartCard.test.tsx @@ -29,7 +29,13 @@ import { mockMediaQueryList, render, renderSnapshot } from '../../../utils'; const mockDashboardUrl = 'http://localhost:3000'; jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of(mockDashboardUrl)); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.settings, 'themeSetting').mockReturnValue(of(ThemeSetting.LIGHT)); diff --git a/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx b/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx index 8a8acf9c7..fe55eb5f1 100644 --- a/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx +++ b/src/test/Dashboard/Charts/mbean/MBeanMetricsChartCard.test.tsx @@ -34,7 +34,13 @@ jest.spyOn(defaultServices.settings, 'datetimeFormat').mockReturnValue(of(defaul jest.spyOn(defaultServices.settings, 'themeSetting').mockReturnValue(of(ThemeSetting.DARK)); jest.spyOn(defaultServices.settings, 'media').mockReturnValue(of(mockMediaQueryList)); -const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockTarget = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); const mockJfrController = new JFRMetricsChartController( diff --git a/src/test/Dashboard/Dashboard.test.tsx b/src/test/Dashboard/Dashboard.test.tsx index 6ee344f75..15dd83c2a 100644 --- a/src/test/Dashboard/Dashboard.test.tsx +++ b/src/test/Dashboard/Dashboard.test.tsx @@ -30,9 +30,10 @@ const mockFooConnectUrl = 'service:jmx:rmi://someFooUrl'; const mockFooTarget: Target = { connectUrl: mockFooConnectUrl, alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; diff --git a/src/test/Events/EventTemplates.test.tsx b/src/test/Events/EventTemplates.test.tsx index 736f28e8b..915705da6 100644 --- a/src/test/Events/EventTemplates.test.tsx +++ b/src/test/Events/EventTemplates.test.tsx @@ -16,7 +16,7 @@ import { authFailMessage } from '@app/ErrorView/types'; import { EventTemplates } from '@app/Events/EventTemplates'; import { DeleteOrDisableWarningType } from '@app/Modal/types'; -import { MessageType, EventTemplate, MessageMeta, NotificationMessage } from '@app/Shared/Services/api.types'; +import { MessageType, EventTemplate, MessageMeta, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, defaultServices, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import '@testing-library/jest-dom'; @@ -25,7 +25,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; @@ -255,7 +261,7 @@ describe('', () => { it('should show error view if failing to retrieve event templates', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/Events/EventTypes.test.tsx b/src/test/Events/EventTypes.test.tsx index df8cd0518..8016f6709 100644 --- a/src/test/Events/EventTypes.test.tsx +++ b/src/test/Events/EventTypes.test.tsx @@ -15,7 +15,7 @@ */ import { authFailMessage } from '@app/ErrorView/types'; import { EventTypes } from '@app/Events/EventTypes'; -import { EventType } from '@app/Shared/Services/api.types'; +import { EventType, Target } from '@app/Shared/Services/api.types'; import { ServiceContext, defaultServices, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import { act as doAct, cleanup, screen } from '@testing-library/react'; @@ -24,7 +24,13 @@ import { of, Subject } from 'rxjs'; import { render, renderSnapshot } from '../utils'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: 'foo', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockEventType: EventType = { name: 'Some Event', @@ -58,7 +64,7 @@ describe('', () => { it('should show error view if failing to retrieve event types', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index 7354520c1..69d73eb08 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -19,6 +19,7 @@ import { ActiveRecording, RecordingState, NotificationMessage, + Target, } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; @@ -33,12 +34,21 @@ jest.mock('@patternfly/react-core', () => ({ const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; - -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget: Target = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; + const mockArchivedRecording: ArchivedRecording = { name: 'someArchivedRecording_some_random', downloadUrl: 'http://downloadUrl', diff --git a/src/test/RecordingMetadata/ClickableLabel.test.tsx b/src/test/RecordingMetadata/ClickableLabel.test.tsx index e38cb4f12..beb9c64a6 100644 --- a/src/test/RecordingMetadata/ClickableLabel.test.tsx +++ b/src/test/RecordingMetadata/ClickableLabel.test.tsx @@ -14,18 +14,18 @@ * limitations under the License. */ import { ClickableLabel } from '@app/RecordingMetadata/ClickableLabel'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import '@testing-library/jest-dom'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { cleanup, screen } from '@testing-library/react'; import { render, renderSnapshot } from '../utils'; const mockLabel = { key: 'someLabel', value: 'someValue', -} as RecordingLabel; +} as KeyValue; const mockLabelAsString = 'someLabel: someValue'; -const onLabelClick = jest.fn((_label: RecordingLabel) => { +const onLabelClick = jest.fn((_label: KeyValue) => { /**Do nothing. Used for checking renders */ }); diff --git a/src/test/RecordingMetadata/LabelCell.test.tsx b/src/test/RecordingMetadata/LabelCell.test.tsx index 572772be9..67fb6f9fa 100644 --- a/src/test/RecordingMetadata/LabelCell.test.tsx +++ b/src/test/RecordingMetadata/LabelCell.test.tsx @@ -15,9 +15,8 @@ */ import { LabelCell } from '@app/RecordingMetadata/LabelCell'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; -import { Target } from '@app/Shared/Services/api.types'; +import { KeyValue, Target } from '@app/Shared/Services/api.types'; import '@testing-library/jest-dom'; import { cleanup, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -26,14 +25,15 @@ import { render, renderSnapshot } from '../utils'; const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; -const mockLabel = { key: 'someLabel', value: 'someValue' } as RecordingLabel; -const mockAnotherLabel = { key: 'anotherLabel', value: 'anotherValue' } as RecordingLabel; +const mockLabel = { key: 'someLabel', value: 'someValue' } as KeyValue; +const mockAnotherLabel = { key: 'anotherLabel', value: 'anotherValue' } as KeyValue; const mockLabelList = [mockLabel, mockAnotherLabel]; // For display diff --git a/src/test/RecordingMetadata/RecordingLabelFields.test.tsx b/src/test/RecordingMetadata/RecordingLabelFields.test.tsx index 9449dfa60..13a48d7e3 100644 --- a/src/test/RecordingMetadata/RecordingLabelFields.test.tsx +++ b/src/test/RecordingMetadata/RecordingLabelFields.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { RecordingLabelFields, RecordingLabelFieldsProps } from '@app/RecordingMetadata/RecordingLabelFields'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { ValidatedOptions } from '@patternfly/react-core'; import '@testing-library/jest-dom'; import * as tlr from '@testing-library/react'; @@ -37,12 +37,12 @@ mockMetadataFile.text = jest.fn( describe('', () => { // RecordingLabelFields component modifies labels in-place, so we need to reinitialize mocks // after every tests. - let mockLabels: RecordingLabel[]; + let mockLabels: KeyValue[]; let mockValid: ValidatedOptions; let mockProps: RecordingLabelFieldsProps; - let mockLabel1: RecordingLabel; - let mockLabel2: RecordingLabel; - let mockEmptyLabel: RecordingLabel; + let mockLabel1: KeyValue; + let mockLabel2: KeyValue; + let mockEmptyLabel: KeyValue; afterEach(cleanup); @@ -50,20 +50,20 @@ describe('', () => { mockLabel1 = { key: 'someLabel', value: 'someValue', - } as RecordingLabel; + } as KeyValue; mockLabel2 = { key: 'anotherLabel', value: 'anotherValue', - } as RecordingLabel; + } as KeyValue; mockEmptyLabel = { key: '', value: '', - } as RecordingLabel; + } as KeyValue; mockLabels = [mockLabel1, mockLabel2]; mockValid = ValidatedOptions.default; mockProps = { labels: mockLabels, - setLabels: jest.fn((labels: RecordingLabel[]) => (mockLabels = labels.slice())), + setLabels: jest.fn((labels: KeyValue[]) => (mockLabels = labels.slice())), setValid: jest.fn((state: ValidatedOptions) => (mockValid = state)), }; }); diff --git a/src/test/Recordings/ActiveRecordingsTable.test.tsx b/src/test/Recordings/ActiveRecordingsTable.test.tsx index 38e98fb0c..865cefc71 100644 --- a/src/test/Recordings/ActiveRecordingsTable.test.tsx +++ b/src/test/Recordings/ActiveRecordingsTable.test.tsx @@ -23,7 +23,7 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { ActiveRecording, RecordingState, NotificationMessage } from '@app/Shared/Services/api.types'; +import { ActiveRecording, RecordingState, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { defaultServices, ServiceContext, Services } from '@app/Shared/Services/Services'; import { TargetService } from '@app/Shared/Services/Target.service'; import dayjs, { defaultDatetimeFormat } from '@i18n/datetime'; @@ -33,10 +33,19 @@ import { basePreloadedState, DEFAULT_DIMENSIONS, render, resize } from '../utils const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', @@ -527,7 +536,7 @@ describe('', () => { it('should show error view if failing to retrieve recordings', async () => { const subj = new Subject(); const mockTargetSvc = { - target: () => of(mockTarget), + target: () => of(mockTarget as Target), authFailure: () => subj.asObservable(), } as TargetService; const services: Services = { diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 4a492c6ab..b8d028bf9 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -21,7 +21,7 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage } from '@app/Shared/Services/api.types'; +import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage, Target } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import { Text } from '@patternfly/react-core'; import '@testing-library/jest-dom'; @@ -32,14 +32,31 @@ import { basePreloadedState, DEFAULT_DIMENSIONS, render, resize } from '../utils const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockJvmId = 'id'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget', jvmId: mockJvmId }; -const mockUploadsTarget = { connectUrl: UPLOADS_SUBDIRECTORY, alias: '' }; -const mockRecordingLabels = { - someLabel: 'someValue', +const mockTarget: Target = { + connectUrl: mockConnectUrl, + alias: 'fooTarget', + jvmId: mockJvmId, + labels: [], + annotations: { cryostat: [], platform: [] }, }; -const mockUploadedRecordingLabels = { - someUploaded: 'someUploadedValue', +const mockUploadsTarget = { + connectUrl: UPLOADS_SUBDIRECTORY, + alias: '', + labels: [], + annotations: { cryostat: [], platform: [] }, }; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; +const mockUploadedRecordingLabels = [ + { + key: 'someUploaded', + value: 'someUpdatedValue', + }, +]; const mockMetadataFileName = 'mock.metadata.json'; const mockMetadataFile = new File( [JSON.stringify({ labels: { ...mockUploadedRecordingLabels } })], diff --git a/src/test/Recordings/Filters/DurationFilter.test.tsx b/src/test/Recordings/Filters/DurationFilter.test.tsx index a6eb14b2b..a9ab62117 100644 --- a/src/test/Recordings/Filters/DurationFilter.test.tsx +++ b/src/test/Recordings/Filters/DurationFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/Filters/LabelFilter.test.tsx b/src/test/Recordings/Filters/LabelFilter.test.tsx index 1bcb48391..117a0960f 100644 --- a/src/test/Recordings/Filters/LabelFilter.test.tsx +++ b/src/test/Recordings/Filters/LabelFilter.test.tsx @@ -19,12 +19,18 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; -const mockAnotherRecordingLabels = { - anotherLabel: 'anotherValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; +const mockAnotherRecordingLabels = [ + { + key: 'anotherLabel', + value: 'anotherValue', + }, +]; const mockRecordingLabelList = ['someLabel:someValue', 'anotherLabel:anotherValue']; const mockRecording: ActiveRecording = { @@ -49,7 +55,7 @@ const mockAnotherRecording = { const mockRecordingWithoutLabel = { ...mockRecording, name: 'noLabelRecording', - metadata: { labels: {} }, + metadata: { labels: [] }, } as ActiveRecording; const mockRecordingList = [mockRecording, mockAnotherRecording, mockRecordingWithoutLabel]; diff --git a/src/test/Recordings/Filters/NameFilter.test.tsx b/src/test/Recordings/Filters/NameFilter.test.tsx index b32477b31..2d04b37b5 100644 --- a/src/test/Recordings/Filters/NameFilter.test.tsx +++ b/src/test/Recordings/Filters/NameFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/Filters/RecordingStateFilter.test.tsx b/src/test/Recordings/Filters/RecordingStateFilter.test.tsx index bf94a3aea..d576b621c 100644 --- a/src/test/Recordings/Filters/RecordingStateFilter.test.tsx +++ b/src/test/Recordings/Filters/RecordingStateFilter.test.tsx @@ -19,9 +19,12 @@ import { ActiveRecording, RecordingState } from '@app/Shared/Services/api.types' import { cleanup, screen, within } from '@testing-library/react'; import { render, renderSnapshot } from '../../utils'; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ActiveRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', diff --git a/src/test/Recordings/RecordingFilters.test.tsx b/src/test/Recordings/RecordingFilters.test.tsx index 547d932bc..2d4370602 100644 --- a/src/test/Recordings/RecordingFilters.test.tsx +++ b/src/test/Recordings/RecordingFilters.test.tsx @@ -37,15 +37,19 @@ import { basePreloadedState, render } from '../utils'; const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockActiveRecording: ActiveRecording = { name: 'someRecording', diff --git a/src/test/Recordings/RecordingLabelsPanel.test.tsx b/src/test/Recordings/RecordingLabelsPanel.test.tsx index 8b845e2fc..3478435aa 100644 --- a/src/test/Recordings/RecordingLabelsPanel.test.tsx +++ b/src/test/Recordings/RecordingLabelsPanel.test.tsx @@ -33,9 +33,12 @@ jest.mock('@app/RecordingMetadata/BulkEditLabels', () => { }; }); -const mockRecordingLabels = { - someLabel: 'someValue', -}; +const mockRecordingLabels = [ + { + key: 'someLabel', + value: 'someValue', + }, +]; const mockRecording: ArchivedRecording = { name: 'someRecording', diff --git a/src/test/Recordings/Recordings.test.tsx b/src/test/Recordings/Recordings.test.tsx index bdf062a7f..4c21e6b8c 100644 --- a/src/test/Recordings/Recordings.test.tsx +++ b/src/test/Recordings/Recordings.test.tsx @@ -53,9 +53,10 @@ jest.mock('@app/TargetView/TargetView', () => { const mockFooTarget: Target = { connectUrl: 'service:jmx:rmi://someFooUrl', alias: 'fooTarget', + labels: [], annotations: { - cryostat: {}, - platform: {}, + cryostat: [], + platform: [], }, }; diff --git a/src/test/Rules/CreateRule.test.tsx b/src/test/Rules/CreateRule.test.tsx index 634a215e4..5bb566850 100644 --- a/src/test/Rules/CreateRule.test.tsx +++ b/src/test/Rules/CreateRule.test.tsx @@ -30,9 +30,10 @@ const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockTarget: Target = { connectUrl: mockConnectUrl, alias: 'io.cryostat.Cryostat', + labels: [], annotations: { - cryostat: { PORT: '9091' }, - platform: {}, + cryostat: [{ key: 'PORT', value: '9091' }], + platform: [], }, }; const mockEventTemplate: EventTemplate = { diff --git a/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx b/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx index 741451f8a..ce7bfe824 100644 --- a/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx +++ b/src/test/SecurityPanel/Credentials/StoreCredentials.test.tsx @@ -35,15 +35,29 @@ const mockAnotherCredential: StoredCredential = { numMatchingTargets: 2, }; -const mockTarget: Target = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'someAlias' }; -const mockAnotherTarget: Target = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherAlias' }; +const mockTarget: Target = { + connectUrl: 'service:jmx:rmi://someUrl', + alias: 'someAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; +const mockAnotherTarget: Target = { + connectUrl: 'service:jmx:rmi://anotherUrl', + alias: 'anotherAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, +}; const mockAnotherMatchingTarget: Target = { connectUrl: 'service:jmx:rmi://anotherMatchUrl', alias: 'anotherMatchAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockYetAnotherMatchingTarget: Target = { connectUrl: 'service:jmx:rmi://yetAnotherMatchUrl', alias: 'yetAnotherMatchAlias', + labels: [], + annotations: { cryostat: [], platform: [] }, }; const mockMatchedCredentialResponse: MatchedCredential = { diff --git a/src/test/TargetView/TargetSelect.test.tsx b/src/test/TargetView/TargetSelect.test.tsx index c31a1b4d7..f30c19ab0 100644 --- a/src/test/TargetView/TargetSelect.test.tsx +++ b/src/test/TargetView/TargetSelect.test.tsx @@ -26,16 +26,20 @@ const mockBarConnectUrl = 'service:jmx:rmi://someBarUrl'; const CUSTOM_TARGET_REALM = 'Custom Targets'; -const cryostatAnnotation = { - REALM: CUSTOM_TARGET_REALM, -}; +const cryostatAnnotation = [ + { + key: 'REALM', + value: CUSTOM_TARGET_REALM, + }, +]; const mockFooTarget: Target = { jvmId: 'abcd', connectUrl: mockFooConnectUrl, alias: 'fooTarget', + labels: [], annotations: { cryostat: cryostatAnnotation, - platform: {}, + platform: [], }, }; const mockBarTarget: Target = { ...mockFooTarget, jvmId: 'efgh', connectUrl: mockBarConnectUrl, alias: 'barTarget' }; From 59a1b937a68102c7779f0e93b1dcc12fba86d925 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:48:23 -0500 Subject: [PATCH 18/64] correct tests --- .../RecordingMetadata/BulkEditLabels.test.tsx | 14 +++++++-- .../Recordings/ActiveRecordingsTable.test.tsx | 6 ++-- .../ArchivedRecordingsTable.test.tsx | 29 ++++++++++++++----- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index 69d73eb08..cec202ce1 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -78,7 +78,12 @@ const mockActiveLabelsNotification = { target: mockConnectUrl, recordingName: 'someActiveRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, }, } as NotificationMessage; @@ -89,7 +94,12 @@ const mockArchivedLabelsNotification = { target: mockConnectUrl, recordingName: 'someArchivedRecording_some_random', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, }, } as NotificationMessage; diff --git a/src/test/Recordings/ActiveRecordingsTable.test.tsx b/src/test/Recordings/ActiveRecordingsTable.test.tsx index 865cefc71..1b01ac846 100644 --- a/src/test/Recordings/ActiveRecordingsTable.test.tsx +++ b/src/test/Recordings/ActiveRecordingsTable.test.tsx @@ -69,7 +69,7 @@ const mockLabelsNotification = { target: mockConnectUrl, recordingName: 'someRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, }, } as NotificationMessage; const mockStopNotification = { @@ -242,8 +242,8 @@ describe('', () => { expect(state).toBeInTheDocument(); expect(state).toBeVisible(); - Object.keys(mockRecordingLabels).forEach((key) => { - const label = screen.getByText(`${key}: ${mockRecordingLabels[key]}`); + mockRecordingLabels.forEach((entry) => { + const label = screen.getByText(`${entry.key}: ${entry.value}`); expect(label).toBeInTheDocument(); expect(label).toBeVisible(); }); diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index b8d028bf9..56dc27a9c 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -21,7 +21,13 @@ import { TargetRecordingFilters, } from '@app/Shared/Redux/Filters/RecordingFilterSlice'; import { RootState } from '@app/Shared/Redux/ReduxStore'; -import { UPLOADS_SUBDIRECTORY, ArchivedRecording, NotificationMessage, Target } from '@app/Shared/Services/api.types'; +import { + UPLOADS_SUBDIRECTORY, + ArchivedRecording, + NotificationMessage, + Target, + KeyValue, +} from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import { Text } from '@patternfly/react-core'; import '@testing-library/jest-dom'; @@ -57,13 +63,22 @@ const mockUploadedRecordingLabels = [ value: 'someUpdatedValue', }, ]; +export const convertLabels = (kv: KeyValue[]): object => { + const out = {}; + for (let e of kv) { + out[e.key] = e.value; + } + return out; +}; const mockMetadataFileName = 'mock.metadata.json'; const mockMetadataFile = new File( - [JSON.stringify({ labels: { ...mockUploadedRecordingLabels } })], + [JSON.stringify({ labels: convertLabels(mockUploadedRecordingLabels) })], mockMetadataFileName, { type: 'json' }, ); -mockMetadataFile.text = jest.fn(() => Promise.resolve(JSON.stringify({ labels: { ...mockUploadedRecordingLabels } }))); +mockMetadataFile.text = jest.fn(() => + Promise.resolve(JSON.stringify({ labels: convertLabels(mockUploadedRecordingLabels) })), +); const mockRecording: ArchivedRecording = { name: 'someRecording', @@ -91,7 +106,7 @@ const mockLabelsNotification = { target: mockConnectUrl, recordingName: 'someRecording', jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, }, } as NotificationMessage; const mockDeleteNotification = { @@ -237,8 +252,8 @@ describe('', () => { expect(size).toBeInTheDocument(); expect(size).toBeVisible(); - Object.keys(mockRecordingLabels).forEach((key) => { - const label = screen.getByText(`${key}: ${mockRecordingLabels[key]}`); + mockRecordingLabels.forEach((entry) => { + const label = screen.getByText(`${entry.key}: ${entry.value}`); expect(label).toBeInTheDocument(); expect(label).toBeVisible(); }); @@ -742,7 +757,7 @@ describe('', () => { expect(uploadSpy).toHaveBeenCalled(); expect(uploadSpy).toHaveBeenCalledWith( mockFileUpload, - mockUploadedRecordingLabels, + convertLabels(mockUploadedRecordingLabels), expect.any(Function), expect.any(Subject), ); From 387b39eb376553fff90129e4e4d93557620ebe28 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 1 Mar 2024 16:51:05 -0500 Subject: [PATCH 19/64] fixup! correct tests --- src/test/Recordings/ArchivedRecordingsTable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index 56dc27a9c..e167c84c1 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -65,7 +65,7 @@ const mockUploadedRecordingLabels = [ ]; export const convertLabels = (kv: KeyValue[]): object => { const out = {}; - for (let e of kv) { + for (const e of kv) { out[e.key] = e.value; } return out; From 7507dc132603f9d440a81271cbf8a13b39354c5d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 13:01:05 -0500 Subject: [PATCH 20/64] fix(graphql): adjustments for Dashboard Automated Analysis with new API schema (#1220) * fix(graphql): adjustments for Dashboard Automated Analysis with new API schema --- .../AutomatedAnalysisCard.tsx | 36 +++++++++++-------- src/app/Shared/Services/Api.service.tsx | 10 +++++- .../AutomatedAnalysisCard.test.tsx | 10 +++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 901257860..4755d9578 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -179,18 +179,23 @@ export const AutomatedAnalysisCard: DashboardCardFC ` query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: { - name: "${automatedAnalysisRecordingName}", - labels: ["origin=${automatedAnalysisRecordingName}"], - }) { - data { - state - name - downloadUrl - reportUrl - metadata { - labels + target { + recordings { + active (filter: { + name: "${automatedAnalysisRecordingName}", + labels: ["origin=${automatedAnalysisRecordingName}"], + }) { + data { + state + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } } } } @@ -214,7 +219,10 @@ export const AutomatedAnalysisCard: DashboardCardFC downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size archivedTime @@ -358,7 +366,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].recordings.active.data[0] as Recording), + map((v) => v.data.targetNodes[0].target.recordings.active.data[0] as Recording), tap((recording) => { if (recording === null || recording === undefined) { throw new Error(NO_RECORDINGS_MESSAGE); diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 2bded2658..4f8b3b7a4 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -310,8 +310,16 @@ export class ApiService { if (archiveOnStop != undefined) { form.append('archiveOnStop', String(archiveOnStop)); } + const transformedMetadata = { + labels: {}, + annotations: { + cryostat: {}, + platform: {}, + }, + }; + metadata?.labels.forEach((label) => (transformedMetadata.labels[label.key] = label.value)); if (metadata) { - form.append('metadata', JSON.stringify(metadata)); + form.append('metadata', JSON.stringify(transformedMetadata)); } if (restart != undefined) { form.append('restart', String(restart)); diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 8f6d9a313..32b6bd382 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -162,7 +162,7 @@ const mockTargetNode = { const mockActiveRecordingsResponse = { data: { - targetNodes: [mockTargetNode], + targetNodes: [{ target: mockTargetNode }], }, }; @@ -170,9 +170,11 @@ const mockEmptyActiveRecordingsResponse = { data: { targetNodes: [ { - recordings: { - active: { - data: [], + target: { + recordings: { + active: { + data: [], + }, }, }, }, From b9977b9e5c92185d7ae6d73071e9ca3264efafd2 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 16:08:58 -0500 Subject: [PATCH 21/64] fix(archives): adjust queries and response handling for updated data schema (#1221) * fix(archives): adjust queries and response handling for updated data schema --- .../AllTargetsArchivedRecordingsTable.tsx | 28 ++++----- src/app/RecordingMetadata/BulkEditLabels.tsx | 2 +- .../Recordings/ArchivedRecordingsTable.tsx | 12 +++- ...AllTargetsArchivedRecordingsTable.test.tsx | 58 ++++++++++--------- 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index a52104a5e..6ddb90d8a 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -112,7 +112,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC { const target: Target = { - connectUrl: node.target.serviceUri, + connectUrl: node.target.connectUrl, alias: node.target.alias, labels: [], annotations: { @@ -123,7 +123,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC = ({ observable = isUploadsTable ? context.api .graphql( - `query GetUploadedRecordings($filter: ArchivedRecordingFilterInput) { + `query GetUploadedRecordings($filter: ArchivedRecordingsFilterInput) { archivedRecordings(filter: $filter) { data { name diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 3d23450f8..9c16de6e2 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -198,7 +198,10 @@ export const ArchivedRecordingsTable: React.FC = ( downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size } @@ -213,14 +216,17 @@ export const ArchivedRecordingsTable: React.FC = ( const queryUploadedRecordings = React.useCallback(() => { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ return context.api.graphql( - `query UploadedRecordings($filter: ArchivedRecordingFilterInput){ + `query UploadedRecordings($filter: ArchivedRecordingsFilterInput) { archivedRecordings(filter: $filter) { data { name downloadUrl reportUrl metadata { - labels + labels { + key + value + } } size } diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 6af7d3070..0fdd8e029 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -76,42 +76,42 @@ const mockTargetsAndCountsResponse = { data: { targetNodes: [ { - recordings: { - archived: { - aggregate: { - count: mockCount1, - }, - }, - }, target: { alias: mockAlias1, - serviceUri: mockConnectUrl1, - }, - }, - { - recordings: { - archived: { - aggregate: { - count: mockCount2, + connectUrl: mockConnectUrl1, + recordings: { + archived: { + aggregate: { + count: mockCount1, + }, }, }, }, - target: { - alias: mockAlias2, - serviceUri: mockConnectUrl2, - }, }, { - recordings: { - archived: { - aggregate: { - count: mockCount3, + target: { + alias: mockAlias2, + connectUrl: mockConnectUrl2, + recordings: { + archived: { + aggregate: { + count: mockCount2, + }, }, }, }, + }, + { target: { alias: mockAlias3, - serviceUri: mockConnectUrl3, + connectUrl: mockConnectUrl3, + recordings: { + archived: { + aggregate: { + count: mockCount3, + }, + }, + }, }, }, ], @@ -122,10 +122,12 @@ const mockNewTargetCountResponse = { data: { targetNodes: [ { - recordings: { - archived: { - aggregate: { - count: mockNewCount, + target: { + recordings: { + archived: { + aggregate: { + count: mockNewCount, + }, }, }, }, From dcc22a53d2ac7e9601304155e05241c77c49de1d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 15 Mar 2024 11:38:13 -0400 Subject: [PATCH 22/64] fix(topology): update for new GraphQL schema (#1224) --- .../AllTargetsArchivedRecordingsTable.tsx | 43 ++-- .../CreateRecording/CustomRecordingForm.tsx | 2 +- src/app/CreateRecording/types.ts | 2 + .../AutomatedAnalysisCard.tsx | 60 +++--- src/app/RecordingMetadata/BulkEditLabels.tsx | 6 +- .../Recordings/ArchivedRecordingsTable.tsx | 26 ++- src/app/Shared/Services/Api.service.tsx | 190 +++++++++++------- src/app/Shared/Services/api.types.ts | 37 ++-- src/app/Topology/Actions/utils.tsx | 81 ++++---- src/app/utils/fakeData.ts | 14 +- src/mirage/index.ts | 82 ++++---- ...AllTargetsArchivedRecordingsTable.test.tsx | 32 ++- .../CustomRecordingForm.test.tsx | 2 +- .../AutomatedAnalysisCard.test.tsx | 36 ++-- .../RecordingMetadata/BulkEditLabels.test.tsx | 4 +- .../ArchivedRecordingsTable.test.tsx | 28 ++- 16 files changed, 365 insertions(+), 280 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 6ddb90d8a..e9396ed54 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -123,7 +123,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC( `query AllTargetsArchives { - targetNodes { - target { - connectUrl - alias - recordings { - archived { - aggregate { - count - } - } + targetNodes { + target { + connectUrl + alias + archivedRecordings { + aggregate { + count } } } - }`, + } + }`, ) .pipe(map((v) => v.data.targetNodes)) .subscribe({ @@ -175,20 +173,17 @@ export const AllTargetsArchivedRecordingsTable: React.FC( - ` - query ArchiveCountForTarget($connectUrl: String) { - targetNodes(filter: { name: $connectUrl }) { - target { - recordings { - archived { - aggregate { - count + `query ArchiveCountForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + aggregate { + count + } } } } - } - } - }`, + }`, { connectUrl: target.connectUrl }, ) .subscribe((v) => { @@ -198,7 +193,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC { events: eventSpecifierString, duration: continuous ? undefined : duration * (durationUnit / 1000), archiveOnStop: archiveOnStop && !continuous, - restart: restart, + replace: restart ? 'ALWAYS' : 'NEVER', advancedOptions: { toDisk: toDisk, maxAge: toDisk ? (continuous ? maxAge * maxAgeUnit : undefined) : undefined, diff --git a/src/app/CreateRecording/types.ts b/src/app/CreateRecording/types.ts index e11288c17..00d9492b7 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -18,6 +18,8 @@ import { ValidatedOptions } from '@patternfly/react-core'; export type EventTemplateIdentifier = Pick; +export type RecordingReplace = 'ALWAYS' | 'NEVER' | 'STOPPED'; + interface _FormBaseData { name: string; template?: EventTemplateIdentifier; diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 4755d9578..ee43d593b 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -180,21 +180,19 @@ export const AutomatedAnalysisCard: DashboardCardFC query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { target { - recordings { - active (filter: { - name: "${automatedAnalysisRecordingName}", - labels: ["origin=${automatedAnalysisRecordingName}"], - }) { - data { - state - name - downloadUrl - reportUrl - metadata { - labels { - key - value - } + activeRecordings(filter: { + name: "${automatedAnalysisRecordingName}", + labels: ["origin=${automatedAnalysisRecordingName}"], + }) { + data { + state + name + downloadUrl + reportUrl + metadata { + labels { + key + value } } } @@ -213,21 +211,25 @@ export const AutomatedAnalysisCard: DashboardCardFC /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ return context.api.graphql( `query ArchivedRecordingsForAutomatedAnalysis($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } } } - size - archivedTime } - } }`, { connectUrl }, ); @@ -311,7 +313,7 @@ export const AutomatedAnalysisCard: DashboardCardFC queryArchivedRecordings(connectUrl) .pipe( first(), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: (recordings) => { @@ -366,7 +368,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].target.recordings.active.data[0] as Recording), + map((v) => v.data.targetNodes[0].target.activeRecordings.data[0] as Recording), tap((recording) => { if (recording === null || recording === undefined) { throw new Error(NO_RECORDINGS_MESSAGE); diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 3ecb392ec..47ec48833 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -188,8 +188,8 @@ export const BulkEditLabels: React.FC = ({ context.api.graphql( `query ArchivedRecordingsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived { + target { + archivedRecordings { data { name downloadUrl @@ -205,7 +205,7 @@ export const BulkEditLabels: React.FC = ({ { connectUrl: target.connectUrl }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), first(), ); } diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 9c16de6e2..88ac40dcd 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -192,18 +192,22 @@ export const ArchivedRecordingsTable: React.FC = ( return context.api.graphql( ` query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size } } - size } } }`, @@ -256,7 +260,7 @@ export const ArchivedRecordingsTable: React.FC = ( filter((target) => !!target), first(), concatMap((target: Target) => queryTargetRecordings(target.connectUrl)), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: handleRecordings, diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 4f8b3b7a4..388a1d107 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -45,21 +45,23 @@ import { CredentialsResponse, RulesResponse, EnvironmentNode, - DiscoveryResponse, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, RecordingCountResponse, MBeanMetrics, MBeanMetricsResponse, EventType, NotificationCategory, - NullableTarget, HttpError, SimpleResponse, XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, KeyValue, - CustomTargetStub, + TargetStub, + TargetForTest, + Metadata, + TargetMetadata, + isTargetMetadata, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -171,7 +173,7 @@ export class ApiService { } createTarget( - target: CustomTargetStub, + target: TargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -207,7 +209,7 @@ export class ApiService { ); } - deleteTarget(target: Target): Observable { + deleteTarget(target: TargetStub): Observable { return this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}`, { method: 'DELETE', }).pipe( @@ -296,7 +298,7 @@ export class ApiService { name, events, duration, - restart, + replace, archiveOnStop, metadata, advancedOptions, @@ -310,19 +312,11 @@ export class ApiService { if (archiveOnStop != undefined) { form.append('archiveOnStop', String(archiveOnStop)); } - const transformedMetadata = { - labels: {}, - annotations: { - cryostat: {}, - platform: {}, - }, - }; - metadata?.labels.forEach((label) => (transformedMetadata.labels[label.key] = label.value)); if (metadata) { - form.append('metadata', JSON.stringify(transformedMetadata)); + form.append('metadata', JSON.stringify(this.transformMetadataToObject(metadata))); } - if (restart != undefined) { - form.append('restart', String(restart)); + if (replace != undefined) { + form.append('replace', String(replace)); } if (advancedOptions) { if (advancedOptions.toDisk != undefined) { @@ -489,7 +483,7 @@ export class ApiService { } uploadArchivedRecordingToGrafana( - sourceTarget: Observable, + sourceTarget: Observable, recordingName: string, ): Observable { return sourceTarget.pipe( @@ -531,7 +525,13 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: KeyValue[]) { + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + stringifyRecordingLabels(labels: KeyValue | KeyValue[]): string { + return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + } + + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + transformAndStringifyToRawLabels(labels: KeyValue[]): string { const rawLabels = {}; for (const label of labels) { rawLabels[label.key] = label.value; @@ -539,6 +539,30 @@ export class ApiService { return JSON.stringify(rawLabels); } + transformMetadataToObject(metadata: Metadata | TargetMetadata): object { + if (isTargetMetadata(metadata)) { + return { + labels: this.transformLabelsToObject(metadata.labels), + annotations: { + cryostat: this.transformLabelsToObject(metadata?.annotations?.cryostat), + platform: this.transformLabelsToObject(metadata?.annotations?.platform), + }, + }; + } else { + return { + labels: this.transformLabelsToObject(metadata.labels), + }; + } + } + + transformLabelsToObject(labels: KeyValue[]): object { + const out = {}; + for (const label of labels) { + out[label.key] = label.value; + } + return out; + } + postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: KeyValue[]): Observable { return this.sendRequest( 'beta', @@ -734,7 +758,7 @@ export class ApiService { } getActiveProbesForTarget( - target: Target, + target: TargetStub, suppressNotifications = false, skipStatusCheck = false, ): Observable { @@ -906,13 +930,11 @@ export class ApiService { ` query PostRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived(filter: { name: $recordingName }) { - data { - doPutMetadata(metadata: { labels: $labels }) { - metadata { - labels - } + archivedRecordings(filter: { name: $recordingName }) { + data { + doPutMetadata(metadata: { labels: $labels }) { + metadata { + labels } } } @@ -922,7 +944,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings as ArchivedRecording[]), ); } @@ -953,13 +975,11 @@ export class ApiService { ` query PostActiveRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active(filter: { name: $recordingName }) { - data { - doPutMetadata(metadata: { labels: $labels }) { - metadata { - labels - } + activeRecordings(filter: { name: $recordingName }) { + data { + doPutMetadata(metadata: { labels: $labels }) { + metadata { + labels } } } @@ -969,7 +989,7 @@ export class ApiService { { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, ), ), - map((v) => v.data.targetNodes[0].recordings.active as ActiveRecording[]), + map((v) => v.data.targetNodes[0].target.activeRecordings as ActiveRecording[]), ); } @@ -1043,11 +1063,10 @@ export class ApiService { } getDiscoveryTree(): Observable { - return this.sendRequest('v2.1', 'discovery', { + return this.sendRequest('v3', 'discovery', { method: 'GET', }).pipe( concatMap((resp) => resp.json()), - map((body: DiscoveryResponse) => body.data.result), first(), ); } @@ -1056,7 +1075,7 @@ export class ApiService { matchTargetsWithExpr(matchExpression: string, targets: Target[]): Observable { const body = JSON.stringify({ matchExpression, - targets, + targets: targets.map((t) => this.transformTarget(t)), }); const headers = new Headers(); headers.set('Content-Type', 'application/json'); @@ -1087,20 +1106,20 @@ export class ApiService { ); } - groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingFilterInput): Observable { + groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingsFilterInput): Observable { return this.graphql( ` - query GetRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query GroupHasRecording ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { - active(filter: $recordingFilter) { - data { - name - } + target { + activeRecordings(filter: $recordingFilter) { + aggregate { + count } + } } } } @@ -1114,22 +1133,22 @@ export class ApiService { first(), map((body) => body.data.environmentNodes[0].descendantTargets.reduce( - (acc: Partial[], curr) => acc.concat(curr.recordings?.active?.data || []), - [] as Partial[], + (acc: number, curr) => acc + curr.target.activeRecordings.aggregate.count, + 0, ), ), catchError((_) => of([])), - map((recs: Partial[]) => recs.length > 0), // At least one + map((acc) => acc > 0), // At least one ); } - targetHasRecording(target: Target, filter: ActiveRecordingFilterInput = {}): Observable { + targetHasRecording(target: TargetStub, filter: ActiveRecordingsFilterInput = {}): Observable { return this.graphql( ` - query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingFilterInput) { + query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingsFilterInput) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: $recordingFilter) { + target { + activeRecordings(filter: $recordingFilter) { aggregate { count } @@ -1149,7 +1168,7 @@ export class ApiService { if (nodes.length === 0) { return false; } - const count = nodes[0].recordings.active.aggregate.count; + const count = nodes[0].target.activeRecordings.aggregate.count; return count > 0; }), catchError((_) => of(false)), @@ -1157,7 +1176,7 @@ export class ApiService { } checkCredentialForTarget( - target: Target, + target: TargetStub, credentials: { username: string; password: string }, ): Observable< | { @@ -1210,7 +1229,7 @@ export class ApiService { ); } - getTargetMBeanMetrics(target: Target, queries: string[]): Observable { + getTargetMBeanMetrics(target: TargetStub, queries: string[]): Observable { return this.graphql( ` query MBeanMXMetricsForTarget($connectUrl: String) { @@ -1235,30 +1254,37 @@ export class ApiService { ); } - getTargetArchivedRecordings(target: Target): Observable { + getTargetArchivedRecordings(target: TargetStub): Observable { return this.graphql( ` - query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels + query ArchivedRecordingsForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime } - size - archivedTime } } - }`, + } + }`, { connectUrl: target.connectUrl }, true, true, - ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); + ).pipe(map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[])); } - getTargetActiveRecordings(target: Target): Observable { + getTargetActiveRecordings(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/recordings`, 'v1', @@ -1268,7 +1294,7 @@ export class ApiService { ); } - getTargetEventTemplates(target: Target): Observable { + getTargetEventTemplates(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/templates`, 'v1', @@ -1278,7 +1304,7 @@ export class ApiService { ); } - getTargetEventTypes(target: Target): Observable { + getTargetEventTypes(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/events`, 'v1', @@ -1317,8 +1343,22 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: KeyValue[]): string { - return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + private transformTarget(target: Target): TargetForTest { + const out: TargetForTest = { + alias: target.alias, + connectUrl: target.connectUrl, + labels: {}, + annotations: { cryostat: {}, platform: {} }, + }; + for (const l of target.labels) { + out.labels[l.key] = l.value; + } + for (const s of ['cryostat', 'platform']) { + for (const e of out.annotations[s]) { + target.annotations[s][e.key] = e.value; + } + } + return out; } private sendRequest( diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 2bd3b0ba3..1f124d35c 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { RecordingReplace } from '@app/CreateRecording/types'; import { AlertVariant } from '@patternfly/react-core'; import { Observable } from 'rxjs'; -export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'beta'; +export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'v3' | 'beta'; // ====================================== // Common Resources @@ -29,7 +30,17 @@ export interface KeyValue { export interface Metadata { labels: KeyValue[]; - annotations?: KeyValue[]; +} + +export type TargetMetadata = Metadata & { + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; + }; +}; + +export function isTargetMetadata(metadata: Metadata | TargetMetadata): metadata is TargetMetadata { + return (metadata as TargetMetadata).annotations !== undefined; } export interface ApiV2Response { @@ -88,7 +99,12 @@ export class XMLHttpError extends Error { } } -export type CustomTargetStub = Omit; +export type TargetStub = Omit; + +export type TargetForTest = Pick & { + labels: object; + annotations: { cryostat: object; platform: object }; +}; // ====================================== // Health Resources @@ -210,7 +226,7 @@ export interface RecordingAttributes { events: string; duration?: number; archiveOnStop?: boolean; - restart?: boolean; + replace?: RecordingReplace; advancedOptions?: AdvancedRecordingOptions; metadata?: Metadata; } @@ -238,7 +254,7 @@ export interface ActiveRecording extends Recording { maxAge: number; } -export interface ActiveRecordingFilterInput { +export interface ActiveRecordingsFilterInput { name?: string; state?: string; continuous?: boolean; @@ -265,8 +281,8 @@ export interface RecordingResponse extends ApiV2Response { export interface RecordingCountResponse { data: { targetNodes: { - recordings: { - active: { + target: { + activeRecordings: { aggregate: { count: number; }; @@ -446,6 +462,7 @@ export const TEMPLATE_UNSUPPORTED_MESSAGE = 'The template type used in this reco // Discovery/Target resources // ====================================== export interface Target { + id?: number; // present in responses but we must not include it in requests to create targets jvmId?: string; // present in responses, but we do not need to provide it in requests connectUrl: string; alias: string; @@ -499,12 +516,6 @@ export interface TargetNode extends _AbstractNode { readonly target: Target; } -export interface DiscoveryResponse extends ApiV2Response { - data: { - result: EnvironmentNode; - }; -} - // ====================================== // Notification resources // ====================================== diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index aee7c9300..f7f358a16 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -122,23 +122,23 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StartRecordingForGroup($filter: EnvironmentNodeFilterInput, $recordingName: String!, $labels: String) { + query StartRecordingForGroup($filter: DiscoveryNodeFilterInput!, $recordingName: String!, $metadata: RecordingMetadataInput!) { environmentNodes(filter: $filter) { name descendantTargets { name - doStartRecording(recording: { - name: $recordingName, - template: "Continuous", - templateType: "TARGET", - duration: 0, - restart: true, - metadata: { - labels: $labels - }, - }) { - name - state + target { + doStartRecording(recording: { + name: $recordingName, + template: "Continuous", + templateType: "TARGET", + duration: 0, + replace: "STOPPED", + metadata: $metadata, + }) { + name + state + } } } } @@ -147,12 +147,9 @@ export const nodeActions: NodeAction[] = [ { filter: { id: group.id }, recordingName: QUICK_RECORDING_NAME, - labels: services.api.stringifyRecordingLabels([ - { - key: QUICK_RECORDING_LABEL_KEY, - value: group.name.replace(/[\s+-]/g, '_'), - }, - ]), + metadata: { + labels: [{ key: QUICK_RECORDING_LABEL_KEY, value: group.name.replace(/[\s+-]/g, '_') }], + }, }, false, true, @@ -171,19 +168,21 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doArchive { - name - } + data { + doArchive { + name } + } } + } } } } @@ -213,20 +212,22 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StopRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query StopRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doStop { - name - state - } + data { + doStop { + name + state } + } } + } } } } @@ -257,20 +258,22 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doDelete { - name - state - } + data { + doDelete { + name + state } + } } + } } } } diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index 0075d0fa0..1723e190c 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -23,7 +23,7 @@ import { RecordingState, Recording, MBeanMetrics, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, ArchivedRecording, EventTemplate, EventProbe, @@ -208,7 +208,7 @@ export const fakeEvaluations: AnalysisResult[] = [ export const fakeCachedReport: CachedReportValue = { report: fakeEvaluations, - timestamp: 1663027200000, + timestamp: Date.now() - 1000 * 60 * 60, }; class FakeTargetService extends TargetService { @@ -301,7 +301,7 @@ class FakeApiService extends ApiService { } // JFR Metrics card - targetHasRecording(_target: Target, _filter?: ActiveRecordingFilterInput): Observable { + targetHasRecording(_target: Target, _filter?: ActiveRecordingsFilterInput): Observable { return of(true); } @@ -347,8 +347,8 @@ class FakeApiService extends ApiService { return of([]); } - // Automatic Analysis Card - // This fakes the fetch for Automatic Analysis recording to return available. + // Automated Analysis Card + // This fakes the fetch for Automated Analysis recording to return available. // Then subsequent graphql call for archived recording is ignored graphql( _query: string, @@ -360,8 +360,8 @@ class FakeApiService extends ApiService { data: { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [fakeAARecording], }, }, diff --git a/src/mirage/index.ts b/src/mirage/index.ts index d42a8b0f1..d78719320 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -126,35 +126,27 @@ export const startMirage = ({ environment = 'development' } = {}) => { }; }); this.get('api/v1/targets', (schema) => schema.all(Resource.TARGET).models); - this.get('api/v2.1/discovery', (schema) => { + this.get('api/v3/discovery', (schema) => { const models = schema.all(Resource.TARGET).models; const realmTypes = models.map((t) => t.annotations.cryostat['REALM']); return { - meta: { - status: 'OK', - type: 'application/json', - }, - data: { - result: { - name: 'Universe', - nodeType: 'Universe', - labels: [], - children: realmTypes.map((r: string) => ({ - name: r, - nodeType: 'Realm', - labels: [], - id: r, - children: models - .filter((t) => t.annotations.cryostat['REALM'] === r) - .map((t) => ({ - id: t.alias, - name: t.alias, - nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', - target: t, - })), + name: 'Universe', + nodeType: 'Universe', + labels: [], + children: realmTypes.map((r: string) => ({ + name: r, + nodeType: 'Realm', + labels: [], + id: r, + children: models + .filter((t) => t.annotations.cryostat['REALM'] === r) + .map((t) => ({ + id: t.alias, + name: t.alias, + nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', + target: t, })), - }, - }, + })), }; }); this.get('api/v1/recordings', (schema) => schema.all(Resource.ARCHIVE).models); @@ -505,17 +497,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { case 'ArchivedRecordingsForTarget': case 'UploadedRecordings': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForTarget': data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: schema.all(Resource.ARCHIVE).models, }, }, @@ -525,17 +523,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { break; case 'ArchivedRecordingsForAutomatedAnalysis': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForAutomatedAnalysis': data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: schema.all(Resource.RECORDING).models, }, }, @@ -556,8 +560,8 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [ { doPutMetadata: { @@ -602,8 +606,8 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [ { doPutMetadata: { diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 0fdd8e029..8a021df10 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -79,11 +79,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias1, connectUrl: mockConnectUrl1, - recordings: { - archived: { - aggregate: { - count: mockCount1, - }, + archivedRecordings: { + aggregate: { + count: mockCount1, }, }, }, @@ -92,11 +90,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias2, connectUrl: mockConnectUrl2, - recordings: { - archived: { - aggregate: { - count: mockCount2, - }, + archivedRecordings: { + aggregate: { + count: mockCount2, }, }, }, @@ -105,11 +101,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias3, connectUrl: mockConnectUrl3, - recordings: { - archived: { - aggregate: { - count: mockCount3, - }, + archivedRecordings: { + aggregate: { + count: mockCount3, }, }, }, @@ -123,11 +117,9 @@ const mockNewTargetCountResponse = { targetNodes: [ { target: { - recordings: { - archived: { - aggregate: { - count: mockNewCount, - }, + archivedRecordings: { + aggregate: { + count: mockNewCount, }, }, }, diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bdc185501..9392a3094 100644 --- a/src/test/CreateRecording/CustomRecordingForm.test.tsx +++ b/src/test/CreateRecording/CustomRecordingForm.test.tsx @@ -118,7 +118,7 @@ describe('', () => { events: 'template=someEventTemplate,type=CUSTOM', duration: 30, archiveOnStop: true, - restart: false, + replace: 'NEVER', advancedOptions: { maxAge: undefined, maxSize: 0, diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 32b6bd382..6b27417d6 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -153,10 +153,8 @@ const mockCachedReport: CachedReportValue = { }; const mockTargetNode = { - recordings: { - active: { - data: [mockRecording], - }, + activeRecordings: { + data: [mockRecording], }, }; @@ -171,10 +169,8 @@ const mockEmptyActiveRecordingsResponse = { targetNodes: [ { target: { - recordings: { - active: { - data: [], - }, + activeRecordings: { + data: [], }, }, }, @@ -184,17 +180,29 @@ const mockEmptyActiveRecordingsResponse = { const mockArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [mockArchivedRecording], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockArchivedRecording], + }, + }, + }, + ], }, }; const mockEmptyArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [], + }, + }, + }, + ], }, }; diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index cec202ce1..4745612ec 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -107,8 +107,8 @@ const mockArchivedRecordingsResponse = { data: { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [mockArchivedRecording] as ArchivedRecording[], }, }, diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index e167c84c1..59a9509b9 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -90,9 +90,27 @@ const mockRecording: ArchivedRecording = { }; const mockArchivedRecordingsResponse = { + data: { + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockRecording], + }, + }, + }, + ], + }, +}; + +const mockAllArchivedRecordingsResponse = { data: { archivedRecordings: { - data: [mockRecording] as ArchivedRecording[], + data: [mockRecording], + aggregate: { + count: 1, + size: mockRecording.size, + }, }, }, }; @@ -135,7 +153,13 @@ jest.spyOn(defaultServices.api, 'deleteArchivedRecording').mockReturnValue(of(tr jest.spyOn(defaultServices.api, 'downloadRecording').mockReturnValue(); jest.spyOn(defaultServices.api, 'grafanaDatasourceUrl').mockReturnValue(of('/datasource')); jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of('/grafanaUrl')); -jest.spyOn(defaultServices.api, 'graphql').mockReturnValue(of(mockArchivedRecordingsResponse)); +jest.spyOn(defaultServices.api, 'graphql').mockImplementation((query: string) => { + if (query.includes('ArchivedRecordingsForTarget')) { + return of(mockArchivedRecordingsResponse); + } else { + return of(mockAllArchivedRecordingsResponse); + } +}); jest.spyOn(defaultServices.api, 'uploadArchivedRecordingToGrafana').mockReturnValue(of(true)); jest From f672769c876487bac42e345d659012f36e88a8a2 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 15 Mar 2024 11:55:58 -0400 Subject: [PATCH 23/64] cleanup --- .../AllTargetsArchivedRecordingsTable.tsx | 43 ++- .../CreateRecording/CustomRecordingForm.tsx | 2 +- src/app/CreateRecording/types.ts | 2 + .../AutomatedAnalysisCard.tsx | 60 ++-- src/app/RecordingMetadata/BulkEditLabels.tsx | 20 +- src/app/Recordings/ActiveRecordingsTable.tsx | 3 +- .../Recordings/ArchivedRecordingsTable.tsx | 29 +- src/app/Shared/Services/Api.service.tsx | 259 ++++++++++++------ src/app/Shared/Services/api.types.ts | 37 ++- src/app/Shared/Services/api.utils.ts | 2 +- src/app/Topology/Actions/utils.tsx | 81 +++--- src/app/utils/fakeData.ts | 14 +- src/mirage/index.ts | 107 ++++---- ...AllTargetsArchivedRecordingsTable.test.tsx | 32 +-- .../CustomRecordingForm.test.tsx | 2 +- .../AutomatedAnalysisCard.test.tsx | 36 ++- .../RecordingMetadata/BulkEditLabels.test.tsx | 4 +- .../ArchivedRecordingsTable.test.tsx | 28 +- 18 files changed, 451 insertions(+), 310 deletions(-) diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 6ddb90d8a..e9396ed54 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -123,7 +123,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC( `query AllTargetsArchives { - targetNodes { - target { - connectUrl - alias - recordings { - archived { - aggregate { - count - } - } + targetNodes { + target { + connectUrl + alias + archivedRecordings { + aggregate { + count } } } - }`, + } + }`, ) .pipe(map((v) => v.data.targetNodes)) .subscribe({ @@ -175,20 +173,17 @@ export const AllTargetsArchivedRecordingsTable: React.FC( - ` - query ArchiveCountForTarget($connectUrl: String) { - targetNodes(filter: { name: $connectUrl }) { - target { - recordings { - archived { - aggregate { - count + `query ArchiveCountForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + aggregate { + count + } } } } - } - } - }`, + }`, { connectUrl: target.connectUrl }, ) .subscribe((v) => { @@ -198,7 +193,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC { events: eventSpecifierString, duration: continuous ? undefined : duration * (durationUnit / 1000), archiveOnStop: archiveOnStop && !continuous, - restart: restart, + replace: restart ? 'ALWAYS' : 'NEVER', advancedOptions: { toDisk: toDisk, maxAge: toDisk ? (continuous ? maxAge * maxAgeUnit : undefined) : undefined, diff --git a/src/app/CreateRecording/types.ts b/src/app/CreateRecording/types.ts index e11288c17..00d9492b7 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -18,6 +18,8 @@ import { ValidatedOptions } from '@patternfly/react-core'; export type EventTemplateIdentifier = Pick; +export type RecordingReplace = 'ALWAYS' | 'NEVER' | 'STOPPED'; + interface _FormBaseData { name: string; template?: EventTemplateIdentifier; diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 4755d9578..ee43d593b 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -180,21 +180,19 @@ export const AutomatedAnalysisCard: DashboardCardFC query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { target { - recordings { - active (filter: { - name: "${automatedAnalysisRecordingName}", - labels: ["origin=${automatedAnalysisRecordingName}"], - }) { - data { - state - name - downloadUrl - reportUrl - metadata { - labels { - key - value - } + activeRecordings(filter: { + name: "${automatedAnalysisRecordingName}", + labels: ["origin=${automatedAnalysisRecordingName}"], + }) { + data { + state + name + downloadUrl + reportUrl + metadata { + labels { + key + value } } } @@ -213,21 +211,25 @@ export const AutomatedAnalysisCard: DashboardCardFC /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ return context.api.graphql( `query ArchivedRecordingsForAutomatedAnalysis($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } } } - size - archivedTime } - } }`, { connectUrl }, ); @@ -311,7 +313,7 @@ export const AutomatedAnalysisCard: DashboardCardFC queryArchivedRecordings(connectUrl) .pipe( first(), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: (recordings) => { @@ -366,7 +368,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].target.recordings.active.data[0] as Recording), + map((v) => v.data.targetNodes[0].target.activeRecordings.data[0] as Recording), tap((recording) => { if (recording === null || recording === undefined) { throw new Error(NO_RECORDINGS_MESSAGE); diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 3ecb392ec..28b6f7d0b 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -171,7 +171,10 @@ export const BulkEditLabels: React.FC = ({ downloadUrl reportUrl metadata { - labels + labels{ + key + value + } } } } @@ -188,15 +191,19 @@ export const BulkEditLabels: React.FC = ({ context.api.graphql( `query ArchivedRecordingsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived { + target { + archivedRecordings { data { name downloadUrl reportUrl metadata { - labels + labels { + key + value + } } + size } } } @@ -205,7 +212,7 @@ export const BulkEditLabels: React.FC = ({ { connectUrl: target.connectUrl }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), first(), ); } @@ -254,9 +261,10 @@ export const BulkEditLabels: React.FC = ({ o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, ), ); + refreshRecordingList(); }), ); - }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable]); + }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable, refreshRecordingList]); React.useEffect(() => { updateCommonLabels(setCommonLabels); diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 5a6369d47..45aac7a56 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -320,9 +320,10 @@ export const ActiveRecordingsTable: React.FC = (prop o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, ), ); + refreshRecordingList(); }), ); - }, [addSubscription, context, context.notificationChannel, setRecordings]); + }, [addSubscription, context, context.notificationChannel, setRecordings, refreshRecordingList]); React.useEffect(() => { setFilteredRecordings( diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 9c16de6e2..eb4ba9f19 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -192,18 +192,22 @@ export const ArchivedRecordingsTable: React.FC = ( return context.api.graphql( ` query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels { - key - value + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size } } - size } } }`, @@ -256,7 +260,7 @@ export const ArchivedRecordingsTable: React.FC = ( filter((target) => !!target), first(), concatMap((target: Target) => queryTargetRecordings(target.connectUrl)), - map((v) => v.data.archivedRecordings.data as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[]), ) .subscribe({ next: handleRecordings, @@ -354,9 +358,10 @@ export const ArchivedRecordingsTable: React.FC = ( o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, ), ); + refreshRecordingList(); }), ); - }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget]); + }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget, refreshRecordingList]); React.useEffect(() => { setFilteredRecordings( diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 4f8b3b7a4..7c389e954 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -45,21 +45,23 @@ import { CredentialsResponse, RulesResponse, EnvironmentNode, - DiscoveryResponse, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, RecordingCountResponse, MBeanMetrics, MBeanMetricsResponse, EventType, NotificationCategory, - NullableTarget, HttpError, SimpleResponse, XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, KeyValue, - CustomTargetStub, + TargetStub, + TargetForTest, + Metadata, + TargetMetadata, + isTargetMetadata, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -171,7 +173,7 @@ export class ApiService { } createTarget( - target: CustomTargetStub, + target: TargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -207,7 +209,7 @@ export class ApiService { ); } - deleteTarget(target: Target): Observable { + deleteTarget(target: TargetStub): Observable { return this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}`, { method: 'DELETE', }).pipe( @@ -296,7 +298,7 @@ export class ApiService { name, events, duration, - restart, + replace, archiveOnStop, metadata, advancedOptions, @@ -310,19 +312,11 @@ export class ApiService { if (archiveOnStop != undefined) { form.append('archiveOnStop', String(archiveOnStop)); } - const transformedMetadata = { - labels: {}, - annotations: { - cryostat: {}, - platform: {}, - }, - }; - metadata?.labels.forEach((label) => (transformedMetadata.labels[label.key] = label.value)); if (metadata) { - form.append('metadata', JSON.stringify(transformedMetadata)); + form.append('metadata', JSON.stringify(this.transformMetadataToObject(metadata))); } - if (restart != undefined) { - form.append('restart', String(restart)); + if (replace != undefined) { + form.append('replace', String(replace)); } if (advancedOptions) { if (advancedOptions.toDisk != undefined) { @@ -489,7 +483,7 @@ export class ApiService { } uploadArchivedRecordingToGrafana( - sourceTarget: Observable, + sourceTarget: Observable, recordingName: string, ): Observable { return sourceTarget.pipe( @@ -531,7 +525,13 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: KeyValue[]) { + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + stringifyRecordingLabels(labels: KeyValue | KeyValue[]): string { + return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + } + + // FIXME remove this, all API endpoints that allow us to send labels in the request body should accept it in as-is JSON form + transformAndStringifyToRawLabels(labels: KeyValue[]): string { const rawLabels = {}; for (const label of labels) { rawLabels[label.key] = label.value; @@ -539,20 +539,58 @@ export class ApiService { return JSON.stringify(rawLabels); } + transformMetadataToObject(metadata: Metadata | TargetMetadata): object { + if (isTargetMetadata(metadata)) { + return { + labels: this.transformLabelsToObject(metadata.labels), + annotations: { + cryostat: this.transformLabelsToObject(metadata?.annotations?.cryostat), + platform: this.transformLabelsToObject(metadata?.annotations?.platform), + }, + }; + } else { + return { + labels: this.transformLabelsToObject(metadata.labels), + }; + } + } + + transformLabelsToObject(labels: KeyValue[]): object { + const out = {}; + for (const label of labels) { + out[label.key] = label.value; + } + return out; + } + postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: KeyValue[]): Observable { - return this.sendRequest( - 'beta', - `fs/recordings/${encodeURIComponent(jvmId)}/${encodeURIComponent(recordingName)}/metadata/labels`, + return this.graphql( + ` + query PostRecordingMetadataFromPath($jvmId: String!, $recordingName: String!, $labels: [Entry_String_StringInput]) { + archivedRecordings(filter: {sourceTarget: $jvmId, name: $recordingName }) { + data { + doPutMetadata(metadataInput: { labels: $labels }) { + metadata { + labels{ + key + value + } + } + size + archivedTime + } + } + } + }`, { - method: 'POST', - body: this.transformAndStringifyToRawLabels(labels), + jvmId, + recordingName, + labels: labels.map((label) => ({ key: label.key, value: label.value })), }, - ).pipe( - map((resp) => resp.ok), - first(), - ); - } + ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); +} + isProbeEnabled(): Observable { return this.getActiveProbes(true).pipe( concatMap((_) => of(true)), @@ -734,7 +772,7 @@ export class ApiService { } getActiveProbesForTarget( - target: Target, + target: TargetStub, suppressNotifications = false, skipStatusCheck = false, ): Observable { @@ -904,43 +942,61 @@ export class ApiService { concatMap((target) => this.graphql( ` - query PostRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { + query PostRecordingMetadata($connectUrl: String, $recordingName: String, $labels: [Entry_String_StringInput]) { targetNodes(filter: { name: $connectUrl }) { - recordings { - archived(filter: { name: $recordingName }) { + target{ + archivedRecordings(filter: { name: $recordingName }) { data { - doPutMetadata(metadata: { labels: $labels }) { + doPutMetadata(metadataInput:{labels: $labels }) { metadata { - labels + labels{ + key + value + } } + size + archivedTime } } } } } }`, - { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, + { + connectUrl: target.connectUrl, + recordingName, + labels: labels.map((label) => ({ key: label.key, value: label.value })), + }, ), ), - map((v) => v.data.targetNodes[0].recordings.archived as ArchivedRecording[]), + map((v) => v.data.targetNodes[0].target.archivedRecordings as ArchivedRecording[]), ); } postUploadedRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.graphql( ` - query PostUploadedRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String){ + query PostUploadedRecordingMetadata($connectUrl: String, $recordingName: String, $labels: [Entry_String_StringInput]){ archivedRecordings(filter: {sourceTarget: $connectUrl, name: $recordingName }) { data { - doPutMetadata(metadata: { labels: $labels }) { + doPutMetadata(metadataInput: { labels: $labels }) { metadata { - labels + labels{ + key + value + } } + size + archivedTime } } } }`, - { connectUrl: UPLOADS_SUBDIRECTORY, recordingName, labels: this.stringifyRecordingLabels(labels) }, + { + connectUrl: UPLOADS_SUBDIRECTORY, + recordingName, + labels: labels.map((label) => ({ key: label.key, value: label.value })), + }, ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); } @@ -951,14 +1007,19 @@ export class ApiService { concatMap((target: Target) => this.graphql( ` - query PostActiveRecordingMetadata($connectUrl: String, $recordingName: String, $labels: String) { + query PostActiveRecordingMetadata($connectUrl: String, $recordingName: String, $labels: [Entry_String_StringInput]) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active(filter: { name: $recordingName }) { + target{ + activeRecordings(filter: { name: $recordingName }) { data { - doPutMetadata(metadata: { labels: $labels }) { + doPutMetadata(metadataInput:{labels: $labels}) { + id + name metadata { - labels + labels{ + key + value + } } } } @@ -966,10 +1027,14 @@ export class ApiService { } } }`, - { connectUrl: target.connectUrl, recordingName, labels: this.stringifyRecordingLabels(labels) }, + { + connectUrl: target.connectUrl, + recordingName, + labels: labels.map((label) => ({ key: label.key, value: label.value })), + }, ), ), - map((v) => v.data.targetNodes[0].recordings.active as ActiveRecording[]), + map((v) => v.data.targetNodes[0].target.activeRecordings as ActiveRecording[]), ); } @@ -1043,11 +1108,10 @@ export class ApiService { } getDiscoveryTree(): Observable { - return this.sendRequest('v2.1', 'discovery', { + return this.sendRequest('v3', 'discovery', { method: 'GET', }).pipe( concatMap((resp) => resp.json()), - map((body: DiscoveryResponse) => body.data.result), first(), ); } @@ -1056,7 +1120,7 @@ export class ApiService { matchTargetsWithExpr(matchExpression: string, targets: Target[]): Observable { const body = JSON.stringify({ matchExpression, - targets, + targets: targets.map((t) => this.transformTarget(t)), }); const headers = new Headers(); headers.set('Content-Type', 'application/json'); @@ -1087,20 +1151,20 @@ export class ApiService { ); } - groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingFilterInput): Observable { + groupHasRecording(group: EnvironmentNode, filter: ActiveRecordingsFilterInput): Observable { return this.graphql( ` - query GetRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query GroupHasRecording ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput){ environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { - active(filter: $recordingFilter) { - data { - name - } + target { + activeRecordings(filter: $recordingFilter) { + aggregate { + count } + } } } } @@ -1114,22 +1178,22 @@ export class ApiService { first(), map((body) => body.data.environmentNodes[0].descendantTargets.reduce( - (acc: Partial[], curr) => acc.concat(curr.recordings?.active?.data || []), - [] as Partial[], + (acc: number, curr) => acc + curr.target.activeRecordings.aggregate.count, + 0, ), ), catchError((_) => of([])), - map((recs: Partial[]) => recs.length > 0), // At least one + map((acc) => acc > 0), // At least one ); } - targetHasRecording(target: Target, filter: ActiveRecordingFilterInput = {}): Observable { + targetHasRecording(target: TargetStub, filter: ActiveRecordingsFilterInput = {}): Observable { return this.graphql( ` - query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingFilterInput) { + query ActiveRecordingsForJFRMetrics($connectUrl: String, $recordingFilter: ActiveRecordingsFilterInput) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: $recordingFilter) { + target { + activeRecordings(filter: $recordingFilter) { aggregate { count } @@ -1149,7 +1213,7 @@ export class ApiService { if (nodes.length === 0) { return false; } - const count = nodes[0].recordings.active.aggregate.count; + const count = nodes[0].target.activeRecordings.aggregate.count; return count > 0; }), catchError((_) => of(false)), @@ -1157,7 +1221,7 @@ export class ApiService { } checkCredentialForTarget( - target: Target, + target: TargetStub, credentials: { username: string; password: string }, ): Observable< | { @@ -1210,7 +1274,7 @@ export class ApiService { ); } - getTargetMBeanMetrics(target: Target, queries: string[]): Observable { + getTargetMBeanMetrics(target: TargetStub, queries: string[]): Observable { return this.graphql( ` query MBeanMXMetricsForTarget($connectUrl: String) { @@ -1235,30 +1299,37 @@ export class ApiService { ); } - getTargetArchivedRecordings(target: Target): Observable { + getTargetArchivedRecordings(target: TargetStub): Observable { return this.graphql( ` - query ArchivedRecordingsForTarget($connectUrl: String) { - archivedRecordings(filter: { sourceTarget: $connectUrl }) { - data { - name - downloadUrl - reportUrl - metadata { - labels + query ArchivedRecordingsForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime } - size - archivedTime } } - }`, + } + }`, { connectUrl: target.connectUrl }, true, true, - ).pipe(map((v) => v.data.archivedRecordings.data as ArchivedRecording[])); + ).pipe(map((v) => v.data.targetNodes[0].target.archivedRecordings.data as ArchivedRecording[])); } - getTargetActiveRecordings(target: Target): Observable { + getTargetActiveRecordings(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/recordings`, 'v1', @@ -1268,7 +1339,7 @@ export class ApiService { ); } - getTargetEventTemplates(target: Target): Observable { + getTargetEventTemplates(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/templates`, 'v1', @@ -1278,7 +1349,7 @@ export class ApiService { ); } - getTargetEventTypes(target: Target): Observable { + getTargetEventTypes(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/events`, 'v1', @@ -1317,8 +1388,22 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: KeyValue[]): string { - return JSON.stringify(labels).replace(/"([^"]+)":/g, '$1:'); + private transformTarget(target: Target): TargetForTest { + const out: TargetForTest = { + alias: target.alias, + connectUrl: target.connectUrl, + labels: {}, + annotations: { cryostat: {}, platform: {} }, + }; + for (const l of target.labels) { + out.labels[l.key] = l.value; + } + for (const s of ['cryostat', 'platform']) { + for (const e of out.annotations[s]) { + target.annotations[s][e.key] = e.value; + } + } + return out; } private sendRequest( diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 2bd3b0ba3..1f124d35c 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { RecordingReplace } from '@app/CreateRecording/types'; import { AlertVariant } from '@patternfly/react-core'; import { Observable } from 'rxjs'; -export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'beta'; +export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'v3' | 'beta'; // ====================================== // Common Resources @@ -29,7 +30,17 @@ export interface KeyValue { export interface Metadata { labels: KeyValue[]; - annotations?: KeyValue[]; +} + +export type TargetMetadata = Metadata & { + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; + }; +}; + +export function isTargetMetadata(metadata: Metadata | TargetMetadata): metadata is TargetMetadata { + return (metadata as TargetMetadata).annotations !== undefined; } export interface ApiV2Response { @@ -88,7 +99,12 @@ export class XMLHttpError extends Error { } } -export type CustomTargetStub = Omit; +export type TargetStub = Omit; + +export type TargetForTest = Pick & { + labels: object; + annotations: { cryostat: object; platform: object }; +}; // ====================================== // Health Resources @@ -210,7 +226,7 @@ export interface RecordingAttributes { events: string; duration?: number; archiveOnStop?: boolean; - restart?: boolean; + replace?: RecordingReplace; advancedOptions?: AdvancedRecordingOptions; metadata?: Metadata; } @@ -238,7 +254,7 @@ export interface ActiveRecording extends Recording { maxAge: number; } -export interface ActiveRecordingFilterInput { +export interface ActiveRecordingsFilterInput { name?: string; state?: string; continuous?: boolean; @@ -265,8 +281,8 @@ export interface RecordingResponse extends ApiV2Response { export interface RecordingCountResponse { data: { targetNodes: { - recordings: { - active: { + target: { + activeRecordings: { aggregate: { count: number; }; @@ -446,6 +462,7 @@ export const TEMPLATE_UNSUPPORTED_MESSAGE = 'The template type used in this reco // Discovery/Target resources // ====================================== export interface Target { + id?: number; // present in responses but we must not include it in requests to create targets jvmId?: string; // present in responses, but we do not need to provide it in requests connectUrl: string; alias: string; @@ -499,12 +516,6 @@ export interface TargetNode extends _AbstractNode { readonly target: Target; } -export interface DiscoveryResponse extends ApiV2Response { - data: { - result: EnvironmentNode; - }; -} - // ====================================== // Notification resources // ====================================== diff --git a/src/app/Shared/Services/api.utils.ts b/src/app/Shared/Services/api.utils.ts index af376af02..22cc28b58 100644 --- a/src/app/Shared/Services/api.utils.ts +++ b/src/app/Shared/Services/api.utils.ts @@ -376,7 +376,7 @@ export const messageKeys = new Map([ { variant: AlertVariant.success, title: 'Recording Metadata Updated', - body: (evt) => `${evt.message.recordingName} metadata was updated`, + body: (evt) => `${evt.message.recording.name} metadata was updated`, } as NotificationMessageMapper, ], [ diff --git a/src/app/Topology/Actions/utils.tsx b/src/app/Topology/Actions/utils.tsx index aee7c9300..f7f358a16 100644 --- a/src/app/Topology/Actions/utils.tsx +++ b/src/app/Topology/Actions/utils.tsx @@ -122,23 +122,23 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StartRecordingForGroup($filter: EnvironmentNodeFilterInput, $recordingName: String!, $labels: String) { + query StartRecordingForGroup($filter: DiscoveryNodeFilterInput!, $recordingName: String!, $metadata: RecordingMetadataInput!) { environmentNodes(filter: $filter) { name descendantTargets { name - doStartRecording(recording: { - name: $recordingName, - template: "Continuous", - templateType: "TARGET", - duration: 0, - restart: true, - metadata: { - labels: $labels - }, - }) { - name - state + target { + doStartRecording(recording: { + name: $recordingName, + template: "Continuous", + templateType: "TARGET", + duration: 0, + replace: "STOPPED", + metadata: $metadata, + }) { + name + state + } } } } @@ -147,12 +147,9 @@ export const nodeActions: NodeAction[] = [ { filter: { id: group.id }, recordingName: QUICK_RECORDING_NAME, - labels: services.api.stringifyRecordingLabels([ - { - key: QUICK_RECORDING_LABEL_KEY, - value: group.name.replace(/[\s+-]/g, '_'), - }, - ]), + metadata: { + labels: [{ key: QUICK_RECORDING_LABEL_KEY, value: group.name.replace(/[\s+-]/g, '_') }], + }, }, false, true, @@ -171,19 +168,21 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doArchive { - name - } + data { + doArchive { + name } + } } + } } } } @@ -213,20 +212,22 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query StopRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query StopRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doStop { - name - state - } + data { + doStop { + name + state } + } } + } } } } @@ -257,20 +258,22 @@ export const nodeActions: NodeAction[] = [ services.api .graphql( ` - query DeleteRecordingForGroup ($groupFilter: EnvironmentNodeFilterInput, $recordingFilter: ActiveRecordingFilterInput){ + query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) { environmentNodes(filter: $groupFilter) { name descendantTargets { name - recordings { + target { + recordings { active(filter: $recordingFilter) { - data { - doDelete { - name - state - } + data { + doDelete { + name + state } + } } + } } } } diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index 0075d0fa0..1723e190c 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -23,7 +23,7 @@ import { RecordingState, Recording, MBeanMetrics, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, ArchivedRecording, EventTemplate, EventProbe, @@ -208,7 +208,7 @@ export const fakeEvaluations: AnalysisResult[] = [ export const fakeCachedReport: CachedReportValue = { report: fakeEvaluations, - timestamp: 1663027200000, + timestamp: Date.now() - 1000 * 60 * 60, }; class FakeTargetService extends TargetService { @@ -301,7 +301,7 @@ class FakeApiService extends ApiService { } // JFR Metrics card - targetHasRecording(_target: Target, _filter?: ActiveRecordingFilterInput): Observable { + targetHasRecording(_target: Target, _filter?: ActiveRecordingsFilterInput): Observable { return of(true); } @@ -347,8 +347,8 @@ class FakeApiService extends ApiService { return of([]); } - // Automatic Analysis Card - // This fakes the fetch for Automatic Analysis recording to return available. + // Automated Analysis Card + // This fakes the fetch for Automated Analysis recording to return available. // Then subsequent graphql call for archived recording is ignored graphql( _query: string, @@ -360,8 +360,8 @@ class FakeApiService extends ApiService { data: { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [fakeAARecording], }, }, diff --git a/src/mirage/index.ts b/src/mirage/index.ts index d42a8b0f1..4d4ecff95 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -20,6 +20,7 @@ import { Server as WSServer, Client } from 'mock-socket'; import factories from './factories'; import models from './models'; import { Resource } from './typings'; +import { sizeUnits } from "src/app/utils/utils"; export const startMirage = ({ environment = 'development' } = {}) => { const wsUrl = `ws://localhost:9091/api/notifications`; @@ -126,35 +127,27 @@ export const startMirage = ({ environment = 'development' } = {}) => { }; }); this.get('api/v1/targets', (schema) => schema.all(Resource.TARGET).models); - this.get('api/v2.1/discovery', (schema) => { + this.get('api/v3/discovery', (schema) => { const models = schema.all(Resource.TARGET).models; const realmTypes = models.map((t) => t.annotations.cryostat['REALM']); return { - meta: { - status: 'OK', - type: 'application/json', - }, - data: { - result: { - name: 'Universe', - nodeType: 'Universe', - labels: [], - children: realmTypes.map((r: string) => ({ - name: r, - nodeType: 'Realm', - labels: [], - id: r, - children: models - .filter((t) => t.annotations.cryostat['REALM'] === r) - .map((t) => ({ - id: t.alias, - name: t.alias, - nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', - target: t, - })), + name: 'Universe', + nodeType: 'Universe', + labels: [], + children: realmTypes.map((r: string) => ({ + name: r, + nodeType: 'Realm', + labels: [], + id: r, + children: models + .filter((t) => t.annotations.cryostat['REALM'] === r) + .map((t) => ({ + id: t.alias, + name: t.alias, + nodeType: r === 'Custom Targets' ? 'CustomTarget' : 'JVM', + target: t, })), - }, - }, + })), }; }); this.get('api/v1/recordings', (schema) => schema.all(Resource.ARCHIVE).models); @@ -505,17 +498,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { case 'ArchivedRecordingsForTarget': case 'UploadedRecordings': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForTarget': data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: schema.all(Resource.ARCHIVE).models, }, }, @@ -525,17 +524,23 @@ export const startMirage = ({ environment = 'development' } = {}) => { break; case 'ArchivedRecordingsForAutomatedAnalysis': data = { - archivedRecordings: { - data: schema.all(Resource.ARCHIVE).models, - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: schema.all(Resource.ARCHIVE).models, + }, + }, + }, + ], }; break; case 'ActiveRecordingsForAutomatedAnalysis': data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: schema.all(Resource.RECORDING).models, }, }, @@ -544,10 +549,9 @@ export const startMirage = ({ environment = 'development' } = {}) => { }; break; case 'PostRecordingMetadata': { - const labels = {}; - for (const l of eval(variables.labels)) { - labels[l.key] = l.value; - } + const labelsArray = JSON.parse(variables.labels) + .map(l => ({ key: l.key, value: l.value })); + schema.findBy(Resource.ARCHIVE, { name: variables.recordingName })?.update({ metadata: { labels, @@ -556,15 +560,17 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [ { doPutMetadata: { metadata: { - labels, + labels: labelsArray, }, }, + size + archivedTime }, ], }, @@ -582,7 +588,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { recordingName: variables.recordingName, target: variables.connectUrl, metadata: { - labels, + labels: labelsArray, }, }, }), @@ -590,10 +596,9 @@ export const startMirage = ({ environment = 'development' } = {}) => { break; } case 'PostActiveRecordingMetadata': { - const labels = {}; - for (const l of eval(variables.labels)) { - labels[l.key] = l.value; - } + const labelsArray = JSON.parse(variables.labels) + .map(l => ({ key: l.key, value: l.value })); + schema.findBy(Resource.RECORDING, { name: variables.recordingName })?.update({ metadata: { labels, @@ -602,13 +607,13 @@ export const startMirage = ({ environment = 'development' } = {}) => { data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [ { doPutMetadata: { metadata: { - labels, + labels: labelsArray, }, }, }, @@ -628,7 +633,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { recordingName: variables.recordingName, target: variables.connectUrl, metadata: { - labels, + labels: labelsArray, }, }, }), diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 0fdd8e029..8a021df10 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -79,11 +79,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias1, connectUrl: mockConnectUrl1, - recordings: { - archived: { - aggregate: { - count: mockCount1, - }, + archivedRecordings: { + aggregate: { + count: mockCount1, }, }, }, @@ -92,11 +90,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias2, connectUrl: mockConnectUrl2, - recordings: { - archived: { - aggregate: { - count: mockCount2, - }, + archivedRecordings: { + aggregate: { + count: mockCount2, }, }, }, @@ -105,11 +101,9 @@ const mockTargetsAndCountsResponse = { target: { alias: mockAlias3, connectUrl: mockConnectUrl3, - recordings: { - archived: { - aggregate: { - count: mockCount3, - }, + archivedRecordings: { + aggregate: { + count: mockCount3, }, }, }, @@ -123,11 +117,9 @@ const mockNewTargetCountResponse = { targetNodes: [ { target: { - recordings: { - archived: { - aggregate: { - count: mockNewCount, - }, + archivedRecordings: { + aggregate: { + count: mockNewCount, }, }, }, diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bdc185501..9392a3094 100644 --- a/src/test/CreateRecording/CustomRecordingForm.test.tsx +++ b/src/test/CreateRecording/CustomRecordingForm.test.tsx @@ -118,7 +118,7 @@ describe('', () => { events: 'template=someEventTemplate,type=CUSTOM', duration: 30, archiveOnStop: true, - restart: false, + replace: 'NEVER', advancedOptions: { maxAge: undefined, maxSize: 0, diff --git a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx index 32b6bd382..6b27417d6 100644 --- a/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx +++ b/src/test/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.test.tsx @@ -153,10 +153,8 @@ const mockCachedReport: CachedReportValue = { }; const mockTargetNode = { - recordings: { - active: { - data: [mockRecording], - }, + activeRecordings: { + data: [mockRecording], }, }; @@ -171,10 +169,8 @@ const mockEmptyActiveRecordingsResponse = { targetNodes: [ { target: { - recordings: { - active: { - data: [], - }, + activeRecordings: { + data: [], }, }, }, @@ -184,17 +180,29 @@ const mockEmptyActiveRecordingsResponse = { const mockArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [mockArchivedRecording], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockArchivedRecording], + }, + }, + }, + ], }, }; const mockEmptyArchivedRecordingsResponse = { data: { - archivedRecordings: { - data: [], - }, + targetNodes: [ + { + target: { + archivedRecordings: { + data: [], + }, + }, + }, + ], }, }; diff --git a/src/test/RecordingMetadata/BulkEditLabels.test.tsx b/src/test/RecordingMetadata/BulkEditLabels.test.tsx index cec202ce1..4745612ec 100644 --- a/src/test/RecordingMetadata/BulkEditLabels.test.tsx +++ b/src/test/RecordingMetadata/BulkEditLabels.test.tsx @@ -107,8 +107,8 @@ const mockArchivedRecordingsResponse = { data: { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [mockArchivedRecording] as ArchivedRecording[], }, }, diff --git a/src/test/Recordings/ArchivedRecordingsTable.test.tsx b/src/test/Recordings/ArchivedRecordingsTable.test.tsx index e167c84c1..59a9509b9 100644 --- a/src/test/Recordings/ArchivedRecordingsTable.test.tsx +++ b/src/test/Recordings/ArchivedRecordingsTable.test.tsx @@ -90,9 +90,27 @@ const mockRecording: ArchivedRecording = { }; const mockArchivedRecordingsResponse = { + data: { + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockRecording], + }, + }, + }, + ], + }, +}; + +const mockAllArchivedRecordingsResponse = { data: { archivedRecordings: { - data: [mockRecording] as ArchivedRecording[], + data: [mockRecording], + aggregate: { + count: 1, + size: mockRecording.size, + }, }, }, }; @@ -135,7 +153,13 @@ jest.spyOn(defaultServices.api, 'deleteArchivedRecording').mockReturnValue(of(tr jest.spyOn(defaultServices.api, 'downloadRecording').mockReturnValue(); jest.spyOn(defaultServices.api, 'grafanaDatasourceUrl').mockReturnValue(of('/datasource')); jest.spyOn(defaultServices.api, 'grafanaDashboardUrl').mockReturnValue(of('/grafanaUrl')); -jest.spyOn(defaultServices.api, 'graphql').mockReturnValue(of(mockArchivedRecordingsResponse)); +jest.spyOn(defaultServices.api, 'graphql').mockImplementation((query: string) => { + if (query.includes('ArchivedRecordingsForTarget')) { + return of(mockArchivedRecordingsResponse); + } else { + return of(mockAllArchivedRecordingsResponse); + } +}); jest.spyOn(defaultServices.api, 'uploadArchivedRecordingToGrafana').mockReturnValue(of(true)); jest From 2b24efdc1cb7f1e13856221b57455ff692d1cbd1 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 25 Mar 2024 21:16:36 -0400 Subject: [PATCH 24/64] update for archive revert some more changes --- src/app/RecordingMetadata/BulkEditLabels.tsx | 1 + src/mirage/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 28b6f7d0b..ff48b2bdf 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -204,6 +204,7 @@ export const BulkEditLabels: React.FC = ({ } } size + archivedTime } } } diff --git a/src/mirage/index.ts b/src/mirage/index.ts index 4d4ecff95..99ca59da7 100644 --- a/src/mirage/index.ts +++ b/src/mirage/index.ts @@ -554,7 +554,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { schema.findBy(Resource.ARCHIVE, { name: variables.recordingName })?.update({ metadata: { - labels, + labels: labelsArray, }, }); data = { @@ -601,7 +601,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { schema.findBy(Resource.RECORDING, { name: variables.recordingName })?.update({ metadata: { - labels, + labels: labelsArray, }, }); data = { From e98cd4b5de6bbe927e6cfc55f87af9a076850f57 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Mar 2024 10:07:38 -0400 Subject: [PATCH 25/64] update ArchivedRecordingsTable --- src/app/RecordingMetadata/BulkEditLabels.tsx | 26 ++++++++++++---- src/app/Recordings/ActiveRecordingsTable.tsx | 22 ++++++++++--- .../Recordings/ArchivedRecordingsTable.tsx | 31 +++++++++++++------ 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index ff48b2bdf..b289346e6 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -254,15 +254,29 @@ export const BulkEditLabels: React.FC = ({ ]).subscribe((parts) => { const currentTarget = parts[0]; const event = parts[1]; + console.log("++++Current Target:", currentTarget); + if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { return; } - setRecordings((old) => - old.map((o) => - o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, - ), - ); - refreshRecordingList(); + + setRecordings((oldRecordings) => { + return oldRecordings.map((recording) => { + if (recording.name === event.message.recordingName) { + const updatedRecording = { + ...recording, + metadata: { + labels: event.message.metadata.labels + } + }; + console.log("++++") + console.log(recording); + console.log("++++"); + console.log("Updated Recording:", updatedRecording); + return updatedRecording; + } + }); + }); }), ); }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable, refreshRecordingList]); diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 45aac7a56..85ba14f59 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -312,18 +312,30 @@ export const ActiveRecordingsTable: React.FC = (prop context.target.target(), context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), ]).subscribe(([currentTarget, event]) => { - if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { + console.log("Active starts here ++"); + console.log(event); + console.log("Active ends here ++"); + console.log("++++Current Target:", currentTarget); + + if (currentTarget?.connectUrl != event.message.target || currentTarget?.jvmId != event.message.jvmId) { return; } setRecordings((old) => - old.map((o) => - o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, + old.map((o) => { + if (o.name === event.message.recordingName) { + return { + ...o, + metadata: { labels: event.message.metadata.labels } + }; + } + } ), ); - refreshRecordingList(); + console.log("+++++2"+ event.message.metadata.labels); + }), ); - }, [addSubscription, context, context.notificationChannel, setRecordings, refreshRecordingList]); + }, [addSubscription, context, context.notificationChannel, setRecordings]); React.useEffect(() => { setFilteredRecordings( diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index eb4ba9f19..904408d6d 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -350,18 +350,29 @@ export const ArchivedRecordingsTable: React.FC = ( propsTarget, context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), ]).subscribe(([currentTarget, event]) => { - if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { - return; - } - setRecordings((old) => - old.map((o) => - o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, - ), - ); - refreshRecordingList(); + console.log("starts here ++"); + console.log(event); + console.log("ends here ++"); + + if (currentTarget?.connectUrl === event.message.target && currentTarget?.jvmId === event.message.recording.jvmId) { + setRecordings((oldRecordings) => { + return oldRecordings.map((recording) => { + if (recording.name === event.message.recordingName) { + return { + ...recording, + metadata: { + labels: event.message.metadata.labels + } + }; + } else { + return recording; + } + }); + }); + } }), ); - }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget, refreshRecordingList]); + }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget]); React.useEffect(() => { setFilteredRecordings( From cf85cd52d272215ee3a523e450f4d620735e590c Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Mar 2024 10:07:38 -0400 Subject: [PATCH 26/64] squash --- src/app/RecordingMetadata/BulkEditLabels.tsx | 19 +++++++----- src/app/Recordings/ActiveRecordingsTable.tsx | 29 +++++++------------ .../Recordings/ArchivedRecordingsTable.tsx | 25 +++++++--------- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index b289346e6..ec0ba3867 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -255,31 +255,36 @@ export const BulkEditLabels: React.FC = ({ const currentTarget = parts[0]; const event = parts[1]; console.log("++++Current Target:", currentTarget); + console.log("++++Event:", event); - if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { - return; - } + const isMatch = ((currentTarget?.connectUrl === event.message.target) || (currentTarget?.jvmId === event.message.recording.jvmId)) + + console.log("++++is there a Match:", isMatch); setRecordings((oldRecordings) => { + console.log("++++Old recording from Bulk",oldRecordings); return oldRecordings.map((recording) => { - if (recording.name === event.message.recordingName) { + console.log("++++Recording from Bulk",recording); + if (isMatch && recording.name === event.message.recording.name) { + console.log("Matching recording found, updating labels for:", recording.name); const updatedRecording = { ...recording, metadata: { - labels: event.message.metadata.labels + labels: event.message.recording.metadata.labels } }; - console.log("++++") + console.log("++++") console.log(recording); console.log("++++"); console.log("Updated Recording:", updatedRecording); return updatedRecording; } + return recording; }); }); }), ); - }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable, refreshRecordingList]); + }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable]); React.useEffect(() => { updateCommonLabels(setCommonLabels); diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index 85ba14f59..f1f52f473 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -312,27 +312,18 @@ export const ActiveRecordingsTable: React.FC = (prop context.target.target(), context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), ]).subscribe(([currentTarget, event]) => { - console.log("Active starts here ++"); - console.log(event); - console.log("Active ends here ++"); - console.log("++++Current Target:", currentTarget); - - if (currentTarget?.connectUrl != event.message.target || currentTarget?.jvmId != event.message.jvmId) { + if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { return; } - setRecordings((old) => - old.map((o) => { - if (o.name === event.message.recordingName) { - return { - ...o, - metadata: { labels: event.message.metadata.labels } - }; - } - } - ), - ); - console.log("+++++2"+ event.message.metadata.labels); - + setRecordings((old) => { + return old.map((o) => { + if (o.name == event.message.recording.name) { + const updatedRecording = { ...o, metadata: { labels: event.message.recording.metadata.labels } }; + return updatedRecording; + } + return o; + }); + }); }), ); }, [addSubscription, context, context.notificationChannel, setRecordings]); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 904408d6d..f8805b4e8 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -350,26 +350,21 @@ export const ArchivedRecordingsTable: React.FC = ( propsTarget, context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), ]).subscribe(([currentTarget, event]) => { - console.log("starts here ++"); - console.log(event); - console.log("ends here ++"); + + const eventConnectUrlLabel = event.message.recording.metadata.labels.find(label => label.key === "connectUrl"); - if (currentTarget?.connectUrl === event.message.target && currentTarget?.jvmId === event.message.recording.jvmId) { - setRecordings((oldRecordings) => { + if (currentTarget?.jvmId != event.message.recording.jvmId && currentTarget?.connectUrl != eventConnectUrlLabel?.value) { + return; + } + setRecordings((oldRecordings) => { return oldRecordings.map((recording) => { - if (recording.name === event.message.recordingName) { - return { - ...recording, - metadata: { - labels: event.message.metadata.labels - } - }; - } else { - return recording; + if (recording.name === event.message.recording.name) { + const updatedRecording = { ...recording, metadata: { labels: event.message.recording.metadata.labels } }; + return updatedRecording; } + return recording; }); }); - } }), ); }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget]); From bd19700bbccd7a8ac6c5662c47422330a07f7921 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 1 Apr 2024 14:39:46 -0400 Subject: [PATCH 27/64] logs to see filtered recordings and labels --- src/app/RecordingMetadata/BulkEditLabels.tsx | 11 -------- src/app/Recordings/Filters/LabelFilter.tsx | 7 +++++ src/app/Recordings/RecordingFilters.tsx | 27 +++++++++++++++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index ec0ba3867..274571de6 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -254,29 +254,18 @@ export const BulkEditLabels: React.FC = ({ ]).subscribe((parts) => { const currentTarget = parts[0]; const event = parts[1]; - console.log("++++Current Target:", currentTarget); - console.log("++++Event:", event); const isMatch = ((currentTarget?.connectUrl === event.message.target) || (currentTarget?.jvmId === event.message.recording.jvmId)) - console.log("++++is there a Match:", isMatch); - setRecordings((oldRecordings) => { - console.log("++++Old recording from Bulk",oldRecordings); return oldRecordings.map((recording) => { - console.log("++++Recording from Bulk",recording); if (isMatch && recording.name === event.message.recording.name) { - console.log("Matching recording found, updating labels for:", recording.name); const updatedRecording = { ...recording, metadata: { labels: event.message.recording.metadata.labels } }; - console.log("++++") - console.log(recording); - console.log("++++"); - console.log("Updated Recording:", updatedRecording); return updatedRecording; } return recording; diff --git a/src/app/Recordings/Filters/LabelFilter.tsx b/src/app/Recordings/Filters/LabelFilter.tsx index 67fc3a7e2..fa5a4a450 100644 --- a/src/app/Recordings/Filters/LabelFilter.tsx +++ b/src/app/Recordings/Filters/LabelFilter.tsx @@ -32,6 +32,7 @@ export const LabelFilter: React.FC = ({ recordings, filteredLa (_, selection, isPlaceholder) => { if (!isPlaceholder) { setIsExpanded(false); + console.log("++++User selected label: ", selection); onSubmit(selection); } }, @@ -49,6 +50,12 @@ export const LabelFilter: React.FC = ({ recordings, filteredLa .sort(); }, [recordings, filteredLabels]); + React.useEffect(()=>{ + console.log("++++Received recordings: ",recordings); + }, [recordings]); + + console.log("++++Filtered labels ready for rendering: ", labels); + return ( = ({ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCategories) => { if (!recordings || !recordings.length) { - console.log("++++No recordings to filter"); return recordings; } let filtered = recordings; - console.log("+++ filtered", filtered); - console.log("+++ Initial recordings:", filtered.map(r => r.name)); - if (filters.Name.length) { filtered = filtered.filter((r) => filters.Name.includes(r.name)); - console.log("+++ After Name filter, count:", filtered.length); } if (!!filters.State && !!filters.State.length) { filtered = filtered.filter((r) => !!filters.State && filters.State.includes(r.state)); - console.log("+++ After State filter, count:", filtered.length); } if (!!filters.DurationSeconds && !!filters.DurationSeconds.length) { @@ -281,8 +275,6 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat (filters.DurationSeconds.includes('continuous') && r.continuous) ); }); - console.log("+++ After DurationSeconds filter, count:", filtered.length); - } if (!!filters.StartedBeforeDate && !!filters.StartedBeforeDate.length) { filtered = filtered.filter((rec) => { @@ -293,8 +285,6 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat return dayjs(rec.startTime).isBefore(beforeDate); }).length; }); - console.log("+++ Before DurationSeconds filter, count:", filtered.length); - } if (!!filters.StartedAfterDate && !!filters.StartedAfterDate.length) { filtered = filtered.filter((rec) => { @@ -304,25 +294,14 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat return dayjs(rec.startTime).isSame(afterDate) || dayjs(rec.startTime).isAfter(afterDate); }).length; }); - console.log("+++ After DurationSeconds filter, count:", filtered.length); - } - /* if (filters.Label.length) { - filtered = filtered.filter( - (r) => Object.entries(r.metadata.labels).filter(([k, v]) => filters.Label.includes(`${k}:${v}`)).length, - ); - console.log("+++ After Label filter, count:", filtered.length); - - } */ if (filters.Label.length) { filtered = filtered.filter(recording => { const recordingLabels = recording.metadata.labels.map(label => `${label.key}:${label.value}`); return filters.Label.some(filterLabel => recordingLabels.includes(filterLabel)); }); - console.log("+++ After Label filter, count:", filtered.length); } return filtered; - console.log("+++ After all filters, count:", filtered.length); }; From 7b33d12701a4ccc6bd5813ed0b71792cf69790d7 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 3 Apr 2024 09:53:14 -0400 Subject: [PATCH 29/64] fixup delete archive recordings --- .../Archives/AllArchivedRecordingsTable.tsx | 35 +++++++++++++++++-- .../Recordings/ArchivedRecordingsTable.tsx | 4 ++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index 96d021975..b82b80299 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -167,13 +167,44 @@ export const AllArchivedRecordingsTable: React.FC window.clearInterval(id); }, [context.settings, refreshDirectoriesAndCounts]); - React.useEffect(() => { + /* React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(() => { refreshDirectoriesAndCounts(); }), ); - }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); + }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); */ + + React.useEffect(() => { + const subscription = context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(event => { + console.log("Received RecordingMetadataUpdated event:", event); + + const updatedRecordingInfo = event.message; + console.log("Updated recording info:", updatedRecordingInfo); + + setDirectories(currentDirectories => { + const newDirectories = currentDirectories.map(directory => ({ + ...directory, + recordings: directory.recordings.map(recording => { + if (recording.name === updatedRecordingInfo.name) { + console.log(`+++Updating labels for recording: ${recording.name}`); + return { ...recording, metadata: { ...recording.metadata, labels: updatedRecordingInfo.metadata.labels }}; + } + return recording; + }), + })); + console.log("New directories after update:", newDirectories); + return newDirectories; + }); + }); + + return () => { + console.log("Unsubscribing from RecordingMetadataUpdated"); + subscription.unsubscribe(); + }; + }, [addSubscription, context.notificationChannel]); + + React.useEffect(() => { addSubscription( diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index f8805b4e8..625777990 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -335,7 +335,9 @@ export const ArchivedRecordingsTable: React.FC = ( propsTarget, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]).subscribe(([currentTarget, event]) => { - if (currentTarget?.connectUrl != event.message.target && currentTarget?.jvmId != event.message.jvmId) { + const eventConnectUrlLabel = event.message.recording.metadata.labels.find(label => label.key === "connectUrl"); + + if (currentTarget?.jvmId != event.message.recording.jvmId && currentTarget?.connectUrl != eventConnectUrlLabel?.value) { return; } setRecordings((old) => old.filter((r) => r.name !== event.message.recording.name)); From 402cca3467b24fd15466448f38a50863556c75d6 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 3 Apr 2024 13:53:40 -0400 Subject: [PATCH 30/64] proper path --- src/app/Archives/AllArchivedRecordingsTable.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index b82b80299..89911eed8 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -170,8 +170,7 @@ export const AllArchivedRecordingsTable: React.FC { addSubscription( context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(() => { - refreshDirectoriesAndCounts(); - }), + }), ); }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); */ @@ -186,9 +185,9 @@ export const AllArchivedRecordingsTable: React.FC ({ ...directory, recordings: directory.recordings.map(recording => { - if (recording.name === updatedRecordingInfo.name) { + if (recording.name === updatedRecordingInfo.recording.name) { console.log(`+++Updating labels for recording: ${recording.name}`); - return { ...recording, metadata: { ...recording.metadata, labels: updatedRecordingInfo.metadata.labels }}; + return { ...recording, metadata: { ...recording.metadata, labels: updatedRecordingInfo.recording.metadata.labels }}; } return recording; }), From ebe5a84ba24aebf7cd6325ccad0f5d66163d73a6 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 3 Apr 2024 14:30:11 -0400 Subject: [PATCH 31/64] cleanup --- src/app/Archives/AllArchivedRecordingsTable.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index 89911eed8..9d86f8163 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -167,38 +167,25 @@ export const AllArchivedRecordingsTable: React.FC window.clearInterval(id); }, [context.settings, refreshDirectoriesAndCounts]); - /* React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(() => { - }), - ); - }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); */ - React.useEffect(() => { const subscription = context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(event => { - console.log("Received RecordingMetadataUpdated event:", event); - const updatedRecordingInfo = event.message; - console.log("Updated recording info:", updatedRecordingInfo); setDirectories(currentDirectories => { const newDirectories = currentDirectories.map(directory => ({ ...directory, recordings: directory.recordings.map(recording => { if (recording.name === updatedRecordingInfo.recording.name) { - console.log(`+++Updating labels for recording: ${recording.name}`); return { ...recording, metadata: { ...recording.metadata, labels: updatedRecordingInfo.recording.metadata.labels }}; } return recording; }), })); - console.log("New directories after update:", newDirectories); return newDirectories; }); }); return () => { - console.log("Unsubscribing from RecordingMetadataUpdated"); subscription.unsubscribe(); }; }, [addSubscription, context.notificationChannel]); From 3d5ddd8a6f064b6d877774ada73d202d108c9a78 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 3 Apr 2024 19:29:44 -0400 Subject: [PATCH 32/64] resolve issues update delete AllTargetArchive logic --- .../Archives/AllArchivedRecordingsTable.tsx | 41 ++++++++++--------- .../AllTargetsArchivedRecordingsTable.tsx | 40 +++++++++++++++++- src/app/RecordingMetadata/BulkEditLabels.tsx | 27 ++++++------ src/app/Recordings/ActiveRecordingsTable.tsx | 12 +++--- .../Recordings/ArchivedRecordingsTable.tsx | 35 ++++++++++------ src/app/Recordings/Filters/LabelFilter.tsx | 3 -- src/app/Recordings/RecordingFilters.tsx | 12 ++---- src/app/Shared/Services/Api.service.tsx | 19 ++++----- 8 files changed, 115 insertions(+), 74 deletions(-) diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index 9d86f8163..af4b1db7e 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -168,29 +168,32 @@ export const AllArchivedRecordingsTable: React.FC { - const subscription = context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(event => { - const updatedRecordingInfo = event.message; - - setDirectories(currentDirectories => { - const newDirectories = currentDirectories.map(directory => ({ - ...directory, - recordings: directory.recordings.map(recording => { - if (recording.name === updatedRecordingInfo.recording.name) { - return { ...recording, metadata: { ...recording.metadata, labels: updatedRecordingInfo.recording.metadata.labels }}; - } - return recording; - }), - })); - return newDirectories; + const subscription = context.notificationChannel + .messages(NotificationCategory.RecordingMetadataUpdated) + .subscribe((event) => { + const updatedRecordingInfo = event.message; + + setDirectories((currentDirectories) => { + const newDirectories = currentDirectories.map((directory) => ({ + ...directory, + recordings: directory.recordings.map((recording) => { + if (recording.name === updatedRecordingInfo.recording.name) { + return { + ...recording, + metadata: { ...recording.metadata, labels: updatedRecordingInfo.recording.metadata.labels }, + }; + } + return recording; + }), + })); + return newDirectories; + }); }); - }); - + return () => { subscription.unsubscribe(); }; - }, [addSubscription, context.notificationChannel]); - - + }, [context.notificationChannel]); React.useEffect(() => { addSubscription( diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index e9396ed54..80097cef7 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -325,13 +325,49 @@ export const AllTargetsArchivedRecordingsTable: React.FC { + /* React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted).subscribe((v) => { updateCount(v.message.target, -1); }), ); - }, [addSubscription, context.notificationChannel, updateCount]); + }, [addSubscription, context.notificationChannel, updateCount]); */ + + React.useEffect(() => { + const subscription = context.notificationChannel + .messages(NotificationCategory.ArchivedRecordingDeleted) + .subscribe((v) => { + const deletedRecordingName = v.message.recording.name; + const targetUrl = v.message.target; + + console.log(`++Deleting recording: ${deletedRecordingName} from target: ${targetUrl}`); + + setArchivesForTargets((currentTargets) => { + console.log('++Current targets before update:', currentTargets); + + return currentTargets.map((target) => { + if (target.target.connectUrl === targetUrl) { + const updatedArchives = Array.isArray(target.archives) + ? target.archives.filter(archive => archive.name !== deletedRecordingName) + : []; + + console.log(`++Updated archives for target ${targetUrl}:`, updatedArchives); + + return { + ...target, + archives: updatedArchives, + archiveCount: Math.max(target.archiveCount - 1, 0), + }; + } + return target; + }); + }); + }); + + return () => subscription.unsubscribe(); +}, [context.notificationChannel, setArchivesForTargets]); + + const toggleExpanded = React.useCallback( (target) => { diff --git a/src/app/RecordingMetadata/BulkEditLabels.tsx b/src/app/RecordingMetadata/BulkEditLabels.tsx index 274571de6..cccf2741b 100644 --- a/src/app/RecordingMetadata/BulkEditLabels.tsx +++ b/src/app/RecordingMetadata/BulkEditLabels.tsx @@ -83,7 +83,7 @@ export const BulkEditLabels: React.FC = ({ return !includesLabel(toDelete, label); }); if (directory) { - tasks.push(context.api.postRecordingMetadataFromPath(directory.jvmId, r.name, updatedLabels).pipe(first())); + tasks.push(context.api.postRecordingMetadataForJvmId(directory.jvmId, r.name, updatedLabels).pipe(first())); } if (isTargetRecording) { tasks.push(context.api.postTargetRecordingMetadata(r.name, updatedLabels).pipe(first())); @@ -171,7 +171,7 @@ export const BulkEditLabels: React.FC = ({ downloadUrl reportUrl metadata { - labels{ + labels { key value } @@ -255,22 +255,23 @@ export const BulkEditLabels: React.FC = ({ const currentTarget = parts[0]; const event = parts[1]; - const isMatch = ((currentTarget?.connectUrl === event.message.target) || (currentTarget?.jvmId === event.message.recording.jvmId)) + const isMatch = + currentTarget?.connectUrl === event.message.target || currentTarget?.jvmId === event.message.recording.jvmId; - setRecordings((oldRecordings) => { - return oldRecordings.map((recording) => { - if (isMatch && recording.name === event.message.recording.name) { - const updatedRecording = { - ...recording, - metadata: { - labels: event.message.recording.metadata.labels - } - }; + setRecordings((oldRecordings) => { + return oldRecordings.map((recording) => { + if (isMatch && recording.name === event.message.recording.name) { + const updatedRecording = { + ...recording, + metadata: { + labels: event.message.recording.metadata.labels, + }, + }; return updatedRecording; } return recording; + }); }); - }); }), ); }, [addSubscription, context.target, context.notificationChannel, setRecordings, isUploadsTable]); diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index f1f52f473..8809cb9be 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -317,13 +317,13 @@ export const ActiveRecordingsTable: React.FC = (prop } setRecordings((old) => { return old.map((o) => { - if (o.name == event.message.recording.name) { - const updatedRecording = { ...o, metadata: { labels: event.message.recording.metadata.labels } }; - return updatedRecording; - } - return o; + if (o.name == event.message.recording.name) { + const updatedRecording = { ...o, metadata: { labels: event.message.recording.metadata.labels } }; + return updatedRecording; + } + return o; }); - }); + }); }), ); }, [addSubscription, context, context.notificationChannel, setRecordings]); diff --git a/src/app/Recordings/ArchivedRecordingsTable.tsx b/src/app/Recordings/ArchivedRecordingsTable.tsx index 625777990..24f933175 100644 --- a/src/app/Recordings/ArchivedRecordingsTable.tsx +++ b/src/app/Recordings/ArchivedRecordingsTable.tsx @@ -335,9 +335,14 @@ export const ArchivedRecordingsTable: React.FC = ( propsTarget, context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted), ]).subscribe(([currentTarget, event]) => { - const eventConnectUrlLabel = event.message.recording.metadata.labels.find(label => label.key === "connectUrl"); + const eventConnectUrlLabel = event.message.recording.metadata.labels.find( + (label) => label.key === 'connectUrl', + ); - if (currentTarget?.jvmId != event.message.recording.jvmId && currentTarget?.connectUrl != eventConnectUrlLabel?.value) { + if ( + currentTarget?.jvmId != event.message.recording.jvmId && + currentTarget?.connectUrl != eventConnectUrlLabel?.value + ) { return; } setRecordings((old) => old.filter((r) => r.name !== event.message.recording.name)); @@ -352,21 +357,25 @@ export const ArchivedRecordingsTable: React.FC = ( propsTarget, context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated), ]).subscribe(([currentTarget, event]) => { - - const eventConnectUrlLabel = event.message.recording.metadata.labels.find(label => label.key === "connectUrl"); + const eventConnectUrlLabel = event.message.recording.metadata.labels.find( + (label) => label.key === 'connectUrl', + ); - if (currentTarget?.jvmId != event.message.recording.jvmId && currentTarget?.connectUrl != eventConnectUrlLabel?.value) { + if ( + currentTarget?.jvmId != event.message.recording.jvmId && + currentTarget?.connectUrl != eventConnectUrlLabel?.value + ) { return; } - setRecordings((oldRecordings) => { - return oldRecordings.map((recording) => { - if (recording.name === event.message.recording.name) { - const updatedRecording = { ...recording, metadata: { labels: event.message.recording.metadata.labels } }; - return updatedRecording; - } - return recording; - }); + setRecordings((oldRecordings) => { + return oldRecordings.map((recording) => { + if (recording.name === event.message.recording.name) { + const updatedRecording = { ...recording, metadata: { labels: event.message.recording.metadata.labels } }; + return updatedRecording; + } + return recording; }); + }); }), ); }, [addSubscription, context, context.notificationChannel, setRecordings, propsTarget]); diff --git a/src/app/Recordings/Filters/LabelFilter.tsx b/src/app/Recordings/Filters/LabelFilter.tsx index 41b4da3d8..67fc3a7e2 100644 --- a/src/app/Recordings/Filters/LabelFilter.tsx +++ b/src/app/Recordings/Filters/LabelFilter.tsx @@ -49,9 +49,6 @@ export const LabelFilter: React.FC = ({ recordings, filteredLa .sort(); }, [recordings, filteredLabels]); - React.useEffect(()=>{ - }, [recordings]); - return (