From d9ac002940452c9a8332149de02e830ae4d71511 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 24 Sep 2024 08:03:28 -0400 Subject: [PATCH 01/19] Initial implementation --- locales/en/public.json | 4 + .../diagnostics/DiagnosticsChartCard.tsx | 196 ++++++++++++++++++ src/app/Dashboard/utils.tsx | 2 + src/app/Shared/Services/Api.service.tsx | 14 ++ 4 files changed, 216 insertions(+) create mode 100644 src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx diff --git a/locales/en/public.json b/locales/en/public.json index 20b7d1170..1795058ba 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -157,6 +157,10 @@ "MBEAN_METRICS_CARD_DESCRIPTION": "Display common performance metrics from current MBean data.", "MBEAN_METRICS_CARD_DESCRIPTION_FULL": "Display a single performance metric from a list of supported MBeans.", "MBEAN_METRICS_CARD_TITLE": "MBean Metrics Chart", + "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", + "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", + "DIAGNOSTICS_CARD_TITLE": "Diagnostics", + "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection", "NO_RECORDING": { "DESCRIPTION": "Metrics cards display data taken from running Flight Recordings with the label . No such Recordings are currently available.", "TITLE": "No source Recording" diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx b/src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx new file mode 100644 index 000000000..358abe8af --- /dev/null +++ b/src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx @@ -0,0 +1,196 @@ +/* + * 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. + */ + +import { + DashboardCardTypeProps, + DashboardCardFC, + DashboardCardSizes, + DashboardCardDescriptor, + } from '@app/Dashboard/types'; +import { ErrorView } from '@app/ErrorView/ErrorView'; + import { FeatureLevel } from '@app/Shared/Services/service.types'; + import { ServiceContext } from '@app/Shared/Services/Services'; + import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; + import { + Bullseye, + Button, + CardBody, + CardHeader, + CardTitle, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Label, + EmptyStateHeader, + EmptyStateFooter, + } from '@patternfly/react-core'; + import { DataSourceIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; + import * as React from 'react'; + import { Trans, useTranslation } from 'react-i18next'; + import { DashboardCard } from '../../DashboardCard'; + +export interface DiagnosticsCardProps extends DashboardCardTypeProps { + chartKind: string; + duration: number; + period: number; + } + + // TODO are these needed? + export enum DiagnosticsCardKind { + } + + export function kindToId(kind: string): number { + return DiagnosticsCardKind[kind]; + } + + export const DiagnosticsCard: DashboardCardFC = (props) => { + const { t } = useTranslation(); + const serviceContext = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); + + const handleError = React.useCallback( + (error) => { + return ( + + ); + }, + [], + ); + + const handleGC = React.useCallback(() => { + addSubscription( + serviceContext.api.getActiveProbes(true).subscribe({ + error: (err) => handleError(err), + }), + ); + }, [addSubscription, serviceContext.api, handleError]); + + const GCButton = React.useMemo(() => { + return ( + + + + + + + ); + }; + + DiagnosticsCard.cardComponentName = 'DiagnosticsCard'; + + export const DiagnosticsCardSizes: DashboardCardSizes = { + span: { + minimum: 3, + default: 4, + maximum: 12, + }, + height: { + // TODO: implement height resizing + minimum: Number.NaN, + default: Number.NaN, + maximum: Number.NaN, + }, + }; + + export const DiagnosticsCardDescriptor: DashboardCardDescriptor = { + featureLevel: FeatureLevel.BETA, + title: 'CHART_CARD.DIAGNOSTICS_CARD_TITLE', + cardSizes: DiagnosticsCardSizes, + description: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION', + descriptionFull: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION_FULL', + component: DiagnosticsCard, + propControls: [], + icon: , + labels: [ + { + content: 'Beta', + color: 'cyan', + }, + { + content: 'Diagnostics', + color: 'blue', + }, + ], + }; + \ No newline at end of file diff --git a/src/app/Dashboard/utils.tsx b/src/app/Dashboard/utils.tsx index 17bb102a6..05f0d753e 100644 --- a/src/app/Dashboard/utils.tsx +++ b/src/app/Dashboard/utils.tsx @@ -27,6 +27,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAnalysisCard'; +import { DiagnosticsCardDescriptor } from './Charts/diagnostics/DiagnosticsChartCard'; import { JFRMetricsChartCardDescriptor } from './Charts/jfr/JFRMetricsChartCard'; import { MBeanMetricsChartCardDescriptor } from './Charts/mbean/MBeanMetricsChartCard'; import { JvmDetailsCardDescriptor } from './JvmDetails/JvmDetailsCard'; @@ -165,6 +166,7 @@ export const getDashboardCards: (featureLevel?: FeatureLevel) => DashboardCardDe AutomatedAnalysisCardDescriptor, JFRMetricsChartCardDescriptor, MBeanMetricsChartCardDescriptor, + DiagnosticsCardDescriptor, ]; return cards.filter((card) => card.featureLevel >= featureLevel); }; diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 8a43d8ba1..1e0a96710 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -671,6 +671,20 @@ export class ApiService { ); } + runGC(): Observable { + return this.target.target().pipe( + concatMap((target) => + this.sendRequest('beta', `diagnostics/targets/${encodeURIComponent(target?.connectUrl || '')}`, { + method: 'POST', + }).pipe( + map((resp) => resp.ok), + catchError(() => of(false)), + first(), + ), + ), + ); + } + insertProbes(templateName: string): Observable { return this.target.target().pipe( concatMap((target) => From db065a481b226553cd654f9f699dddf385aaaecc Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 24 Sep 2024 14:30:08 -0400 Subject: [PATCH 02/19] Fix incorrect API call --- .../{DiagnosticsChartCard.tsx => DiagnosticsCard.tsx} | 2 +- src/app/Dashboard/utils.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/Dashboard/Charts/diagnostics/{DiagnosticsChartCard.tsx => DiagnosticsCard.tsx} (98%) diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx similarity index 98% rename from src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx rename to src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx index 358abe8af..aaa2387aa 100644 --- a/src/app/Dashboard/Charts/diagnostics/DiagnosticsChartCard.tsx +++ b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx @@ -76,7 +76,7 @@ export interface DiagnosticsCardProps extends DashboardCardTypeProps { const handleGC = React.useCallback(() => { addSubscription( - serviceContext.api.getActiveProbes(true).subscribe({ + serviceContext.api.runGC().subscribe({ error: (err) => handleError(err), }), ); diff --git a/src/app/Dashboard/utils.tsx b/src/app/Dashboard/utils.tsx index 05f0d753e..8d330ddb0 100644 --- a/src/app/Dashboard/utils.tsx +++ b/src/app/Dashboard/utils.tsx @@ -27,7 +27,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAnalysisCard'; -import { DiagnosticsCardDescriptor } from './Charts/diagnostics/DiagnosticsChartCard'; +import { DiagnosticsCardDescriptor } from './Charts/diagnostics/DiagnosticsCard'; import { JFRMetricsChartCardDescriptor } from './Charts/jfr/JFRMetricsChartCard'; import { MBeanMetricsChartCardDescriptor } from './Charts/mbean/MBeanMetricsChartCard'; import { JvmDetailsCardDescriptor } from './JvmDetails/JvmDetailsCard'; From 36576c111d625aa77e00ddc24390780690648fe2 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 24 Sep 2024 14:34:24 -0400 Subject: [PATCH 03/19] Fix API path --- src/app/Shared/Services/Api.service.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 1e0a96710..d04bd10f6 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -674,7 +674,7 @@ export class ApiService { runGC(): Observable { return this.target.target().pipe( concatMap((target) => - this.sendRequest('beta', `diagnostics/targets/${encodeURIComponent(target?.connectUrl || '')}`, { + this.sendRequest('beta', `diagnostics/targets/${encodeURIComponent(target?.connectUrl || '')}/gc`, { method: 'POST', }).pipe( map((resp) => resp.ok), From e8d97130be82257b842c67723c4ad0e59cbf3b13 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 24 Sep 2024 14:54:46 -0400 Subject: [PATCH 04/19] Fix API request --- src/app/Shared/Services/Api.service.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index d04bd10f6..cd6244500 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -674,7 +674,7 @@ export class ApiService { runGC(): Observable { return this.target.target().pipe( concatMap((target) => - this.sendRequest('beta', `diagnostics/targets/${encodeURIComponent(target?.connectUrl || '')}/gc`, { + this.sendRequest('beta', `diagnostics/targets/${target?.id}/gc`, { method: 'POST', }).pipe( map((resp) => resp.ok), From 5c3a96a5f08c57c5a2582692886b5495fb9d5611 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 24 Sep 2024 14:56:38 -0400 Subject: [PATCH 05/19] Run Prettier --- .../Charts/diagnostics/DiagnosticsCard.tsx | 330 +++++++++--------- src/app/Shared/Services/Api.service.tsx | 6 +- 2 files changed, 163 insertions(+), 173 deletions(-) diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx index aaa2387aa..05b6b747a 100644 --- a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx @@ -15,182 +15,172 @@ */ import { - DashboardCardTypeProps, - DashboardCardFC, - DashboardCardSizes, - DashboardCardDescriptor, - } from '@app/Dashboard/types'; + DashboardCardTypeProps, + DashboardCardFC, + DashboardCardSizes, + DashboardCardDescriptor, +} from '@app/Dashboard/types'; import { ErrorView } from '@app/ErrorView/ErrorView'; - import { FeatureLevel } from '@app/Shared/Services/service.types'; - import { ServiceContext } from '@app/Shared/Services/Services'; - import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; - import { - Bullseye, - Button, - CardBody, - CardHeader, - CardTitle, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateVariant, - Label, - EmptyStateHeader, - EmptyStateFooter, - } from '@patternfly/react-core'; - import { DataSourceIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; - import * as React from 'react'; - import { Trans, useTranslation } from 'react-i18next'; - import { DashboardCard } from '../../DashboardCard'; - +import { FeatureLevel } from '@app/Shared/Services/service.types'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; +import { + Bullseye, + Button, + CardBody, + CardHeader, + CardTitle, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Label, + EmptyStateHeader, + EmptyStateFooter, +} from '@patternfly/react-core'; +import { DataSourceIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; +import * as React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { DashboardCard } from '../../DashboardCard'; + export interface DiagnosticsCardProps extends DashboardCardTypeProps { - chartKind: string; - duration: number; - period: number; - } - - // TODO are these needed? - export enum DiagnosticsCardKind { - } - - export function kindToId(kind: string): number { - return DiagnosticsCardKind[kind]; - } - - export const DiagnosticsCard: DashboardCardFC = (props) => { - const { t } = useTranslation(); - const serviceContext = React.useContext(ServiceContext); - const addSubscription = useSubscriptions(); - - const handleError = React.useCallback( - (error) => { - return ( - - ); - }, - [], - ); - - const handleGC = React.useCallback(() => { - addSubscription( - serviceContext.api.runGC().subscribe({ - error: (err) => handleError(err), - }), + chartKind: string; + duration: number; + period: number; +} + +// TODO are these needed? +export enum DiagnosticsCardKind {} + +export function kindToId(kind: string): number { + return DiagnosticsCardKind[kind]; +} + +export const DiagnosticsCard: DashboardCardFC = (props) => { + const { t } = useTranslation(); + const serviceContext = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); + + const handleError = React.useCallback((error) => { + return ; + }, []); + + const handleGC = React.useCallback(() => { + addSubscription( + serviceContext.api.runGC().subscribe({ + error: (err) => handleError(err), + }), ); - }, [addSubscription, serviceContext.api, handleError]); + }, [addSubscription, serviceContext.api, handleError]); - const GCButton = React.useMemo(() => { - return ( - - - - - - + + {t('CHART_CARD.TITLE', { chartKind: props.chartKind, duration: props.duration, period: props.period })} + + ); - }; - - DiagnosticsCard.cardComponentName = 'DiagnosticsCard'; - - export const DiagnosticsCardSizes: DashboardCardSizes = { - span: { - minimum: 3, - default: 4, - maximum: 12, + }, [props.chartKind, props.duration, props.period, t, actions]); + + return ( + + + + + {t('CHART_CARD.DIAGNOSTICS_CARD_TITLE')}} + icon={} + headingLevel="h2" + /> + + }} + > + CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION + + + + + + + + + + ); +}; + +DiagnosticsCard.cardComponentName = 'DiagnosticsCard'; + +export const DiagnosticsCardSizes: DashboardCardSizes = { + span: { + minimum: 3, + default: 4, + maximum: 12, + }, + height: { + // TODO: implement height resizing + minimum: Number.NaN, + default: Number.NaN, + maximum: Number.NaN, + }, +}; + +export const DiagnosticsCardDescriptor: DashboardCardDescriptor = { + featureLevel: FeatureLevel.BETA, + title: 'CHART_CARD.DIAGNOSTICS_CARD_TITLE', + cardSizes: DiagnosticsCardSizes, + description: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION', + descriptionFull: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION_FULL', + component: DiagnosticsCard, + propControls: [], + icon: , + labels: [ + { + content: 'Beta', + color: 'cyan', }, - height: { - // TODO: implement height resizing - minimum: Number.NaN, - default: Number.NaN, - maximum: Number.NaN, + { + content: 'Diagnostics', + color: 'blue', }, - }; - - export const DiagnosticsCardDescriptor: DashboardCardDescriptor = { - featureLevel: FeatureLevel.BETA, - title: 'CHART_CARD.DIAGNOSTICS_CARD_TITLE', - cardSizes: DiagnosticsCardSizes, - description: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION', - descriptionFull: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION_FULL', - component: DiagnosticsCard, - propControls: [], - icon: , - labels: [ - { - content: 'Beta', - color: 'cyan', - }, - { - content: 'Diagnostics', - color: 'blue', - }, - ], - }; - \ No newline at end of file + ], +}; diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index cd6244500..515d3b92b 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -673,16 +673,16 @@ export class ApiService { runGC(): Observable { return this.target.target().pipe( - concatMap((target) => + concatMap((target) => this.sendRequest('beta', `diagnostics/targets/${target?.id}/gc`, { method: 'POST', }).pipe( map((resp) => resp.ok), catchError(() => of(false)), first(), - ), ), - ); + ), + ); } insertProbes(templateName: string): Observable { From ec57f58e45017c3223b618560d29e158131e788b Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Wed, 25 Sep 2024 15:19:12 -0400 Subject: [PATCH 06/19] Fix error handling --- .../Charts/diagnostics/DiagnosticsCard.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx index 05b6b747a..16a41f8de 100644 --- a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx @@ -49,7 +49,6 @@ export interface DiagnosticsCardProps extends DashboardCardTypeProps { period: number; } -// TODO are these needed? export enum DiagnosticsCardKind {} export function kindToId(kind: string): number { @@ -60,10 +59,15 @@ export const DiagnosticsCard: DashboardCardFC = (props) => const { t } = useTranslation(); const serviceContext = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); + const [errorMessage, setErrorMessage] = React.useState(''); + const isError = React.useMemo(() => errorMessage != '', [errorMessage]); - const handleError = React.useCallback((error) => { - return ; - }, []); + const handleError = React.useCallback( + (error) => { + setErrorMessage(error.message); + }, + [setErrorMessage], + ); const handleGC = React.useCallback(() => { addSubscription( @@ -107,7 +111,9 @@ export const DiagnosticsCard: DashboardCardFC = (props) => ); }, [props.chartKind, props.duration, props.period, t, actions]); - return ( + return isError ? ( + + ) : ( = (props) => headingLevel="h2" /> - }} - > + }}> CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION From 5ad55e9a82025bd627dae7d19797ec3b8cfd0c4d Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Fri, 27 Sep 2024 17:09:26 -0400 Subject: [PATCH 07/19] Add loading spinner to diagnostic card --- .../Charts/diagnostics/DiagnosticsCard.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx index 16a41f8de..86d3f6d28 100644 --- a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx @@ -22,6 +22,7 @@ import { } from '@app/Dashboard/types'; import { ErrorView } from '@app/ErrorView/ErrorView'; import { FeatureLevel } from '@app/Shared/Services/service.types'; +import { LoadingProps } from '@app/Shared/Components/types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { @@ -60,6 +61,7 @@ export const DiagnosticsCard: DashboardCardFC = (props) => const serviceContext = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); const [errorMessage, setErrorMessage] = React.useState(''); + const [running, setRunning] = React.useState(false); const isError = React.useMemo(() => errorMessage != '', [errorMessage]); const handleError = React.useCallback( @@ -95,6 +97,16 @@ export const DiagnosticsCard: DashboardCardFC = (props) => return [GCButton, ...a]; }, [props.actions, GCButton]); + const gcButtonLoadingProps = React.useMemo( + () => + ({ + spinnerAriaValueText: 'Invoke GC', + spinnerAriaLabel: 'saving-credentials', + isLoading: running, + }) as LoadingProps, + [running], + ); + const header = React.useMemo(() => { return ( = (props) => - From ad18b1bdcd869b015a645ce759a6873736a50571 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 1 Oct 2024 11:36:35 -0400 Subject: [PATCH 08/19] Move DiagnosticsCard, clean up localization --- locales/en/public.json | 10 +++++---- .../DiagnosticsCard.tsx | 22 +++++++++++-------- src/app/Dashboard/utils.tsx | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) rename src/app/Dashboard/{Charts/diagnostics => Diagnostics}/DiagnosticsCard.tsx (87%) diff --git a/locales/en/public.json b/locales/en/public.json index 1795058ba..dd8a5118d 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -138,6 +138,12 @@ "CONTENT": "Severity scores are calculated based on the number of JFR events that were triggered by the application in the time the report was generated." } }, + "DiagnosticsCard": { + "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", + "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", + "DIAGNOSTICS_CARD_TITLE": "Diagnostics", + "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection" + }, "CHART_CARD": { "BUTTONS": { "CREATE": { @@ -157,10 +163,6 @@ "MBEAN_METRICS_CARD_DESCRIPTION": "Display common performance metrics from current MBean data.", "MBEAN_METRICS_CARD_DESCRIPTION_FULL": "Display a single performance metric from a list of supported MBeans.", "MBEAN_METRICS_CARD_TITLE": "MBean Metrics Chart", - "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", - "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", - "DIAGNOSTICS_CARD_TITLE": "Diagnostics", - "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection", "NO_RECORDING": { "DESCRIPTION": "Metrics cards display data taken from running Flight Recordings with the label . No such Recordings are currently available.", "TITLE": "No source Recording" diff --git a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx similarity index 87% rename from src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx rename to src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index 86d3f6d28..c39274ec1 100644 --- a/src/app/Dashboard/Charts/diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -42,7 +42,7 @@ import { import { DataSourceIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { DashboardCard } from '../../DashboardCard'; +import { DashboardCard } from '../DashboardCard'; export interface DiagnosticsCardProps extends DashboardCardTypeProps { chartKind: string; @@ -83,7 +83,7 @@ export const DiagnosticsCard: DashboardCardFC = (props) => return ( @@ -180,10 +184,10 @@ export const DiagnosticsCardSizes: DashboardCardSizes = { export const DiagnosticsCardDescriptor: DashboardCardDescriptor = { featureLevel: FeatureLevel.BETA, - title: 'CHART_CARD.DIAGNOSTICS_CARD_TITLE', + title: 'DiagnosticsCard.DIAGNOSTICS_CARD_TITLE', cardSizes: DiagnosticsCardSizes, - description: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION', - descriptionFull: 'CHART_CARD.DIAGNOSTICS_CARD_DESCRIPTION_FULL', + description: 'DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION', + descriptionFull: 'DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION_FULL', component: DiagnosticsCard, propControls: [], icon: , diff --git a/src/app/Dashboard/utils.tsx b/src/app/Dashboard/utils.tsx index 8d330ddb0..aeb9bcc3d 100644 --- a/src/app/Dashboard/utils.tsx +++ b/src/app/Dashboard/utils.tsx @@ -27,7 +27,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAnalysisCard'; -import { DiagnosticsCardDescriptor } from './Charts/diagnostics/DiagnosticsCard'; +import { DiagnosticsCardDescriptor } from './Diagnostics/DiagnosticsCard'; import { JFRMetricsChartCardDescriptor } from './Charts/jfr/JFRMetricsChartCard'; import { MBeanMetricsChartCardDescriptor } from './Charts/mbean/MBeanMetricsChartCard'; import { JvmDetailsCardDescriptor } from './JvmDetails/JvmDetailsCard'; From c1628d097b56490677f5c43a094f59a4559291c3 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 1 Oct 2024 14:45:57 -0400 Subject: [PATCH 09/19] Remove refresh button, cleanup --- .../Dashboard/Diagnostics/DiagnosticsCard.tsx | 39 ++++++------------- src/app/Dashboard/utils.tsx | 2 +- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index c39274ec1..f3ba5a607 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -21,8 +21,8 @@ import { DashboardCardDescriptor, } from '@app/Dashboard/types'; import { ErrorView } from '@app/ErrorView/ErrorView'; -import { FeatureLevel } from '@app/Shared/Services/service.types'; import { LoadingProps } from '@app/Shared/Components/types'; +import { FeatureLevel } from '@app/Shared/Services/service.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { @@ -39,7 +39,7 @@ import { EmptyStateHeader, EmptyStateFooter, } from '@patternfly/react-core'; -import { DataSourceIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; +import { WrenchIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { DashboardCard } from '../DashboardCard'; @@ -67,41 +67,26 @@ export const DiagnosticsCard: DashboardCardFC = (props) => const handleError = React.useCallback( (error) => { setErrorMessage(error.message); + setRunning(false); }, - [setErrorMessage], + [setErrorMessage, setRunning], ); const handleGC = React.useCallback(() => { + setRunning(true); addSubscription( serviceContext.api.runGC().subscribe({ + next: () => setRunning(false), error: (err) => handleError(err), }), ); - }, [addSubscription, serviceContext.api, handleError]); - - const GCButton = React.useMemo(() => { - return ( - From d8aceef8c2bfdb97becd697192a76c9f8e374dc0 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:03:11 -0400 Subject: [PATCH 14/19] remove unused title prop --- src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index 68ae3061c..5fc6f9880 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -88,7 +88,6 @@ export const DiagnosticsCard: DashboardCardFC = (props) => cardSizes={DiagnosticsCardSizes} isCompact cardHeader={header} - title={props.chartKind} isDraggable={props.isDraggable} isResizable={props.isResizable} isFullHeight={props.isFullHeight} From ca8fde944397a1b95c4e6af26684b7466881b7f1 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:08:15 -0400 Subject: [PATCH 15/19] ensure observables complete after first emission --- src/app/Shared/Services/Api.service.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 8725fce03..11a5d65e8 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -361,6 +361,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -376,6 +377,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -395,6 +397,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -410,6 +413,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -424,6 +428,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -451,6 +456,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -467,6 +473,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -617,6 +624,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -631,6 +639,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -654,6 +663,7 @@ export class ApiService { first(), ), ), + first(), ); } @@ -750,6 +760,7 @@ export class ApiService { first(), ), ), + first(), ); } From 4bfdef2e081d632d33193c4db261e585802e7ab5 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:08:27 -0400 Subject: [PATCH 16/19] loading state cleanup --- locales/en/public.json | 3 ++- src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/en/public.json b/locales/en/public.json index dd8a5118d..86b0fd013 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -142,7 +142,8 @@ "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", "DIAGNOSTICS_CARD_TITLE": "Diagnostics", - "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection" + "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection", + "UNKNOWN_FAILURE": "Action failed for unknown reason" }, "CHART_CARD": { "BUTTONS": { diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index 5fc6f9880..d8dea0f98 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -55,18 +55,17 @@ export const DiagnosticsCard: DashboardCardFC = (props) => const handleError = React.useCallback( (error) => { - setErrorMessage(error.message); - setRunning(false); + setErrorMessage(error?.message ?? t('DiagnosticsCard.UNKNOWN_FAILURE')); }, - [setErrorMessage, setRunning], + [setErrorMessage], ); const handleGC = React.useCallback(() => { setRunning(true); addSubscription( serviceContext.api.runGC().subscribe({ - next: () => setRunning(false), error: (err) => handleError(err), + complete: () => setRunning(false), }), ); }, [addSubscription, serviceContext.api, handleError, setRunning]); From 420a52b21c02763ba346d08d3eca47a7595fb080 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:15:22 -0400 Subject: [PATCH 17/19] pass diagnostics failure back to card for handling --- .../Dashboard/Diagnostics/DiagnosticsCard.tsx | 84 +++++++++---------- src/app/Shared/Services/Api.service.tsx | 7 +- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index d8dea0f98..4ac17fcb4 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -42,28 +42,28 @@ import { WrenchIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { DashboardCard } from '../DashboardCard'; +import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; export interface DiagnosticsCardProps extends DashboardCardTypeProps {} export const DiagnosticsCard: DashboardCardFC = (props) => { const { t } = useTranslation(); const serviceContext = React.useContext(ServiceContext); + const notifications = React.useContext(NotificationsContext); const addSubscription = useSubscriptions(); - const [errorMessage, setErrorMessage] = React.useState(''); const [running, setRunning] = React.useState(false); - const isError = React.useMemo(() => errorMessage != '', [errorMessage]); const handleError = React.useCallback( (error) => { - setErrorMessage(error?.message ?? t('DiagnosticsCard.UNKNOWN_FAILURE')); + notifications.danger(t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE'), error); }, - [setErrorMessage], + [notifications], ); const handleGC = React.useCallback(() => { setRunning(true); addSubscription( - serviceContext.api.runGC().subscribe({ + serviceContext.api.runGC(true).subscribe({ error: (err) => handleError(err), complete: () => setRunning(false), }), @@ -78,43 +78,43 @@ export const DiagnosticsCard: DashboardCardFC = (props) => ); }, [t]); - return isError ? ( - - ) : ( - - - - - {t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE')}} - icon={} - headingLevel="h2" - /> - {t('DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION')} - - - - - - - + return ( + <> + + + + + {t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE')}} + icon={} + headingLevel="h2" + /> + {t('DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION')} + + + + + + + + ); }; diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 11a5d65e8..b02c93191 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -628,14 +628,13 @@ export class ApiService { ); } - runGC(): Observable { + runGC(suppressNotifications = false): Observable { return this.target.target().pipe( concatMap((target) => - this.sendRequest('beta', `diagnostics/targets/${target?.id}/gc`, { + this.sendRequest('beta', `diagnostics/targets/${target?.id}/gcd`, { method: 'POST', - }).pipe( + }, undefined, suppressNotifications).pipe( map((resp) => resp.ok), - catchError(() => of(false)), first(), ), ), From 4f5896da2eca6c5b3e4c10ab3881f529fb2bf2ed Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:19:44 -0400 Subject: [PATCH 18/19] error notification styling --- locales/en/public.json | 18 ++++++++++-------- .../Dashboard/Diagnostics/DiagnosticsCard.tsx | 8 ++++---- src/app/Shared/Services/Api.service.tsx | 12 +++++++++--- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/locales/en/public.json b/locales/en/public.json index 86b0fd013..8771e7c77 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -138,13 +138,6 @@ "CONTENT": "Severity scores are calculated based on the number of JFR events that were triggered by the application in the time the report was generated." } }, - "DiagnosticsCard": { - "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", - "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", - "DIAGNOSTICS_CARD_TITLE": "Diagnostics", - "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection", - "UNKNOWN_FAILURE": "Action failed for unknown reason" - }, "CHART_CARD": { "BUTTONS": { "CREATE": { @@ -203,7 +196,6 @@ }, "EVALUATING_EXPRESSION": "Evaluating Match Expression...", "FAILING_EVALUATION": "The expression matching failed.", - "MATCH_EXPRESSION_HELPER_TEXT": "Enter a Match Expression. This is a Common Expression Language (CEL) code snippet that is evaluated against each target application to determine whether the rule should be applied.", "MATCH_EXPRESSION_HINT_BODY": "Try an expression like:", "MATCH_EXPRESSION_HINT_MODAL_HEADER": "Match Expression hint", "MODAL_DESCRIPTION": "Create Stored Credentials for target JVMs. Cryostat will use these credentials to connect to Cryostat agents or target JVMs over JMX (if required).", @@ -335,6 +327,16 @@ }, "DATETIME": "Date and Time" }, + "DiagnosticsCard": { + "DIAGNOSTICS_ACTION_FAILURE": "Diagnostics Failure: {{kind}}", + "DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.", + "DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.", + "DIAGNOSTICS_CARD_TITLE": "Diagnostics", + "DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection", + "KINDS": { + "GC": "Garbage Collection" + } + }, "DurationFilter": { "ARIA_LABELS": { "FROM_DURATION": "duration-from", diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index 4ac17fcb4..bf019dc09 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -54,8 +54,8 @@ export const DiagnosticsCard: DashboardCardFC = (props) => const [running, setRunning] = React.useState(false); const handleError = React.useCallback( - (error) => { - notifications.danger(t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE'), error); + (kind, error) => { + notifications.danger(t('DiagnosticsCard.DIAGNOSTICS_ACTION_FAILURE', { kind }), error?.message || error); }, [notifications], ); @@ -64,11 +64,11 @@ export const DiagnosticsCard: DashboardCardFC = (props) => setRunning(true); addSubscription( serviceContext.api.runGC(true).subscribe({ - error: (err) => handleError(err), + error: (err) => handleError(t('DiagnosticsCard.KINDS.GC'), err), complete: () => setRunning(false), }), ); - }, [addSubscription, serviceContext.api, handleError, setRunning]); + }, [addSubscription, serviceContext.api, handleError, setRunning, t]); const header = React.useMemo(() => { return ( diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index b02c93191..246f731bb 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -631,9 +631,15 @@ export class ApiService { runGC(suppressNotifications = false): Observable { return this.target.target().pipe( concatMap((target) => - this.sendRequest('beta', `diagnostics/targets/${target?.id}/gcd`, { - method: 'POST', - }, undefined, suppressNotifications).pipe( + this.sendRequest( + 'beta', + `diagnostics/targets/${target?.id}/gc`, + { + method: 'POST', + }, + undefined, + suppressNotifications, + ).pipe( map((resp) => resp.ok), first(), ), From 76a3344db61d44bdfd7e3c3828d816d44381f10a Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Tue, 1 Oct 2024 15:26:44 -0400 Subject: [PATCH 19/19] lint --- src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index bf019dc09..b852a30e3 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -20,8 +20,7 @@ import { DashboardCardSizes, DashboardCardDescriptor, } from '@app/Dashboard/types'; -import { ErrorView } from '@app/ErrorView/ErrorView'; -import { LoadingProps } from '@app/Shared/Components/types'; +import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; import { FeatureLevel } from '@app/Shared/Services/service.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; @@ -42,7 +41,6 @@ import { WrenchIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { DashboardCard } from '../DashboardCard'; -import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; export interface DiagnosticsCardProps extends DashboardCardTypeProps {} @@ -57,7 +55,7 @@ export const DiagnosticsCard: DashboardCardFC = (props) => (kind, error) => { notifications.danger(t('DiagnosticsCard.DIAGNOSTICS_ACTION_FAILURE', { kind }), error?.message || error); }, - [notifications], + [notifications, t], ); const handleGC = React.useCallback(() => { @@ -76,7 +74,7 @@ export const DiagnosticsCard: DashboardCardFC = (props) => {t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE')} ); - }, [t]); + }, [props.actions, t]); return ( <>