diff --git a/locales/en/public.json b/locales/en/public.json index f20e6fb3b..44c2e82d7 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -365,7 +365,7 @@ "DATETIME": "Date and Time" }, "Diagnostics": { - "THREAD_DUMPS_TAB_TITLE": "Thread Dumps" + "TARGET_THREAD_DUMPS_TAB_TITLE": "Targets" }, "DiagnosticsCard": { "DIAGNOSTICS_ACTION_FAILURE": "Diagnostics Failure: {{kind}}", @@ -374,6 +374,7 @@ "DIAGNOSTICS_CARD_TITLE": "Diagnostics", "DIAGNOSTICS_GC_BUTTON": "Invoke Garbage Collection", "DIAGNOSTICS_THREAD_DUMP_BUTTON": "Invoke Thread Dump", + "DIAGNOSTICS_THREAD_DUMP_TABLE_TOOLTIP": "View captured Thread Dumps", "DIAGONSTICS_THREAD_REDIRECT_BUTTON": "View collected Thread Dumps", "KINDS": { "GC": "Garbage Collection", diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index ced7da5f8..77be03bfd 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -592,6 +592,8 @@ export const AppLayout: React.FC = ({ children }) => { .map((route, idx) => renderable(route, idx)); if (!k) { items = renderables; + } else if (!renderables.length) { + items = []; } else { const anyActive = rs.some((r) => isActiveRoute(r)); items = [ @@ -612,11 +614,13 @@ export const AppLayout: React.FC = ({ children }) => { aria-label={t('AppLayout.TOOLBAR.ARIA_LABELS.GLOBAL_NAVIGATION')} > - {Array.from(groups.entries()).map(([groupTitle, items]) => ( - - {items} - - ))} + {Array.from(groups.entries()) + .filter(([_, items]) => items.length) + .map(([groupTitle, items]) => ( + + {items} + + ))} ); diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index a323b8ec7..1e54f2d20 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -172,7 +172,6 @@ export const Archives: React.FC = ({ ...props }) => { {cardBody} - <> ); }; diff --git a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx index 607d4e599..21f7c6326 100644 --- a/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx +++ b/src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx @@ -21,6 +21,7 @@ import { DashboardCardDescriptor, } from '@app/Dashboard/types'; import { CryostatLink } from '@app/Shared/Components/CryostatLink'; +import { FeatureFlag } from '@app/Shared/Components/FeatureFlag'; import { NotificationCategory } from '@app/Shared/Services/api.types'; import { NotificationsContext } from '@app/Shared/Services/Notifications.service'; import { FeatureLevel } from '@app/Shared/Services/service.types'; @@ -41,6 +42,8 @@ import { EmptyStateFooter, ActionList, Tooltip, + Stack, + StackItem, } from '@patternfly/react-core'; import { ListIcon, WrenchIcon } from '@patternfly/react-icons'; import * as React from 'react'; @@ -141,36 +144,44 @@ export const DiagnosticsCard: DashboardCardFC = (props) => /> {t('DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION')} - - - - - - - + + + + + + + + + + + + + + + - - - {deleteThreadDumpModal} - - - {threadDumpRows.length ? ( - - - - {tableColumns.map(({ title, sortable }, index) => ( - - ))} - - - {threadDumpRows} -
- {title} -
- ) : ( - - } - headingLevel="h4" - /> - - )} -
- - + + + + + + + + + + {deleteThreadDumpModal} + + + {threadDumpRows.length ? ( + + + + {tableColumns.map(({ title, sortable }, index) => ( + + ))} + + + {threadDumpRows} +
+ {title} +
+ ) : ( + + } + headingLevel="h4" + /> + + )} +
+
); } }; @@ -371,7 +340,7 @@ export const ThreadDumpAction: React.FC = ({ threadDump, const actionItems = React.useMemo(() => { return [ { - title: 'Download Thread Dump', + title: 'Download', key: 'download-threaddump', onClick: () => onDownload(threadDump), }, @@ -391,19 +360,23 @@ export const ThreadDumpAction: React.FC = ({ threadDump, const dropdownItems = React.useMemo( () => - actionItems.map((action) => ( - { - setIsOpen(false); - action.onClick && action.onClick(); - }} - isDanger={action.isDanger} - > - {action.title} - - )), + actionItems.map((action, idx) => + action.isSeparator ? ( + + ) : ( + { + setIsOpen(false); + action.onClick && action.onClick(); + }} + isDanger={action.isDanger} + > + {action.title} + + ), + ), [actionItems, setIsOpen], ); diff --git a/src/app/JMCAgent/AboutAgentCard.tsx b/src/app/JMCAgent/AboutAgentCard.tsx deleted file mode 100644 index afa67237a..000000000 --- a/src/app/JMCAgent/AboutAgentCard.tsx +++ /dev/null @@ -1,33 +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. - */ -import { Card, CardBody, CardTitle } from '@patternfly/react-core'; -import * as React from 'react'; - -export interface AboutAgentCardProps {} - -export const AboutAgentCard: React.FC = (_) => { - return ( - - About the JMC Agent - - The JMC Agent allows users to dynamically inject custom JFR events into running JVMs. In order to make use of - the JMC Agent, the agent jar must be present in the same container as the target, and the target must be started - with the agent (-javaagent:/path/to/agent.jar). Once these pre-requisites are met, the user can upload Probe - Templates to Cryostat and insert them to the target, as well as view or remove currently active probes. - - - ); -}; diff --git a/src/app/JMCAgent/AgentLiveProbes.tsx b/src/app/JMCAgent/AgentLiveProbes.tsx index 16f4537de..6105ca630 100644 --- a/src/app/JMCAgent/AgentLiveProbes.tsx +++ b/src/app/JMCAgent/AgentLiveProbes.tsx @@ -30,8 +30,6 @@ import { ToolbarContent, ToolbarGroup, ToolbarItem, - Stack, - StackItem, EmptyState, EmptyStateIcon, EmptyStateHeader, @@ -53,7 +51,6 @@ import { import _ from 'lodash'; import * as React from 'react'; import { combineLatest } from 'rxjs'; -import { AboutAgentCard } from './AboutAgentCard'; export type LiveProbeActions = 'REMOVE'; @@ -319,72 +316,65 @@ export const AgentLiveProbes: React.FC = () => { } else { return ( <> - - - - - - - - - - - - - - - - - - - - - - {probeRows.length ? ( - - - - {tableColumns.map(({ title, sortable }, index) => ( - - ))} - - - {probeRows} -
- {title} -
- ) : ( - - } - headingLevel="h4" + + + + + - - )} -
-
+ + + + + + + + + + + + {probeRows.length ? ( + + + + {tableColumns.map(({ title, sortable }, index) => ( + + ))} + + + {probeRows} +
+ {title} +
+ ) : ( + + } + headingLevel="h4" + /> + + )} ); } diff --git a/src/app/JMCAgent/AgentProbeTemplates.tsx b/src/app/JMCAgent/AgentProbeTemplates.tsx index c3d1681ff..29f6194b7 100644 --- a/src/app/JMCAgent/AgentProbeTemplates.tsx +++ b/src/app/JMCAgent/AgentProbeTemplates.tsx @@ -35,6 +35,8 @@ import { ModalVariant, Stack, StackItem, + Text, + TextVariants, Toolbar, ToolbarContent, ToolbarGroup, @@ -47,6 +49,7 @@ import { MenuToggle, SearchInput, Divider, + TextContent, } from '@patternfly/react-core'; import { SearchIcon, EllipsisVIcon, UploadIcon } from '@patternfly/react-icons'; import { @@ -65,7 +68,6 @@ import _ from 'lodash'; import * as React from 'react'; import { forkJoin, Observable, of } from 'rxjs'; import { catchError, defaultIfEmpty, first, tap } from 'rxjs/operators'; -import { AboutAgentCard } from './AboutAgentCard'; const tableColumns: TableColumn[] = [ { @@ -294,7 +296,16 @@ export const AgentProbeTemplates: React.FC = ({ agentD <> - + + About the JMC Agent + + The JMC Agent allows users to dynamically inject custom JFR events into running JVMs. In order to make + use of the JMC Agent, the agent jar must be present in the same container as the target, and the target + must be started with the agent (-javaagent:/path/to/agent.jar). Once these pre-requisites are met, the + user can upload Probe Templates to Cryostat and insert them to the target, as well as view or remove + currently active probes. + + diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 34338176b..3941e8e9a 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -246,6 +246,7 @@ export interface ThreadDump { uuid: string; jvmId?: string; lastModified?: number; + size?: number; } export interface ArchivedRecording extends Recording { diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 474408d6c..0740cf24e 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -21,7 +21,8 @@ import Archives from './Archives/Archives'; import CreateRecording from './CreateRecording/CreateRecording'; import Dashboard from './Dashboard/Dashboard'; import DashboardSolo from './Dashboard/DashboardSolo'; -import Diagnostics from './Diagnostics/Diagnostics'; +import AnalyzeThreadDumps from './Diagnostics/AnalyzeThreadDumps'; +import CaptureDiagnostics from './Diagnostics/CaptureDiagnostics'; import Events from './Events/Events'; import JMCAgent from './JMCAgent/JMCAgent'; import NotFound from './NotFound/NotFound'; @@ -170,13 +171,23 @@ const flightRecorderRoutes: IAppRoute[] = [ const diagnosticsRoutes: IAppRoute[] = [ { - component: Diagnostics, - label: 'Thread Dumps', + component: CaptureDiagnostics, + label: 'Capture', path: toPath('/diagnostics'), + title: 'Capture', + description: 'Perform garbage collection and create thread dumps on single target JVMs.', + navGroup: DIAGNOSTICS, + featureLevel: FeatureLevel.BETA, + }, + { + component: AnalyzeThreadDumps, + label: 'Thread Dumps', + path: toPath('/thread-dumps'), title: 'Thread Dumps', - description: 'Create and view thread dumps on single target JVMs.', + description: 'View thread dumps on single target JVMs.', navGroup: DIAGNOSTICS, navSubgroup: ANALYZE, + featureLevel: FeatureLevel.BETA, }, ]; diff --git a/src/test/Archives/__snapshots__/Archives.test.tsx.snap b/src/test/Archives/__snapshots__/Archives.test.tsx.snap index 140486ab4..72becfe71 100644 --- a/src/test/Archives/__snapshots__/Archives.test.tsx.snap +++ b/src/test/Archives/__snapshots__/Archives.test.tsx.snap @@ -159,9 +159,6 @@ exports[` renders correctly 1`] = ` -
diff --git a/src/test/Diagnostics/Diagnostics.test.tsx b/src/test/Diagnostics/ThreadDumpsTable.test.tsx similarity index 89% rename from src/test/Diagnostics/Diagnostics.test.tsx rename to src/test/Diagnostics/ThreadDumpsTable.test.tsx index ad611ac92..ca0158c2c 100644 --- a/src/test/Diagnostics/Diagnostics.test.tsx +++ b/src/test/Diagnostics/ThreadDumpsTable.test.tsx @@ -70,8 +70,6 @@ jest .mockReturnValueOnce(of(mockThreadDumpNotification)) .mockReturnValue(of()); -const dumpThreadsSpy = jest.spyOn(defaultServices.api, 'runThreadDump').mockReturnValue(of('someJobId')); - describe('', () => { afterEach(cleanup); @@ -95,21 +93,6 @@ describe('', () => { expect(xmlHeader).toBeVisible(); }); - it('should upload a Thread Dump when button is clicked', async () => { - const { user } = render({ - routerConfigs: { routes: [{ path: '/diagnostics', element: }] }, - }); - - await act(async () => { - const uploadButton = screen.getByRole('button', { name: 'dump-threads' }); - expect(uploadButton).toBeInTheDocument(); - expect(uploadButton).toBeVisible(); - - await user.click(uploadButton); - expect(dumpThreadsSpy).toHaveBeenCalledTimes(1); - }); - }); - it('should show warning modal and delete a Thread Dump when confirmed', async () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteThreadDump').mockReturnValue(of(true)); const { user } = render({ diff --git a/src/test/JMCAgent/__snapshots__/AgentLiveProbes.test.tsx.snap b/src/test/JMCAgent/__snapshots__/AgentLiveProbes.test.tsx.snap index 90e4d495c..2d173b292 100644 --- a/src/test/JMCAgent/__snapshots__/AgentLiveProbes.test.tsx.snap +++ b/src/test/JMCAgent/__snapshots__/AgentLiveProbes.test.tsx.snap @@ -1,463 +1,426 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[` renders correctly 1`] = ` -
+[
- About the JMC Agent -
-
-
- The JMC Agent allows users to dynamically inject custom JFR events into running JVMs. In order to make use of the JMC Agent, the agent jar must be present in the same container as the target, and the target must be started with the agent (-javaagent:/path/to/agent.jar). Once these pre-requisites are met, the user can upload Probe Templates to Cryostat and insert them to the target, as well as view or remove currently active probes. -
-
-
-
-
-
-
-
- - - - - - + + -
+ +
-
+
+
+
-
- -
+ Remove + all probes +
+
+ -
+ some_id + + + some_name + + + some_clazz + + + some_desc + + + a_method + + + + , +] `;