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(), ); }