diff --git a/locales/en/public.json b/locales/en/public.json
index 20b7d1170..8771e7c77 100644
--- a/locales/en/public.json
+++ b/locales/en/public.json
@@ -196,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).",
@@ -328,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
new file mode 100644
index 000000000..b852a30e3
--- /dev/null
+++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx
@@ -0,0 +1,154 @@
+/*
+ * 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 { 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';
+import {
+ Bullseye,
+ Button,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ EmptyStateVariant,
+ EmptyStateHeader,
+ EmptyStateFooter,
+} from '@patternfly/react-core';
+import { WrenchIcon } from '@patternfly/react-icons';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { DashboardCard } from '../DashboardCard';
+
+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 [running, setRunning] = React.useState(false);
+
+ const handleError = React.useCallback(
+ (kind, error) => {
+ notifications.danger(t('DiagnosticsCard.DIAGNOSTICS_ACTION_FAILURE', { kind }), error?.message || error);
+ },
+ [notifications, t],
+ );
+
+ const handleGC = React.useCallback(() => {
+ setRunning(true);
+ addSubscription(
+ serviceContext.api.runGC(true).subscribe({
+ error: (err) => handleError(t('DiagnosticsCard.KINDS.GC'), err),
+ complete: () => setRunning(false),
+ }),
+ );
+ }, [addSubscription, serviceContext.api, handleError, setRunning, t]);
+
+ const header = React.useMemo(() => {
+ return (
+ {...props.actions || []}>, hasNoOffset: false, className: undefined }}>
+ {t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE')}
+
+ );
+ }, [props.actions, t]);
+
+ return (
+ <>
+
+
+
+
+ {t('DiagnosticsCard.DIAGNOSTICS_CARD_TITLE')}>}
+ icon={}
+ headingLevel="h2"
+ />
+ {t('DiagnosticsCard.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: 'DiagnosticsCard.DIAGNOSTICS_CARD_TITLE',
+ cardSizes: DiagnosticsCardSizes,
+ description: 'DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION',
+ descriptionFull: 'DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION_FULL',
+ component: DiagnosticsCard,
+ propControls: [],
+ icon: ,
+ labels: [
+ {
+ content: 'Beta',
+ color: 'cyan',
+ },
+ {
+ content: 'Diagnostics',
+ color: 'blue',
+ },
+ ],
+};
diff --git a/src/app/Dashboard/utils.tsx b/src/app/Dashboard/utils.tsx
index 17bb102a6..88e45848c 100644
--- a/src/app/Dashboard/utils.tsx
+++ b/src/app/Dashboard/utils.tsx
@@ -29,6 +29,7 @@ import { useDispatch } from 'react-redux';
import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAnalysisCard';
import { JFRMetricsChartCardDescriptor } from './Charts/jfr/JFRMetricsChartCard';
import { MBeanMetricsChartCardDescriptor } from './Charts/mbean/MBeanMetricsChartCard';
+import { DiagnosticsCardDescriptor } from './Diagnostics/DiagnosticsCard';
import { JvmDetailsCardDescriptor } from './JvmDetails/JvmDetailsCard';
import {
SerialLayoutTemplate,
@@ -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 2ec5037ae..246f731bb 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,27 @@ export class ApiService {
first(),
),
),
+ first(),
+ );
+ }
+
+ runGC(suppressNotifications = false): Observable {
+ return this.target.target().pipe(
+ concatMap((target) =>
+ this.sendRequest(
+ 'beta',
+ `diagnostics/targets/${target?.id}/gc`,
+ {
+ method: 'POST',
+ },
+ undefined,
+ suppressNotifications,
+ ).pipe(
+ map((resp) => resp.ok),
+ first(),
+ ),
+ ),
+ first(),
);
}
@@ -640,6 +668,7 @@ export class ApiService {
first(),
),
),
+ first(),
);
}
@@ -736,6 +765,7 @@ export class ApiService {
first(),
),
),
+ first(),
);
}