diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index 96d021975..1c1d4867c 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -169,19 +169,28 @@ export const AllArchivedRecordingsTable: React.FC { addSubscription( - context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe(() => { - refreshDirectoriesAndCounts(); - }), - ); - }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); + context.notificationChannel.messages(NotificationCategory.RecordingMetadataUpdated).subscribe((event) => { + const updatedRecordingInfo = event.message; - React.useEffect(() => { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved).subscribe(() => { - refreshDirectoriesAndCounts(); + 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; + }); }), ); - }, [addSubscription, context.notificationChannel, refreshDirectoriesAndCounts]); + }, [addSubscription, context.notificationChannel, setDirectories]); React.useEffect(() => { addSubscription( diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 32a7a16b8..5bfb80944 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -17,7 +17,7 @@ import { ErrorView } from '@app/ErrorView/ErrorView'; import { authFailMessage, isAuthFail } from '@app/ErrorView/types'; import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable'; import { LoadingView } from '@app/Shared/Components/LoadingView'; -import { Target, TargetDiscoveryEvent, NotificationCategory } from '@app/Shared/Services/api.types'; +import { Target, TargetDiscoveryEvent, NotificationCategory, Metadata } from '@app/Shared/Services/api.types'; import { isEqualTarget, indexOfTarget, includesTarget } from '@app/Shared/Services/api.utils'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSort } from '@app/utils/hooks/useSort'; @@ -70,10 +70,21 @@ const tableColumns: TableColumn[] = [ }, ]; +interface ArchivedRecording { + jvmId?: string; + name: string; + downloadUrl: string; + reportUrl: string; + metadata: Metadata; + size: number; + archivedTime: number; +} + type ArchivesForTarget = { target: Target; targetAsObs: Observable; archiveCount: number; + recordings: ArchivedRecording[]; }; export interface AllTargetsArchivedRecordingsTableProps {} @@ -89,16 +100,26 @@ export const AllTargetsArchivedRecordingsTable: React.FC { + const handleNotification = React.useCallback( + (_: string, recording: ArchivedRecording, delta: number) => { setArchivesForTargets((old) => { - const idx = old.findIndex(({ target }) => target.connectUrl === connectUrl); - if (idx >= 0) { - const matched = old[idx]; - old.splice(idx, 1, { ...matched, archiveCount: matched.archiveCount + delta }); - return [...old]; + const matchingTargets = old.filter(({ target }) => { + return target.jvmId === recording.jvmId; + }); + for (const matchedTarget of matchingTargets) { + const targetIdx = old.findIndex(({ target }) => target.connectUrl === matchedTarget.target.connectUrl); + const recordings = [...matchedTarget.recordings]; + if (delta === 1) { + recordings.push(recording); + } else { + const recordingIdx = recordings.findIndex((r) => r.name === recording.name); + recordings.splice(recordingIdx, 1); + } + + old.splice(targetIdx, 1, { ...matchedTarget, archiveCount: matchedTarget.archiveCount + delta, recordings }); } - return old; + + return [...old]; }); }, [setArchivesForTargets], @@ -112,13 +133,20 @@ export const AllTargetsArchivedRecordingsTable: React.FC { const target: Target = { - connectUrl: node.target.serviceUri, + jvmId: node.target.jvmId, + connectUrl: node.target.connectUrl, alias: node.target.alias, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; return { target, targetAsObs: of(target), - archiveCount: node.recordings.archived.aggregate.count, + archiveCount: node?.archiveCount ?? 0, + recordings: node?.recordings ?? [], }; }), ); @@ -134,29 +162,54 @@ export const AllTargetsArchivedRecordingsTable: React.FC { setIsLoading(true); addSubscription( context.api + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ .graphql( `query AllTargetsArchives { - targetNodes { - target { - serviceUri - alias - } - recordings { - archived { - aggregate { - count - } - } - } - } - }`, + targetNodes { + target { + connectUrl + alias + jvmId + archivedRecordings { + data { + jvmId + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } + aggregate { + count + } + } + } + } + }`, + ) + .pipe( + map((v) => { + return v.data.targetNodes.map((node) => { + const target: Target = node.target; + return { + target, + targetAsObs: of(target), + archiveCount: node.target.archivedRecordings.aggregate.count, + recordings: node.target.archivedRecordings.data as ArchivedRecording[], + }; + }); + }), ) - .pipe(map((v) => v.data.targetNodes)) .subscribe({ next: handleArchivesForTargets, error: handleError, @@ -170,18 +223,31 @@ export const AllTargetsArchivedRecordingsTable: React.FC( - ` - query ArchiveCountForTarget($connectUrl: String) { - targetNodes(filter: { name: $connectUrl }) { - recordings { - archived { - aggregate { - count + `query ArchiveCountForTarget($connectUrl: String) { + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + jvmId + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } + aggregate { + count + } + } } } - } - } - }`, + }`, { connectUrl: target.connectUrl }, ) .subscribe((v) => { @@ -191,7 +257,8 @@ export const AllTargetsArchivedRecordingsTable: React.FC { - const target: Target = { - connectUrl: evt.serviceRef.connectUrl, - alias: evt.serviceRef.alias, - }; if (evt.kind === 'FOUND') { - getCountForNewTarget(target); + getCountForNewTarget(evt.serviceRef); } else if (evt.kind === 'MODIFIED') { setArchivesForTargets((old) => { - const idx = old.findIndex(({ target: t }) => isEqualTarget(t, target)); + const idx = old.findIndex(({ target: t }) => isEqualTarget(t, evt.serviceRef)); if (idx >= 0) { const matched = old[idx]; - if (target.connectUrl === matched.target.connectUrl && target.alias === matched.target.alias) { + if ( + evt.serviceRef.connectUrl === matched.target.connectUrl && + evt.serviceRef.alias === matched.target.alias + ) { // If alias and connectUrl are not updated, ignore changes. return old; } - return old.splice(idx, 1, { ...matched, target: target, targetAsObs: of(target) }); + return old.splice(idx, 1, { ...matched, target: evt.serviceRef, targetAsObs: of(evt.serviceRef) }); } return old; }); } else if (evt.kind === 'LOST') { - handleLostTarget(target); + handleLostTarget(evt.serviceRef); } }, [setArchivesForTargets, getCountForNewTarget, handleLostTarget], @@ -302,29 +368,21 @@ export const AllTargetsArchivedRecordingsTable: React.FC { - addSubscription( - context.notificationChannel.messages(NotificationCategory.ActiveRecordingSaved).subscribe((v) => { - updateCount(v.message.target, 1); - }), - ); - }, [addSubscription, context.notificationChannel, updateCount]); - React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingCreated).subscribe((v) => { - updateCount(v.message.target, 1); + handleNotification(v.message.target, v.message.recording, 1); }), ); - }, [addSubscription, context.notificationChannel, updateCount]); + }, [addSubscription, context.notificationChannel, handleNotification]); React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.ArchivedRecordingDeleted).subscribe((v) => { - updateCount(v.message.target, -1); + handleNotification(v.message.target, v.message.recording, -1); }), ); - }, [addSubscription, context.notificationChannel, updateCount]); + }, [addSubscription, context.notificationChannel, handleNotification]); const toggleExpanded = React.useCallback( (target) => { diff --git a/src/app/Archives/ArchiveUploadModal.tsx b/src/app/Archives/ArchiveUploadModal.tsx index a9b4abf85..5cabf1a54 100644 --- a/src/app/Archives/ArchiveUploadModal.tsx +++ b/src/app/Archives/ArchiveUploadModal.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ import { RecordingLabelFields } from '@app/RecordingMetadata/RecordingLabelFields'; -import { RecordingLabel } from '@app/RecordingMetadata/types'; import { FUpload, MultiFileUpload, UploadCallbacks } from '@app/Shared/Components/FileUploads'; import { LoadingProps } from '@app/Shared/Components/types'; +import { KeyValue } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { portalRoot } from '@app/utils/utils'; @@ -51,7 +51,7 @@ export const ArchiveUploadModal: 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..dfe0a1688 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], @@ -260,16 +253,16 @@ 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, 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..00d9492b7 100644 --- a/src/app/CreateRecording/types.ts +++ b/src/app/CreateRecording/types.ts @@ -13,16 +13,17 @@ * 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; +export type RecordingReplace = 'ALWAYS' | 'NEVER' | 'STOPPED'; + interface _FormBaseData { name: string; template?: EventTemplateIdentifier; - labels: RecordingLabel[]; + labels: KeyValue[]; continuous: boolean; archiveOnStop: boolean; restart: boolean; diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 901257860..ee43d593b 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -179,8 +179,8 @@ export const AutomatedAnalysisCard: DashboardCardFC ` query ActiveRecordingsForAutomatedAnalysis($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - recordings { - active (filter: { + target { + activeRecordings(filter: { name: "${automatedAnalysisRecordingName}", labels: ["origin=${automatedAnalysisRecordingName}"], }) { @@ -190,7 +190,10 @@ export const AutomatedAnalysisCard: DashboardCardFC downloadUrl reportUrl metadata { - labels + labels { + key + value + } } } } @@ -208,18 +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 + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + archivedTime + } + } } - size - archivedTime } - } }`, { connectUrl }, ); @@ -303,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) => { @@ -358,7 +368,7 @@ export const AutomatedAnalysisCard: DashboardCardFC } } }), - map((v) => v.data.targetNodes[0].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/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..a89f956d7 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,12 +78,12 @@ 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); }); 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())); @@ -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); } }); @@ -164,14 +164,17 @@ export const BulkEditLabels: React.FC = ({ observable = isUploadsTable ? context.api .graphql( - `query GetUploadedRecordings($filter: ArchivedRecordingFilterInput) { + `query GetUploadedRecordings($filter: ArchivedRecordingsFilterInput) { archivedRecordings(filter: $filter) { data { name downloadUrl reportUrl metadata { - labels + labels { + key + value + } } } } @@ -188,15 +191,20 @@ 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 + archivedTime } } } @@ -205,7 +213,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(), ); } @@ -246,14 +254,26 @@ export const BulkEditLabels: React.FC = ({ ]).subscribe((parts) => { const currentTarget = parts[0]; const event = parts[1]; - 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, - ), - ); + + const isMatch = + currentTarget?.connectUrl === event.message.target || + currentTarget?.jvmId === event.message.recording.jvmId || + currentTarget?.connectUrl === 'uploads'; + + 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/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..8809cb9be 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'; @@ -316,11 +315,15 @@ export const ActiveRecordingsTable: React.FC = (prop 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, - ), - ); + 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]); @@ -861,10 +864,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 +973,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..ca08d6e1a 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'; @@ -193,15 +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 + targetNodes(filter: { name: $connectUrl }) { + target { + archivedRecordings { + data { + name + downloadUrl + reportUrl + metadata { + labels { + key + value + } + } + size + } } - size } } }`, @@ -214,14 +220,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 } @@ -251,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, @@ -326,14 +335,33 @@ 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', + ); + const matchesUploadsUrlAndJvmId = + currentTarget?.connectUrl === 'uploads' && event.message.recording.jvmId === 'uploads'; + if (isUploadsTable && matchesUploadsUrlAndJvmId) { + refreshRecordingList(); + } + if ( + currentTarget?.jvmId != event.message.recording.jvmId && + currentTarget?.connectUrl != eventConnectUrlLabel?.value + ) { return; } setRecordings((old) => old.filter((r) => r.name !== event.message.recording.name)); setCheckedIndices((old) => old.filter((idx) => idx !== hashCode(event.message.recording.name))); }), ); - }, [addSubscription, context.notificationChannel, setRecordings, setCheckedIndices, propsTarget]); + }, [ + addSubscription, + context.notificationChannel, + setRecordings, + setCheckedIndices, + propsTarget, + isUploadsTable, + refreshRecordingList, + ]); React.useEffect(() => { addSubscription( @@ -341,14 +369,26 @@ 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) { + 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.map((o) => - o.name == event.message.recordingName ? { ...o, metadata: { labels: event.message.metadata.labels } } : o, - ), - ); + + 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]); @@ -772,10 +812,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 +885,7 @@ export const ArchivedRecordingRow: React.FC = ({ updateFilters: updateFilters, labelFilters: labelFilters, }} - labels={parsedLabels} + labels={recording.metadata.labels} /> @@ -874,7 +910,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/Recordings/RecordingFilters.tsx b/src/app/Recordings/RecordingFilters.tsx index 20c6a21bd..16571ea78 100644 --- a/src/app/Recordings/RecordingFilters.tsx +++ b/src/app/Recordings/RecordingFilters.tsx @@ -277,7 +277,6 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat if (!!filters.StartedBeforeDate && !!filters.StartedBeforeDate.length) { filtered = filtered.filter((rec) => { if (!filters.StartedBeforeDate) return true; - return filters.StartedBeforeDate.filter((startedBefore) => { const beforeDate = dayjs(startedBefore); return dayjs(rec.startTime).isBefore(beforeDate); @@ -294,9 +293,10 @@ export const filterRecordings = (recordings: any[], filters: RecordingFiltersCat }); } if (filters.Label.length) { - filtered = filtered.filter( - (r) => Object.entries(r.metadata.labels).filter(([k, v]) => filters.Label.includes(`${k}:${v}`)).length, - ); + filtered = filtered.filter((recording) => { + const recordingLabels = recording.metadata.labels.map((label) => `${label.key}:${label.value}`); + return filters.Label.some((filterLabel) => recordingLabels.includes(filterLabel)); + }); } return filtered; 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..886ad2725 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'; @@ -46,19 +45,23 @@ import { CredentialsResponse, RulesResponse, EnvironmentNode, - DiscoveryResponse, - ActiveRecordingFilterInput, + ActiveRecordingsFilterInput, RecordingCountResponse, MBeanMetrics, MBeanMetricsResponse, EventType, NotificationCategory, - NullableTarget, HttpError, SimpleResponse, XMLHttpError, XMLHttpRequestConfig, XMLHttpResponse, + KeyValue, + TargetStub, + TargetForTest, + Metadata, + TargetMetadata, + isTargetMetadata, } from './api.types'; import { isHttpError, includesTarget, isHttpOk, isXMLHttpError } from './api.utils'; import { LoginService } from './Login.service'; @@ -170,7 +173,7 @@ export class ApiService { } createTarget( - target: Target, + target: TargetStub, credentials?: { username?: string; password?: string }, storeCredentials = false, dryrun = false, @@ -206,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( @@ -295,7 +298,7 @@ export class ApiService { name, events, duration, - restart, + replace, archiveOnStop, metadata, advancedOptions, @@ -310,10 +313,10 @@ export class ApiService { form.append('archiveOnStop', String(archiveOnStop)); } if (metadata) { - form.append('metadata', JSON.stringify(metadata)); + 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) { @@ -480,7 +483,7 @@ export class ApiService { } uploadArchivedRecordingToGrafana( - sourceTarget: Observable, + sourceTarget: Observable, recordingName: string, ): Observable { return sourceTarget.pipe( @@ -522,7 +525,13 @@ export class ApiService { ); } - transformAndStringifyToRawLabels(labels: RecordingLabel[]) { + // 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; @@ -530,18 +539,59 @@ export class ApiService { return JSON.stringify(rawLabels); } - postRecordingMetadataFromPath(jvmId: string, recordingName: string, labels: RecordingLabel[]): Observable { - return this.sendRequest( - 'beta', - `fs/recordings/${encodeURIComponent(jvmId)}/${encodeURIComponent(recordingName)}/metadata/labels`, + 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; + } + + postRecordingMetadataForJvmId( + jvmId: string, + recordingName: string, + labels: KeyValue[], + ): Observable { + return this.graphql( + ` + query postRecordingMetadataForJvmId($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 { @@ -725,7 +775,7 @@ export class ApiService { } getActiveProbesForTarget( - target: Target, + target: TargetStub, suppressNotifications = false, skipStatusCheck = false, ): Observable { @@ -888,68 +938,91 @@ 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(), 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: RecordingLabel[]): Observable { + 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[])); } - postTargetRecordingMetadata(recordingName: string, labels: RecordingLabel[]): Observable { + postTargetRecordingMetadata(recordingName: string, labels: KeyValue[]): Observable { return this.target.target().pipe( filter((target) => !!target), first(), 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 + } } } } @@ -957,10 +1030,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[]), ); } @@ -1034,11 +1111,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(), ); } @@ -1047,7 +1123,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'); @@ -1078,20 +1154,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 } + } } } } @@ -1105,22 +1181,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 } @@ -1140,7 +1216,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)), @@ -1148,7 +1224,7 @@ export class ApiService { } checkCredentialForTarget( - target: Target, + target: TargetStub, credentials: { username: string; password: string }, ): Observable< | { @@ -1201,13 +1277,15 @@ export class ApiService { ); } - getTargetMBeanMetrics(target: Target, queries: string[]): Observable { + getTargetMBeanMetrics(target: TargetStub, queries: string[]): Observable { return this.graphql( ` query MBeanMXMetricsForTarget($connectUrl: String) { targetNodes(filter: { name: $connectUrl }) { - mbeanMetrics { - ${queries.join('\n')} + target { + mbeanMetrics { + ${queries.join('\n')} + } } } }`, @@ -1218,36 +1296,43 @@ export class ApiService { if (!nodes || nodes.length === 0) { return {}; } - return nodes[0]?.mbeanMetrics; + return nodes[0]?.target.mbeanMetrics; }), catchError((_) => of({})), ); } - 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', @@ -1257,7 +1342,7 @@ export class ApiService { ); } - getTargetEventTemplates(target: Target): Observable { + getTargetEventTemplates(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/templates`, 'v1', @@ -1267,7 +1352,7 @@ export class ApiService { ); } - getTargetEventTypes(target: Target): Observable { + getTargetEventTypes(target: TargetStub): Observable { return this.doGet( `targets/${encodeURIComponent(target.connectUrl)}/events`, 'v1', @@ -1306,8 +1391,22 @@ export class ApiService { anchor.remove(); } - stringifyRecordingLabels(labels: RecordingLabel[]): 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 86cd9f6d7..fc847143c 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -14,21 +14,33 @@ * 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 // ====================================== export interface KeyValue { - readonly [key: string]: string; + key: string; + value: string; } export interface Metadata { - labels: KeyValue; - annotations?: KeyValue; + labels: 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 { @@ -87,6 +99,13 @@ export class XMLHttpError extends Error { } } +export type TargetStub = Omit; + +export type TargetForTest = Pick & { + labels: object; + annotations: { cryostat: object; platform: object }; +}; + // ====================================== // Health Resources // ====================================== @@ -122,7 +141,7 @@ export interface AuthV2Response extends ApiV2Response { // ====================================== // MBean metric resources // ====================================== -export interface MemoryUsage { +export interface MemoryUtilization { init: number; used: number; committed: number; @@ -147,8 +166,8 @@ export interface MBeanMetrics { totalSwapSpaceSize?: number; }; memory?: { - heapMemoryUsage?: MemoryUsage; - nonHeapMemoryUsage?: MemoryUsage; + heapMemoryUsage?: MemoryUtilization; + nonHeapMemoryUsage?: MemoryUtilization; heapMemoryUsagePercent?: number; }; runtime?: { @@ -161,7 +180,7 @@ export interface MBeanMetrics { specName?: string; specVendor?: string; startTime?: number; - systemProperties?: object; + systemProperties?: KeyValue[]; uptime?: number; vmName?: string; vmVendor?: string; @@ -173,7 +192,9 @@ export interface MBeanMetrics { export interface MBeanMetricsResponse { data: { targetNodes: { - mbeanMetrics: MBeanMetrics; + target: { + mbeanMetrics: MBeanMetrics; + }; }[]; }; } @@ -205,7 +226,7 @@ export interface RecordingAttributes { events: string; duration?: number; archiveOnStop?: boolean; - restart?: boolean; + replace?: RecordingReplace; advancedOptions?: AdvancedRecordingOptions; metadata?: Metadata; } @@ -218,6 +239,7 @@ export interface Recording { } export interface ArchivedRecording extends Recording { + jvmId?: string; archivedTime: number; size: number; } @@ -233,7 +255,7 @@ export interface ActiveRecording extends Recording { maxAge: number; } -export interface ActiveRecordingFilterInput { +export interface ActiveRecordingsFilterInput { name?: string; state?: string; continuous?: boolean; @@ -260,8 +282,8 @@ export interface RecordingResponse extends ApiV2Response { export interface RecordingCountResponse { data: { targetNodes: { - recordings: { - active: { + target: { + activeRecordings: { aggregate: { count: number; }; @@ -441,13 +463,14 @@ 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; - labels?: KeyValue; - annotations?: { - cryostat: KeyValue; - platform: KeyValue; + labels: KeyValue[]; + annotations: { + cryostat: KeyValue[]; + platform: KeyValue[]; }; } @@ -483,7 +506,7 @@ interface _AbstractNode { readonly id: number; readonly name: string; readonly nodeType: NodeType; - readonly labels: KeyValue; + readonly labels: KeyValue[]; } export interface EnvironmentNode extends _AbstractNode { @@ -494,12 +517,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 5eb3caf73..22cc28b58 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[] => { @@ -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/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/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/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..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, @@ -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, @@ -178,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 { @@ -271,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); } @@ -317,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, @@ -330,8 +360,8 @@ class FakeApiService extends ApiService { data: { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [fakeAARecording], }, }, 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..31a40217a 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`; @@ -101,10 +102,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( @@ -123,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); @@ -211,11 +207,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( @@ -496,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, }, }, @@ -516,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, }, }, @@ -535,27 +549,27 @@ 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, + labels: labelsArray, }, }); data = { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [ { doPutMetadata: { metadata: { - labels, + labels: labelsArray, }, }, + size: 1024 * 1024 * 50, + archivedTime: +Date.now(), }, ], }, @@ -573,7 +587,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { recordingName: variables.recordingName, target: variables.connectUrl, metadata: { - labels, + labels: labelsArray, }, }, }), @@ -581,25 +595,23 @@ 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, + labels: labelsArray, }, }); data = { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [ { doPutMetadata: { metadata: { - labels, + labels: labelsArray, }, }, }, @@ -619,7 +631,7 @@ export const startMirage = ({ environment = 'development' } = {}) => { recordingName: variables.recordingName, target: variables.connectUrl, metadata: { - labels, + labels: labelsArray, }, }, }), @@ -630,52 +642,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..f14cf25c6 100644 --- a/src/test/Archives/AllArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllArchivedRecordingsTable.test.tsx @@ -18,7 +18,7 @@ import { AllArchivedRecordingsTable } from '@app/Archives/AllArchivedRecordingsT import { NotificationMessage, ArchivedRecording, RecordingDirectory } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; -import { cleanup, screen, within } from '@testing-library/react'; +import { cleanup, screen, within, waitFor } from '@testing-library/react'; import { of } from 'rxjs'; import { render, renderSnapshot } from '../utils'; @@ -28,30 +28,44 @@ const mockConnectUrl2 = 'service:jmx:rmi://someUrl2'; const mockJvmId2 = 'fooJvmId2'; const mockConnectUrl3 = 'service:jmx:rmi://someUrl3'; const mockJvmId3 = 'fooJvmId3'; +const mockName3 = 'someRecording3'; const mockCount1 = 1; const mockRecordingSavedNotification = { message: { - target: mockConnectUrl3, + recording: { + name: mockName3, + metadata: { + labels: { + key: 'someLabel', + value: 'someValue', + }, + }, + }, }, } as NotificationMessage; const mockRecordingDeletedNotification = { message: { - target: mockConnectUrl1, + recording: { + name: mockName3, + }, }, } as NotificationMessage; -const mockRecordingLabels = { - someLabel: 'someValue', -}; - const mockRecording: ArchivedRecording = { name: 'someRecording', downloadUrl: 'http://downloadUrl', reportUrl: 'http://reportUrl', - metadata: { labels: mockRecordingLabels }, + metadata: { + labels: [ + { + key: 'someLabel', + value: 'someValue', + }, + ], + }, size: 2048, archivedTime: 2048, }; @@ -119,39 +133,32 @@ jest jest .spyOn(defaultServices.notificationChannel, 'messages') .mockReturnValueOnce(of()) // renders correctly // NotificationCategory.RecordingMetadataUpdated - .mockReturnValueOnce(of()) // NotificationCategory.ActiveRecordingSaved .mockReturnValueOnce(of()) // NotificationCategory.ArchivedRecordingCreated .mockReturnValueOnce(of()) // NotificationCategory.ArchivedRecordingDeleted .mockReturnValueOnce(of()) // shows no recordings when empty .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // has the correct table elements .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // correctly handles the search function .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // expands targets to show their .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockRecordingSavedNotification)) // increments the count when an archived recording is saved - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // increments the count when an archived recording is saved + .mockReturnValueOnce(of(mockRecordingSavedNotification)) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockRecordingDeletedNotification)) // decrements the count when an archived recording is deleted + .mockReturnValueOnce(of()) // decrements the count when an archived recording is deleted .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()); + .mockReturnValueOnce(of(mockRecordingDeletedNotification)); describe('', () => { afterEach(cleanup); @@ -246,7 +253,9 @@ describe('', () => { const thirdTarget = rows[2]; expect(within(thirdTarget).getByText(`${mockConnectUrl3}`)).toBeTruthy(); - expect(within(thirdTarget).getByText(4)).toBeTruthy(); + await waitFor(() => { + expect(within(thirdTarget).getByText('4')).toBeInTheDocument(); + }); }); it('decrements the count when an archived recording is deleted', async () => { diff --git a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx index 1c12b36da..2ea05ac6f 100644 --- a/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx +++ b/src/test/Archives/AllTargetsArchivedRecordingsTable.test.tsx @@ -24,18 +24,50 @@ import { render, renderSnapshot } from '../utils'; const mockConnectUrl1 = 'service:jmx:rmi://someUrl1'; const mockAlias1 = 'fooTarget1'; const mockTarget1: Target = { + jvmId: 'target1', connectUrl: mockConnectUrl1, alias: mockAlias1, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; const mockConnectUrl2 = 'service:jmx:rmi://someUrl2'; const mockAlias2 = 'fooTarget2'; +const mockTarget2: Target = { + jvmId: 'target2', + connectUrl: mockConnectUrl2, + alias: mockAlias2, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, +}; const mockConnectUrl3 = 'service:jmx:rmi://someUrl3'; const mockAlias3 = 'fooTarget3'; +const mockTarget3: Target = { + jvmId: 'target3', + connectUrl: mockConnectUrl3, + alias: mockAlias3, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, +}; const mockNewConnectUrl = 'service:jmx:rmi://someNewUrl'; const mockNewAlias = 'newTarget'; const mockNewTarget: Target = { + jvmId: 'target4', connectUrl: mockNewConnectUrl, alias: mockNewAlias, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, }; const mockCount1 = 1; const mockCount2 = 3; @@ -48,21 +80,31 @@ const mockTargetFoundNotification = { }, } as NotificationMessage; -const mockTargetLostNotification = { - message: { - event: { kind: 'LOST', serviceRef: mockTarget1 }, +const mockRecording = { + jvmId: mockTarget1.jvmId, + name: 'SampleRecording', + downloadUrl: 'http://downloadurl.com/sample', + reportUrl: 'http://reporturl.com/sample', + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: 'service:jmx:rmi://someNewUrl' }, + ], }, -} as NotificationMessage; + size: 1234, + archivedTime: 987654321, +}; -const mockRecordingSavedNotification = { +const mockTargetLostNotification = { message: { - target: mockConnectUrl3, + event: { kind: 'LOST', serviceRef: mockTarget1 }, }, } as NotificationMessage; -const mockRecordingDeletedNotification = { +const mockRecordingNotification = { message: { - target: mockConnectUrl1, + target: mockTarget1, + recording: mockRecording, }, } as NotificationMessage; @@ -70,54 +112,77 @@ const mockTargetsAndCountsResponse = { data: { targetNodes: [ { - recordings: { - archived: { + target: { + ...mockTarget1, + archivedRecordings: { + jvmId: mockTarget1.jvmId, + name: 'fooRecording1', + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: 'service:jmx:rmi://someUrl1' }, + ], + }, aggregate: { count: mockCount1, }, }, }, - target: { - alias: mockAlias1, - serviceUri: mockConnectUrl1, - }, }, { - recordings: { - archived: { + target: { + ...mockTarget2, + archivedRecordings: { + jvmId: mockTarget2.jvmId, + name: 'fooRecording2', + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: 'service:jmx:rmi://someUrl2' }, + ], + }, aggregate: { count: mockCount2, }, }, }, - target: { - alias: mockAlias2, - serviceUri: mockConnectUrl2, - }, }, { - recordings: { - archived: { + target: { + ...mockTarget3, + archivedRecordings: { + jvmId: mockTarget3.jvmId, + name: 'fooRecording3', + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: 'service:jmx:rmi://someUrl3' }, + ], + }, aggregate: { count: mockCount3, }, }, }, - target: { - alias: mockAlias3, - serviceUri: mockConnectUrl3, - }, }, ], }, }; - const mockNewTargetCountResponse = { data: { targetNodes: [ { - recordings: { - archived: { + target: { + ...mockNewTarget, + archivedRecordings: { + jvmId: mockNewTarget.jvmId, + name: 'fooRecording1', + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: mockNewTarget.connectUrl }, + ], + }, aggregate: { count: mockNewCount, }, @@ -147,8 +212,9 @@ jest .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // hides targets with zero recordings .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // correctly handles the search function .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // expands targets to show their - .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // adds a target upon receiving a notification - .mockReturnValueOnce(of(mockNewTargetCountResponse)) + .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // adds a target upon receiving a notification (on load) + .mockReturnValueOnce(of(mockNewTargetCountResponse)) // adds a target upon receiving a notification (on notification) + .mockReturnValueOnce(of(mockTargetsAndCountsResponse)) // removes a target upon receiving a notification) .mockReturnValue(of(mockTargetsAndCountsResponse)); // remaining tests jest @@ -156,47 +222,38 @@ jest .mockReturnValueOnce(of()) // renders correctly .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // has the correct table elements .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // hides targets with zero recordings .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // correctly handles the search function .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // expands targets to show their .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockTargetFoundNotification)) // adds a target upon receiving a notification .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockTargetLostNotification)) // removes a target upon receiving a notification .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // increments the count when an archived recording is saved - .mockReturnValueOnce(of(mockRecordingSavedNotification)) - .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockRecordingNotification)) .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) // decrements the count when an archived recording is deleted .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockRecordingDeletedNotification)); + .mockReturnValueOnce(of(mockRecordingNotification)); describe('', () => { afterEach(cleanup); @@ -214,12 +271,12 @@ describe('', () => { expect(screen.getByLabelText('all-targets-table')).toBeInTheDocument(); expect(screen.getByText('Target')).toBeInTheDocument(); expect(screen.getByText('Archives')).toBeInTheDocument(); - expect(screen.getByText(`${mockAlias1} (${mockConnectUrl1})`)).toBeInTheDocument(); + expect(screen.getByText(`${mockTarget1.alias} (${mockTarget1.connectUrl})`)).toBeInTheDocument(); expect(screen.getByText(`${mockCount1}`)).toBeInTheDocument(); - expect(screen.getByText(`${mockAlias2} (${mockConnectUrl2})`)).toBeInTheDocument(); + expect(screen.getByText(`${mockTarget2.alias} (${mockTarget2.connectUrl})`)).toBeInTheDocument(); expect(screen.getByText(`${mockCount2}`)).toBeInTheDocument(); // Default to hide target with 0 archives - expect(screen.queryByText(`${mockAlias3} (${mockConnectUrl3})`)).not.toBeInTheDocument(); + expect(screen.queryByText(`${mockTarget3.alias} (${mockTarget3.connectUrl})`)).not.toBeInTheDocument(); expect(screen.queryByText(`${mockCount3}`)).not.toBeInTheDocument(); }); @@ -234,10 +291,10 @@ describe('', () => { let rows = within(tableBody).getAllByRole('row'); expect(rows).toHaveLength(2); const firstTarget = rows[0]; - expect(within(firstTarget).getByText(`${mockAlias1} (${mockConnectUrl1})`)).toBeTruthy(); + expect(within(firstTarget).getByText(`${mockTarget1.alias} (${mockTarget1.connectUrl})`)).toBeTruthy(); expect(within(firstTarget).getByText(`${mockCount1}`)).toBeTruthy(); const secondTarget = rows[1]; - expect(within(secondTarget).getByText(`${mockAlias2} (${mockConnectUrl2})`)).toBeTruthy(); + expect(within(secondTarget).getByText(`${mockTarget2.alias} (${mockTarget2.connectUrl})`)).toBeTruthy(); expect(within(secondTarget).getByText(`${mockCount2}`)).toBeTruthy(); const checkbox = screen.getByLabelText('all-targets-hide-check'); @@ -247,7 +304,7 @@ describe('', () => { rows = within(tableBody).getAllByRole('row'); expect(rows).toHaveLength(3); const thirdTarget = rows[2]; - expect(within(thirdTarget).getByText(`${mockAlias3} (${mockConnectUrl3})`)).toBeTruthy(); + expect(within(thirdTarget).getByText(`${mockTarget3.alias} (${mockTarget3.connectUrl})`)).toBeTruthy(); expect(within(thirdTarget).getByText(`${mockCount3}`)).toBeTruthy(); }); @@ -266,7 +323,7 @@ describe('', () => { rows = within(tableBody).getAllByRole('row'); expect(rows).toHaveLength(1); const firstTarget = rows[0]; - expect(within(firstTarget).getByText(`${mockAlias1} (${mockConnectUrl1})`)).toBeTruthy(); + expect(within(firstTarget).getByText(`${mockTarget1.alias} (${mockTarget1.connectUrl})`)).toBeTruthy(); expect(within(firstTarget).getByText(`${mockCount1}`)).toBeTruthy(); await user.type(search, 'asdasdjhj'); @@ -311,14 +368,14 @@ describe('', () => { it('adds a target upon receiving a notification', async () => { render({ routerConfigs: { routes: [{ path: '/archives', element: }] } }); - expect(screen.getByText(`${mockNewAlias} (${mockNewConnectUrl})`)).toBeInTheDocument(); + expect(screen.getByText(`${mockNewTarget.alias} (${mockNewTarget.connectUrl})`)).toBeInTheDocument(); expect(screen.getByText(`${mockNewCount}`)).toBeInTheDocument(); }); it('removes a target upon receiving a notification', async () => { render({ routerConfigs: { routes: [{ path: '/archives', element: }] } }); - expect(screen.queryByText(`${mockAlias1} (${mockConnectUrl1})`)).not.toBeInTheDocument(); + expect(screen.queryByText(`${mockTarget1.alias} (${mockTarget1.connectUrl})`)).not.toBeInTheDocument(); expect(screen.queryByText(`${mockCount1}`)).not.toBeInTheDocument(); }); @@ -327,10 +384,10 @@ describe('', () => { const tableBody = screen.getAllByRole('rowgroup')[1]; const rows = within(tableBody).getAllByRole('row'); - expect(rows).toHaveLength(3); + expect(rows).toHaveLength(2); - const thirdTarget = rows[2]; - expect(within(thirdTarget).getByText(`${mockCount3 + 1}`)).toBeTruthy(); + const firstTarget = rows[0]; + expect(within(firstTarget).getByText(`${mockCount1 + 1}`)).toBeTruthy(); }); it('decrements the count when an archived recording is deleted', async () => { @@ -345,7 +402,7 @@ describe('', () => { const rows = within(tableBody).getAllByRole('row'); const firstTarget = rows[0]; - expect(within(firstTarget).getByText(`${mockAlias1} (${mockConnectUrl1})`)).toBeTruthy(); + expect(within(firstTarget).getByText(`${mockTarget1.alias} (${mockTarget1.connectUrl})`)).toBeTruthy(); expect(within(firstTarget).getByText(`${mockCount1 - 1}`)).toBeTruthy(); }); }); diff --git a/src/test/CreateRecording/CustomRecordingForm.test.tsx b/src/test/CreateRecording/CustomRecordingForm.test.tsx index bb28193ba..9392a3094 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', @@ -112,13 +118,13 @@ describe('', () => { events: 'template=someEventTemplate,type=CUSTOM', duration: 30, archiveOnStop: true, - restart: false, + replace: 'NEVER', advancedOptions: { maxAge: undefined, 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..6b27417d6 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 }; @@ -147,16 +153,14 @@ const mockCachedReport: CachedReportValue = { }; const mockTargetNode = { - recordings: { - active: { - data: [mockRecording], - }, + activeRecordings: { + data: [mockRecording], }, }; const mockActiveRecordingsResponse = { data: { - targetNodes: [mockTargetNode], + targetNodes: [{ target: mockTargetNode }], }, }; @@ -164,8 +168,8 @@ const mockEmptyActiveRecordingsResponse = { data: { targetNodes: [ { - recordings: { - active: { + target: { + activeRecordings: { data: [], }, }, @@ -176,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/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..d1da80c57 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', @@ -66,9 +76,16 @@ const mockActiveRecording: ActiveRecording = { const mockActiveLabelsNotification = { message: { target: mockConnectUrl, - recordingName: 'someActiveRecording', - jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + recording: { + name: 'someActiveRecording', + jvmId: mockJvmId, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, + }, }, } as NotificationMessage; @@ -77,9 +94,16 @@ const mockActiveRecordingResponse = [mockActiveRecording]; const mockArchivedLabelsNotification = { message: { target: mockConnectUrl, - recordingName: 'someArchivedRecording_some_random', - jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someValue', someNewLabel: 'someNewValue' } }, + recording: { + name: 'someArchivedRecording_some_random', + jvmId: mockJvmId, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'someNewLabel', value: 'someNewValue' }, + ], + }, + }, }, } as NotificationMessage; @@ -87,8 +111,8 @@ const mockArchivedRecordingsResponse = { data: { targetNodes: [ { - recordings: { - archived: { + target: { + archivedRecordings: { data: [mockArchivedRecording] as ArchivedRecording[], }, }, 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..fb6a9bbf3 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', @@ -58,9 +67,11 @@ const mockCreateNotification = { const mockLabelsNotification = { message: { target: mockConnectUrl, - recordingName: 'someRecording', + recording: { + name: 'someRecording', + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, + }, jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, }, } as NotificationMessage; const mockStopNotification = { @@ -233,8 +244,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(); }); @@ -527,7 +538,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..056a441c2 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 } 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'; @@ -32,35 +38,85 @@ 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', + }, +]; +export const convertLabels = (kv: KeyValue[]): object => { + const out = {}; + for (const 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', + jvmId: mockJvmId, downloadUrl: 'http://downloadUrl', reportUrl: 'http://reportUrl', - metadata: { labels: mockRecordingLabels }, + metadata: { + labels: [ + { key: 'someLabel', value: 'someValue' }, + { key: 'connectUrl', value: 'service:jmx:rmi://someUrl' }, + ], + }, size: 2048, archivedTime: 2048, }; const mockArchivedRecordingsResponse = { + data: { + targetNodes: [ + { + target: { + archivedRecordings: { + data: [mockRecording], + }, + }, + }, + ], + }, +}; + +const mockAllArchivedRecordingsResponse = { data: { archivedRecordings: { - data: [mockRecording] as ArchivedRecording[], + data: [mockRecording], + aggregate: { + count: 1, + size: mockRecording.size, + }, }, }, }; @@ -72,9 +128,11 @@ const mockCreateNotification = { const mockLabelsNotification = { message: { target: mockConnectUrl, - recordingName: 'someRecording', - jvmId: mockJvmId, - metadata: { labels: { someLabel: 'someUpdatedValue' } }, + recording: { + name: 'someRecording', + jvmId: mockJvmId, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, + }, }, } as NotificationMessage; const mockDeleteNotification = { @@ -103,7 +161,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 @@ -220,11 +284,17 @@ 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(); - }); + }); */ + + for (const entry of mockRecordingLabels) { + const label = await screen.findByText(`${entry.key}: ${entry.value}`); + expect(label).toBeInTheDocument(); + expect(label).toBeVisible(); + } const actionIcon = within(screen.getByLabelText(`${mockRecording.name}-actions`)).getByLabelText('Actions'); expect(actionIcon).toBeInTheDocument(); @@ -725,7 +795,7 @@ describe('', () => { expect(uploadSpy).toHaveBeenCalled(); expect(uploadSpy).toHaveBeenCalledWith( mockFileUpload, - mockUploadedRecordingLabels, + convertLabels(mockUploadedRecordingLabels), expect.any(Function), expect.any(Subject), ); 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' };