From 850fad4a3e48ef894e4ded53a6f1b28b019e035e Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 15 Aug 2022 15:57:09 -0400 Subject: [PATCH 01/43] Agent Plugin Changes --- package.json | 2 +- src/app/Agent/Agent.tsx | 68 ++++++ src/app/Agent/AgentLiveProbes.tsx | 181 ++++++++++++++ src/app/Agent/AgentProbeTemplates.tsx | 304 ++++++++++++++++++++++++ src/app/Shared/Services/Api.service.tsx | 100 ++++++++ src/app/routes.tsx | 9 + 6 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 src/app/Agent/Agent.tsx create mode 100644 src/app/Agent/AgentLiveProbes.tsx create mode 100644 src/app/Agent/AgentProbeTemplates.tsx diff --git a/package.json b/package.json index fbba3c16f..561adaa01 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "webpack-cli": "^4.7.2", "webpack-dev-server": "^3.11.2", "webpack-merge": "^5.8.0", - "yarn": "^1.22.13" + "yarn": "^1.22.18" }, "dependencies": { "@patternfly/react-core": "^4.157.3", diff --git a/src/app/Agent/Agent.tsx b/src/app/Agent/Agent.tsx new file mode 100644 index 000000000..689e8f108 --- /dev/null +++ b/src/app/Agent/Agent.tsx @@ -0,0 +1,68 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { TargetView } from '@app/TargetView/TargetView'; +import { Card, CardBody, Tab, Tabs } from '@patternfly/react-core'; +import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; +import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; + +export const Agent = () => { + const [activeTab, setActiveTab] = React.useState(0); + + const handleTabSelect = (evt, idx) => { + setActiveTab(idx); + } + + return (<> + + + + + + + + + + + + + + + ); + +} \ No newline at end of file diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx new file mode 100644 index 000000000..a3caf44ce --- /dev/null +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -0,0 +1,181 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; +import { NO_TARGET } from '@app/Shared/Services/Target.service'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { ActionGroup, Button, FileUpload, Form, FormGroup, Modal, ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, TextInput } from '@patternfly/react-core'; +import { PlusIcon } from '@patternfly/react-icons'; +import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraData, ISortBy, SortByDirection, sortable } from '@patternfly/react-table'; +import { useHistory } from 'react-router-dom'; +import { concatMap, filter, first } from 'rxjs/operators'; +import { LoadingView } from '@app/LoadingView/LoadingView'; +import { ErrorView } from '@app/ErrorView/ErrorView'; +import { EventProbe } from '@app/Shared/Services/Api.service'; + +export const AgentLiveProbes = () => { + + const context = React.useContext(ServiceContext); + const history = useHistory(); + + const [templates, setTemplates] = React.useState([] as EventProbe[]); + const [filteredTemplates, setFilteredTemplates] = React.useState([] as EventProbe[]); + const [filterText, setFilterText] = React.useState(''); + const [sortBy, setSortBy] = React.useState({} as ISortBy); + const [isLoading, setIsLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const addSubscription = useSubscriptions(); + + const tableColumns = [ + { title: 'Label', transforms: [ sortable ] }, + { title: 'Description' , transforms: [ sortable ] }, + { title: 'Class' , transforms: [ sortable ] }, + { title: 'Stacktrace' , transforms: [ sortable ] }, + { title: 'Rethrow' , transforms: [ sortable ] }, + { title: 'Method' , transforms: [ sortable ] } + ]; + + React.useEffect(() => { + let filtered; + if (!filterText) { + filtered = templates; + } else { + console.log("did we get here?"); + const ft = filterText.trim().toLowerCase(); + filtered = templates.filter((t: EventProbe) => t.label.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) + || t.class.toLowerCase().includes(ft) || t.stacktrace.toLowerCase().includes(ft) || + t.rethrow.toLowerCase().includes(ft) || t.methodname.toLowerCase().includes(ft)); + } + const { index, direction } = sortBy; + if (typeof index === 'number') { + const keys = ['Label', 'Description', 'Class', 'Stacktrace', 'Rethrow', 'Method']; + const key = keys[index]; + const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); + filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); + } + setFilteredTemplates([...filtered]); + }, [filterText, templates, sortBy]); + + const handleTemplates = React.useCallback((templates) => { + console.log(templates); + templates = JSON.parse(templates); + setTemplates(templates); + setIsLoading(false); + setErrorMessage(''); + }, [setTemplates, setIsLoading, setErrorMessage]); + + const handleError = React.useCallback((error) => { + setIsLoading(false); + setErrorMessage(error.message); + }, [setIsLoading, setErrorMessage]); + + const refreshTemplates = React.useCallback(() => { + setIsLoading(true) + addSubscription( + context.target.target() + .pipe( + concatMap(target => context.api.getActiveProbes()), + first() + ).subscribe(value => handleTemplates(value), err => handleError(err)) + ); + }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe(() => { + setFilterText(''); + refreshTemplates(); + })); + }, [context, context.target, addSubscription, refreshTemplates]); + + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval(() => refreshTemplates(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + return () => window.clearInterval(id); + }, []); + + React.useEffect(() => { + const sub = context.target.authFailure().subscribe(() => { + setErrorMessage("Auth failure"); + }); + return () => sub.unsubscribe(); + }, [context.target]); + + const displayTemplates = React.useMemo( + () => templates.map((t: EventProbe) => ([ t.label , t.description , t.class , t.rethrow , t.stacktrace , t.methodname ])), + [templates] + ); + + const handleModalToggle = () => { + addSubscription( + context.api.removeProbes() + .pipe(first()) + .subscribe(() => {}) + ); + }; + + if (errorMessage != '') { + return () + } else if (isLoading) { + return () + } else { + return (<> + + + + + + + + + + + + +
+ ); + } + +} diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx new file mode 100644 index 000000000..698bb8dfb --- /dev/null +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -0,0 +1,304 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; +import { NO_TARGET } from '@app/Shared/Services/Target.service'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { ActionGroup, Button, FileUpload, Form, FormGroup, Modal, ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, TextInput } from '@patternfly/react-core'; +import { PlusIcon } from '@patternfly/react-icons'; +import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraData, ISortBy, SortByDirection, sortable } from '@patternfly/react-table'; +import { useHistory } from 'react-router-dom'; +import { concatMap, filter, first } from 'rxjs/operators'; +import { LoadingView } from '@app/LoadingView/LoadingView'; +import { ErrorView } from '@app/ErrorView/ErrorView'; +import { ProbeTemplate } from '@app/Shared/Services/Api.service'; + +export const AgentProbeTemplates = () => { + + const context = React.useContext(ServiceContext); + const history = useHistory(); + + const [templates, setTemplates] = React.useState([] as ProbeTemplate[]); + const [filteredTemplates, setFilteredTemplates] = React.useState([] as ProbeTemplate[]); + const [filterText, setFilterText] = React.useState(''); + const [modalOpen, setModalOpen] = React.useState(false); + const [uploadFile, setUploadFile] = React.useState(undefined as File | undefined); + const [uploadFilename, setUploadFilename] = React.useState(''); + const [uploading, setUploading] = React.useState(false); + const [fileRejected, setFileRejected] = React.useState(false); + const [sortBy, setSortBy] = React.useState({} as ISortBy); + const [isLoading, setIsLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const addSubscription = useSubscriptions(); + + const tableColumns = [ + { title: 'name', transforms: [ sortable ] }, + { title: 'xml' , transforms: [ sortable ] } + ]; + + React.useEffect(() => { + let filtered; + if (!filterText) { + filtered = templates; + } else { + console.log("did we get here?"); + const ft = filterText.trim().toLowerCase(); + filtered = templates.filter((t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft)); + } + const { index, direction } = sortBy; + if (typeof index === 'number') { + const keys = ['name', 'xml']; + const key = keys[index]; + const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); + filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); + } + setFilteredTemplates([...filtered]); + }, [filterText, templates, sortBy]); + + const handleTemplates = React.useCallback((templates) => { + console.log(templates); + templates = JSON.parse(templates); + setTemplates(templates); + setIsLoading(false); + setErrorMessage(''); + }, [setTemplates, setIsLoading, setErrorMessage]); + + const handleError = React.useCallback((error) => { + setIsLoading(false); + setErrorMessage(error.message); + }, [setIsLoading, setErrorMessage]); + + const refreshTemplates = React.useCallback(() => { + setIsLoading(true) + addSubscription( + context.target.target() + .pipe( + concatMap(target => context.api.getProbeTemplates()), + first() + ).subscribe(value => handleTemplates(value), err => handleError(err)) + ); + }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe(() => { + setFilterText(''); + refreshTemplates(); + })); + }, [context, context.target, addSubscription, refreshTemplates]); + + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval(() => refreshTemplates(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + return () => window.clearInterval(id); + }, []); + + React.useEffect(() => { + const sub = context.target.authFailure().subscribe(() => { + setErrorMessage("Auth failure"); + }); + return () => sub.unsubscribe(); + }, [context.target]); + + const displayTemplates = React.useMemo( + () => templates.map((t: ProbeTemplate) => ([ t.name , t.xml])), + [templates] + ); + + const handleDelete = (rowData) => { + addSubscription( + context.api.deleteCustomProbeTemplate(rowData[0]) + .pipe(first()) + .subscribe(() => {} /* do nothing - notification will handle updating state */) + ); + }; + + const handleInsert = (rowData) => { + addSubscription( + context.api.insertProbes(rowData[0]) + .pipe(first()) + .subscribe(() => {}) + ); + }; + + const actionResolver = (rowData: IRowData, extraData: IExtraData) => { + if (typeof extraData.rowIndex == 'undefined') { + return []; + } + let actions = [ + { + title: 'Insert Probes...', + onClick: (event, rowId, rowData) => handleInsert(rowData) + }, + ] as IAction[]; + actions = actions.concat([ + { + isSeparator: true, + }, + { + title: 'Delete', + onClick: (event, rowId, rowData) => handleDelete(rowData) + } + ]); + return actions; + }; + + const handleModalToggle = () => { + setModalOpen(v => { + if (v) { + setUploadFile(undefined); + setUploadFilename(''); + setUploading(false); + } + return !v; + }); + }; + + const handleFileChange = (value, filename) => { + setFileRejected(false); + setUploadFile(value); + setUploadFilename(filename); + }; + + const handleUploadSubmit = () => { + if (!uploadFile) { + window.console.error('Attempted to submit template upload without a file selected'); + return; + } + setUploading(true); + addSubscription( + context.api.addCustomProbeTemplate(uploadFile) + .pipe(first()) + .subscribe(success => { + setUploading(false); + if (success) { + setUploadFile(undefined); + setUploadFilename(''); + setModalOpen(false); + } + }) + ); + }; + + const handleUploadCancel = () => { + setUploadFile(undefined); + setUploadFilename(''); + setModalOpen(false); + }; + + const handleFileRejected = () => { + setFileRejected(true); + }; + + const handleSort = (event, index, direction) => { + setSortBy({ index, direction }); + }; + + if (errorMessage != '') { + return () + } else if (isLoading) { + return () + } else { + return (<> + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + +
+
+ ); + } + +} diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index fa95b4749..441a3f895 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -351,6 +351,76 @@ export class ApiService { ); } + removeProbes(): Observable { + return this.target.target().pipe(concatMap(target => + this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { + method: 'DELETE', + }).pipe( + tap(resp => { + if (resp.status == 200) { + this.notifications.success('Probes Removed'); + } else if (resp.status == 400) { + this.notifications.warning('Failed to remove Probes', 'The probes failed to be removed from the target'); + } + }), + map(resp => resp.status == 200), + first(), + ) + )); + } + + insertProbes(templateName: string): Observable { + return this.target.target().pipe(concatMap(target => + this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes/${encodeURIComponent(templateName)}`, { + method: 'POST', + }).pipe( + tap(resp => { + if (resp.status == 200) { + this.notifications.success('Probes Inserted'); + } else if (resp.status == 400) { + this.notifications.warning('Failed to Insert Probes', 'The probes failed to be injected. Check that the agent is present in the same container as the target JVM and the target is running with -javaagent:/path/to/agent'); + } + }), + map(resp => resp.status == 200), + first(), + ) + )); + } + + addCustomProbeTemplate(file: File): Observable { + const body = new window.FormData(); + body.append('probeTemplate', file); + return this.sendRequest('v2', `probes/`+file.name, { + method: 'POST', + body, + }) + .pipe( + map(response => { + if (!response.ok) { + throw response.statusText; + } + return true; + }), + catchError((): ObservableInput => of(false)), + ); + } + + deleteCustomProbeTemplate(templateName: string): Observable { + return this.sendRequest('v2', `probes/${encodeURIComponent(templateName)}`, { + method: 'DELETE', + body: null, + }) + .pipe( + map(response => { + if (!response.ok) { + throw response.statusText; + } + return true; + }), + catchError((): ObservableInput => of(false)), + ); + } + cryostatVersion(): Observable { return this.cryostatVersionSubject.asObservable(); } @@ -367,6 +437,22 @@ export class ApiService { return this.sendRequest(apiVersion, path, { method: 'GET' }).pipe(map(resp => resp.json()), concatMap(from), first()); } + getProbeTemplates(): Observable { + return this.sendRequest('v2', 'probes', { method: 'GET' }). + pipe(concatMap(resp => resp.json()), + map(response => response.data.result), + first()); + } + + getActiveProbes(): Observable { + return this.target.target().pipe(concatMap(target => + this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { + method: 'GET', + }).pipe(concatMap(resp => resp.json()), + map(response => response.data.result), + first()))); + } + downloadReport(recording: ArchivedRecording): void { const body = new window.FormData(); body.append('resource', recording.reportUrl.replace('/api/v1', '/api/v2.1')); @@ -588,3 +674,17 @@ export interface RecordingAttributes { duration?: number; options?: RecordingOptions; } + +export interface ProbeTemplate { + name: string; + xml: string; +} + +export interface EventProbe { + label: string; + description: string; + class: string; + rethrow: string; + stacktrace: string; + methodname: string; +} \ No newline at end of file diff --git a/src/app/routes.tsx b/src/app/routes.tsx index d55b0f640..c2368fe64 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -39,6 +39,7 @@ import * as React from 'react'; import { CreateRecording } from '@app/CreateRecording/CreateRecording'; import { Dashboard } from '@app/Dashboard/Dashboard'; import { Events } from '@app/Events/Events'; +import { Agent } from '@app/Agent/Agent'; import { Login } from '@app/Login/Login'; import { NotFound } from '@app/NotFound/NotFound'; import { Recordings } from '@app/Recordings/Recordings'; @@ -129,6 +130,14 @@ const routes: IAppRoute[] = [ title: 'Events', navGroup: CONSOLE, }, + { + component: Agent, + exact: true, + label: 'Agent', + path: '/agent', + title: 'Agent', + navGroup: CONSOLE, + }, { component: SecurityPanel, exact: true, From aff2c4fb5d6bcc7181ffec68bf929464a0bef25e Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 15 Aug 2022 15:57:48 -0400 Subject: [PATCH 02/43] tmp --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index d7efdf5bd..bd519850f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8399,10 +8399,10 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yarn@^1.22.13: - version "1.22.13" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.13.tgz#8789ef23b630fe99b819b044f4b7b93ab1bc1b8f" - integrity sha512-G8qG4t7Ef5cLVpzbM3HWWsow4hpfeSCfKtMnjfERmp9V5qSCOKz0uGAIQCM/x3gWfCzH8Bvb4hl3ZfhG/XD1Jg== +yarn@^1.22.18: + version "1.22.18" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.18.tgz#05b822ade8c672987bab8858635145da0850f78a" + integrity sha512-oFffv6Jp2+BTUBItzx1Z0dpikTX+raRdqupfqzeMKnoh7WD6RuPAxcqDkMUy9vafJkrB0YaV708znpuMhEBKGQ== yocto-queue@^0.1.0: version "0.1.0" From 9776154e243db22419187c0f3704f2b5bd56690a Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Wed, 12 Oct 2022 16:43:52 -0400 Subject: [PATCH 03/43] Fix refreshing of ProbeTemplate table, add notifications --- src/app/Agent/AgentLiveProbes.tsx | 25 +++++++++++++------ src/app/Agent/AgentProbeTemplates.tsx | 20 ++++++++++++--- .../Services/NotificationChannel.service.tsx | 24 ++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index a3caf44ce..ca6c2d9d6 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -76,7 +76,6 @@ export const AgentLiveProbes = () => { if (!filterText) { filtered = templates; } else { - console.log("did we get here?"); const ft = filterText.trim().toLowerCase(); filtered = templates.filter((t: EventProbe) => t.label.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) || t.class.toLowerCase().includes(ft) || t.stacktrace.toLowerCase().includes(ft) || @@ -93,25 +92,25 @@ export const AgentLiveProbes = () => { }, [filterText, templates, sortBy]); const handleTemplates = React.useCallback((templates) => { - console.log(templates); templates = JSON.parse(templates); setTemplates(templates); - setIsLoading(false); setErrorMessage(''); + setIsLoading(false); }, [setTemplates, setIsLoading, setErrorMessage]); const handleError = React.useCallback((error) => { - setIsLoading(false); setErrorMessage(error.message); + setIsLoading(false); }, [setIsLoading, setErrorMessage]); const refreshTemplates = React.useCallback(() => { - setIsLoading(true) + setIsLoading(true); addSubscription( context.target.target() .pipe( + filter(target => target !== NO_TARGET), + first(), concatMap(target => context.api.getActiveProbes()), - first() ).subscribe(value => handleTemplates(value), err => handleError(err)) ); }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); @@ -148,10 +147,17 @@ export const AgentLiveProbes = () => { addSubscription( context.api.removeProbes() .pipe(first()) - .subscribe(() => {}) + .subscribe(() => {refreshTemplates();}) ); }; + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.TargetProbesGet) + .subscribe(v => setTemplates(old => old.concat(v.message.probes))) + ); + }, [addSubscription, context, context.notificationChannel, setTemplates]); + if (errorMessage != '') { return () } else if (isLoading) { @@ -160,6 +166,11 @@ export const AgentLiveProbes = () => { return (<> + + + + + diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 698bb8dfb..e094cec05 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -77,7 +77,6 @@ export const AgentProbeTemplates = () => { if (!filterText) { filtered = templates; } else { - console.log("did we get here?"); const ft = filterText.trim().toLowerCase(); filtered = templates.filter((t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft)); } @@ -92,7 +91,6 @@ export const AgentProbeTemplates = () => { }, [filterText, templates, sortBy]); const handleTemplates = React.useCallback((templates) => { - console.log(templates); templates = JSON.parse(templates); setTemplates(templates); setIsLoading(false); @@ -110,7 +108,7 @@ export const AgentProbeTemplates = () => { context.target.target() .pipe( concatMap(target => context.api.getProbeTemplates()), - first() + first(), ).subscribe(value => handleTemplates(value), err => handleError(err)) ); }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); @@ -147,7 +145,7 @@ export const AgentProbeTemplates = () => { addSubscription( context.api.deleteCustomProbeTemplate(rowData[0]) .pipe(first()) - .subscribe(() => {} /* do nothing - notification will handle updating state */) + .subscribe(() => {}) ); }; @@ -218,6 +216,20 @@ export const AgentProbeTemplates = () => { ); }; + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ProbeTemplateUploaded) + .subscribe(v => refreshTemplates()) + ); + }, [addSubscription, context, context.notificationChannel, setTemplates]); + + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.TemplateDeleted) + .subscribe(v => refreshTemplates()) + ) + }, [addSubscription, context, context.notificationChannel, setTemplates]); + const handleUploadCancel = () => { setUploadFile(undefined); setUploadFilename(''); diff --git a/src/app/Shared/Services/NotificationChannel.service.tsx b/src/app/Shared/Services/NotificationChannel.service.tsx index 072a116bf..3a28fc2f1 100644 --- a/src/app/Shared/Services/NotificationChannel.service.tsx +++ b/src/app/Shared/Services/NotificationChannel.service.tsx @@ -59,6 +59,9 @@ export enum NotificationCategory { ArchivedRecordingDeleted = 'ArchivedRecordingDeleted', TemplateUploaded = 'TemplateUploaded', TemplateDeleted = 'TemplateDeleted', + ProbeTemplateUploaded = 'ProbeTemplateUploaded', + ProbeTemplateDeleted = 'ProbeTemplateDeleted', + TargetProbesGet = 'TargetProbesGet', RuleCreated = 'RuleCreated', RuleDeleted = 'RuleDeleted', RecordingMetadataUpdated = 'RecordingMetadataUpdated', @@ -182,6 +185,13 @@ export const messageKeys = new Map([ body: evt => `${evt.message.template.name} was created` } as NotificationMessageMapper ], + [ + NotificationCategory.ProbeTemplateUploaded, { + variant: AlertVariant.success, + title: 'Probe Template Created', + body: evt => `${evt.message.template.name} was created` + } as NotificationMessageMapper + ], [ NotificationCategory.TemplateDeleted, { variant: AlertVariant.success, @@ -189,6 +199,13 @@ export const messageKeys = new Map([ body: evt => `${evt.message.template.name} was deleted` } as NotificationMessageMapper ], + [ + NotificationCategory.ProbeTemplateDeleted, { + variant: AlertVariant.success, + title: 'Probe Template Deleted', + body: evt => `${evt.message.template.name} was deleted` + } as NotificationMessageMapper + ], [ NotificationCategory.RuleCreated, { variant: AlertVariant.success, @@ -224,6 +241,13 @@ export const messageKeys = new Map([ body: evt => `Credentials deleted for target: ${evt.message.target}` } as NotificationMessageMapper ], + [ + NotificationCategory.TargetProbesGet, { + variant: AlertVariant.success, + title: 'Target Probes Fetched', + body: evt => `Probes Fetched: ${evt.message.probes}` + } as NotificationMessageMapper + ], [ NotificationCategory.CredentialsStored, { variant: AlertVariant.success, From e9a53dbdda7eeaf2fe834797c4b895095f725ff2 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Wed, 12 Oct 2022 17:41:12 -0400 Subject: [PATCH 04/43] Adding Agent Tests --- src/test/Agent/Agent.test.tsx | 197 ++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/test/Agent/Agent.test.tsx diff --git a/src/test/Agent/Agent.test.tsx b/src/test/Agent/Agent.test.tsx new file mode 100644 index 000000000..6b24ee6b5 --- /dev/null +++ b/src/test/Agent/Agent.test.tsx @@ -0,0 +1,197 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import { render, screen, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { of } from 'rxjs'; +import { EventTemplate, ProbeTemplate } from '@app/Shared/Services/Api.service'; +import { MessageMeta, MessageType, NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; +import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; +import { EventTemplates } from '@app/Events/EventTemplates'; +import userEvent from '@testing-library/user-event'; +import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; +import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; + +const mockConnectUrl = 'service:jmx:rmi://someUrl'; +const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; + +const mockMessageType = {type: "application", subtype: "json"} as MessageType; + +const mockCustomEventTemplate: ProbeTemplate = { + name: 'someProbeTemplate', + xml: '' +}; + +const mockAnotherTemplate = {...mockCustomEventTemplate, name: 'anotherProbeTemplate'} + +const mockCreateTemplateNotification = { + meta: { + category: 'ProbeTemplateUploaded', + type: mockMessageType + } as MessageMeta, + message: { + template: mockAnotherTemplate + } +} as NotificationMessage; +const mockDeleteTemplateNotification = +{...mockCreateTemplateNotification, + meta: { + category: 'ProbeTemplateDeleted', + type: mockMessageType + } +}; + +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: () => ({ url: '/baseUrl' }), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor') + .mockReturnValueOnce(true) // show deletion warning + .mockReturnValue(false); // don't ask again + +jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); +jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); +jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); +jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); + +jest.spyOn(defaultServices.api, 'getProbeTemplates').mockReturnValue(of([mockCustomEventTemplate])); + +jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); +jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); + +jest + .spyOn(defaultServices.notificationChannel, 'messages') + .mockReturnValueOnce(of()) // renders correctly + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of(mockCreateTemplateNotification)) // adds a template after receiving a notification + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of(mockDeleteTemplateNotification)) // removes a template after receiving a notification + .mockReturnValue(of()); // all other tests + + describe('', () => { + it('renders correctly', async () => { + let tree; + await act(async () => { + tree = renderer.create( + + + + ); + }); + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('adds a recording after receiving a notification', () => { + render( + + + + ); + + expect(screen.getByText('someEventTemplate')).toBeInTheDocument(); + expect(screen.getByText('anotherEventTemplate')).toBeInTheDocument(); + }); + + it('removes a recording after receiving a notification', () => { + render( + + + + ); + expect(screen.queryByText('anotherEventTemplate')).not.toBeInTheDocument(); + }); + + it('displays the column header fields', () => { + render( + + + + ); + expect(screen.getByText('name')).toBeInTheDocument(); + expect(screen.getByText('xml')).toBeInTheDocument(); + }); + + it('shows a popup when uploading', () => { + render( + + + + ); + expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); + + const buttons = screen.getAllByRole('button'); + const uploadButton = buttons[0]; + userEvent.click(uploadButton); + + expect(screen.getByLabelText('Create Custom Probe Template')); + + }); + + it('Tests that delete works correctly', () => { + render( + + + + ); + + userEvent.click(screen.getByLabelText('Actions')); + + expect(screen.getByText('Insert Probes...')); + expect(screen.getByText('Delete')); + + const deleteAction = screen.getByText('Delete'); + userEvent.click(deleteAction); + + expect(screen.getByLabelText('Event template delete warning')); + + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + expect(deleteRequestSpy).toBeCalledWith('someEventTemplate');; + }); + }); From 8a5a5b3251f50620b65a6b104cd399563d206c0e Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 13 Oct 2022 18:05:21 -0400 Subject: [PATCH 05/43] Fix ErrorView and AuthRetry --- src/app/Agent/AgentLiveProbes.tsx | 14 ++++++++++++-- src/app/Agent/AgentProbeTemplates.tsx | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index ca6c2d9d6..302b58f95 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -46,7 +46,7 @@ import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraD import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; -import { ErrorView } from '@app/ErrorView/ErrorView'; +import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { EventProbe } from '@app/Shared/Services/Api.service'; export const AgentLiveProbes = () => { @@ -158,8 +158,18 @@ export const AgentLiveProbes = () => { ); }, [addSubscription, context, context.notificationChannel, setTemplates]); + + const authRetry = React.useCallback(() => { + context.target.setAuthRetry(); + }, [context.target, context.target.setAuthRetry]); + + if (errorMessage != '') { - return () + return () } else if (isLoading) { return () } else { diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index e094cec05..e3aa4e3c5 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -46,7 +46,7 @@ import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraD import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; -import { ErrorView } from '@app/ErrorView/ErrorView'; +import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; export const AgentProbeTemplates = () => { @@ -145,7 +145,7 @@ export const AgentProbeTemplates = () => { addSubscription( context.api.deleteCustomProbeTemplate(rowData[0]) .pipe(first()) - .subscribe(() => {}) + .subscribe(() => {refreshTemplates();}) ); }; @@ -211,6 +211,7 @@ export const AgentProbeTemplates = () => { setUploadFile(undefined); setUploadFilename(''); setModalOpen(false); + refreshTemplates(); } }) ); @@ -244,8 +245,17 @@ export const AgentProbeTemplates = () => { setSortBy({ index, direction }); }; + const authRetry = React.useCallback(() => { + context.target.setAuthRetry(); + }, [context.target, context.target.setAuthRetry]); + + if (errorMessage != '') { - return () + return () } else if (isLoading) { return () } else { From fbfb31b207e9a8ab1bbed2aa5e7fcc49086ba20f Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Wed, 19 Oct 2022 19:32:24 -0400 Subject: [PATCH 06/43] Fix Agent Probe Templates page tests, fix probe templates table, add deletion warning --- src/app/Agent/AgentProbeTemplates.tsx | 55 +++- src/app/Shared/Services/Api.service.tsx | 8 +- src/test/Agent/Agent.test.tsx | 37 ++- .../Agent/__snapshots__/Agent.test.tsx.snap | 290 ++++++++++++++++++ 4 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 src/test/Agent/__snapshots__/Agent.test.tsx.snap diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index e3aa4e3c5..33c11d97c 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -48,6 +48,8 @@ import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; +import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; +import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; export const AgentProbeTemplates = () => { @@ -65,12 +67,15 @@ export const AgentProbeTemplates = () => { const [sortBy, setSortBy] = React.useState({} as ISortBy); const [isLoading, setIsLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); + const [rowDeleteData, setRowDeleteData] = React.useState({} as IRowData); + const [warningModalOpen, setWarningModalOpen] = React.useState(false); const addSubscription = useSubscriptions(); - const tableColumns = [ + const tableColumns = React.useMemo( + () => [ { title: 'name', transforms: [ sortable ] }, - { title: 'xml' , transforms: [ sortable ] } - ]; + { title: 'xml' , transforms: [ sortable ] }, + ], [sortable]); React.useEffect(() => { let filtered; @@ -78,7 +83,11 @@ export const AgentProbeTemplates = () => { filtered = templates; } else { const ft = filterText.trim().toLowerCase(); - filtered = templates.filter((t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft)); + filtered = templates.filter( + (t: ProbeTemplate) => + t.name.toLowerCase().includes(ft) || + t.xml.toLowerCase().includes(ft) + ); } const { index, direction } = sortBy; if (typeof index === 'number') { @@ -91,7 +100,7 @@ export const AgentProbeTemplates = () => { }, [filterText, templates, sortBy]); const handleTemplates = React.useCallback((templates) => { - templates = JSON.parse(templates); + console.log(templates); setTemplates(templates); setIsLoading(false); setErrorMessage(''); @@ -107,7 +116,7 @@ export const AgentProbeTemplates = () => { addSubscription( context.target.target() .pipe( - concatMap(target => context.api.getProbeTemplates()), + concatMap((target) => context.api.getProbeTemplates()), first(), ).subscribe(value => handleTemplates(value), err => handleError(err)) ); @@ -137,8 +146,9 @@ export const AgentProbeTemplates = () => { }, [context.target]); const displayTemplates = React.useMemo( - () => templates.map((t: ProbeTemplate) => ([ t.name , t.xml])), - [templates] + () => + filteredTemplates.map((t: ProbeTemplate) => ([ t.name , t.xml])), + [filteredTemplates] ); const handleDelete = (rowData) => { @@ -173,7 +183,7 @@ export const AgentProbeTemplates = () => { }, { title: 'Delete', - onClick: (event, rowId, rowData) => handleDelete(rowData) + onClick: (event, rowId, rowData) => handleDeleteButton(rowData) } ]); return actions; @@ -237,6 +247,27 @@ export const AgentProbeTemplates = () => { setModalOpen(false); }; + const handleDeleteButton = React.useCallback( + (rowData) => { + if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) { + setRowDeleteData(rowData); + setWarningModalOpen(true); + } else { + handleDelete(rowData); + } + }, + [context, context.settings, setWarningModalOpen, setRowDeleteData, handleDelete] + ); + + const handleWarningModalAccept = React.useCallback(() => { + handleDelete(rowDeleteData); + }, [handleDelete, rowDeleteData]); + + const handleWarningModalClose = React.useCallback(() => { + setWarningModalOpen(false); + }, [setWarningModalOpen]); + + const handleFileRejected = () => { setFileRejected(true); }; @@ -272,6 +303,12 @@ export const AgentProbeTemplates = () => { + { return this.sendRequest('v2', 'probes', { method: 'GET' }). pipe(concatMap(resp => resp.json()), - map(response => response.data.result), + map((response : ProbeTemplateResponse) => response.data.result), first()); } @@ -938,6 +938,12 @@ interface CredentialResponse extends ApiV2Response { }; } +interface ProbeTemplateResponse extends ApiV2Response { + data: { + result: ProbeTemplate[]; + } +} + interface CredentialsResponse extends ApiV2Response { data: { result: StoredCredential[]; diff --git a/src/test/Agent/Agent.test.tsx b/src/test/Agent/Agent.test.tsx index 6b24ee6b5..fd894cfb9 100644 --- a/src/test/Agent/Agent.test.tsx +++ b/src/test/Agent/Agent.test.tsx @@ -58,7 +58,14 @@ const mockCustomEventTemplate: ProbeTemplate = { xml: '' }; -const mockAnotherTemplate = {...mockCustomEventTemplate, name: 'anotherProbeTemplate'} +const mockAnotherTemplate: ProbeTemplate = { + name: 'anotherProbeTemplate', + xml: '' +}; + +const mockData : ProbeTemplate[] = [ + mockCustomEventTemplate, mockAnotherTemplate +]; const mockCreateTemplateNotification = { meta: { @@ -88,7 +95,6 @@ jest.mock('react-router-dom', () => ({ })); jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor') - .mockReturnValueOnce(true) // show deletion warning .mockReturnValue(false); // don't ask again jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); @@ -96,7 +102,14 @@ jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of( jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); -jest.spyOn(defaultServices.api, 'getProbeTemplates').mockReturnValue(of([mockCustomEventTemplate])); +jest.spyOn(defaultServices.api, 'getProbeTemplates') +.mockReturnValueOnce(of([mockCustomEventTemplate])) // Renders Correctly +.mockReturnValueOnce(of([mockCustomEventTemplate])) +.mockReturnValueOnce(of(mockData)) // Adds a probe template +.mockReturnValueOnce(of(mockData)) +.mockReturnValueOnce(of([])) // Removes a probe template +.mockReturnValueOnce(of([])) +.mockReturnValue(of([mockCustomEventTemplate])); // All other tests jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); @@ -107,13 +120,12 @@ jest .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockCreateTemplateNotification)) // adds a template after receiving a notification - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockDeleteTemplateNotification)) // removes a template after receiving a notification - .mockReturnValue(of()); // all other tests - describe('', () => { + .mockReturnValue(of()); // All Other tests + + + describe('', () => { it('renders correctly', async () => { let tree; await act(async () => { @@ -133,8 +145,7 @@ jest ); - expect(screen.getByText('someEventTemplate')).toBeInTheDocument(); - expect(screen.getByText('anotherEventTemplate')).toBeInTheDocument(); + expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); }); it('removes a recording after receiving a notification', () => { @@ -143,7 +154,7 @@ jest ); - expect(screen.queryByText('anotherEventTemplate')).not.toBeInTheDocument(); + expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); }); it('displays the column header fields', () => { @@ -187,11 +198,11 @@ jest const deleteAction = screen.getByText('Delete'); userEvent.click(deleteAction); - expect(screen.getByLabelText('Event template delete warning')); + //expect(screen.getByLabelText('Event template delete warning')); const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); expect(deleteRequestSpy).toHaveBeenCalledTimes(1); - expect(deleteRequestSpy).toBeCalledWith('someEventTemplate');; + expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate');; }); }); diff --git a/src/test/Agent/__snapshots__/Agent.test.tsx.snap b/src/test/Agent/__snapshots__/Agent.test.tsx.snap new file mode 100644 index 000000000..4a5266fc0 --- /dev/null +++ b/src/test/Agent/__snapshots__/Agent.test.tsx.snap @@ -0,0 +1,290 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +Array [ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ , +
+ + + + + + + + + + + + + + +
, +] +`; From cd26d775a8c68d3bb14afc4b3680187bbb4a1e07 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Wed, 19 Oct 2022 20:32:02 -0400 Subject: [PATCH 07/43] Fixing Agent Live Probes page to work with new API implementation --- src/app/Agent/AgentLiveProbes.tsx | 22 ++++++------ src/app/Shared/Services/Api.service.tsx | 36 +++++++++++++------ .../Services/NotificationChannel.service.tsx | 16 ++++----- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 302b58f95..e3f10d641 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -63,11 +63,10 @@ export const AgentLiveProbes = () => { const addSubscription = useSubscriptions(); const tableColumns = [ + { title: 'ID', transforms: [ sortable ] }, { title: 'Label', transforms: [ sortable ] }, - { title: 'Description' , transforms: [ sortable ] }, { title: 'Class' , transforms: [ sortable ] }, - { title: 'Stacktrace' , transforms: [ sortable ] }, - { title: 'Rethrow' , transforms: [ sortable ] }, + { title: 'Description' , transforms: [ sortable ] }, { title: 'Method' , transforms: [ sortable ] } ]; @@ -77,13 +76,12 @@ export const AgentLiveProbes = () => { filtered = templates; } else { const ft = filterText.trim().toLowerCase(); - filtered = templates.filter((t: EventProbe) => t.label.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) - || t.class.toLowerCase().includes(ft) || t.stacktrace.toLowerCase().includes(ft) || - t.rethrow.toLowerCase().includes(ft) || t.methodname.toLowerCase().includes(ft)); + filtered = templates.filter((t: EventProbe) => t.name.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) + || t.clazz.toLowerCase().includes(ft) || t.methodDescriptor.toLowerCase().includes(ft) || t.methodName.toLowerCase().includes(ft)); } const { index, direction } = sortBy; if (typeof index === 'number') { - const keys = ['Label', 'Description', 'Class', 'Stacktrace', 'Rethrow', 'Method']; + const keys = ['ID', 'Label', 'Description', 'Class', 'Method']; const key = keys[index]; const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); @@ -92,7 +90,7 @@ export const AgentLiveProbes = () => { }, [filterText, templates, sortBy]); const handleTemplates = React.useCallback((templates) => { - templates = JSON.parse(templates); + console.log(templates); setTemplates(templates); setErrorMessage(''); setIsLoading(false); @@ -139,7 +137,7 @@ export const AgentLiveProbes = () => { }, [context.target]); const displayTemplates = React.useMemo( - () => templates.map((t: EventProbe) => ([ t.label , t.description , t.class , t.rethrow , t.stacktrace , t.methodname ])), + () => templates.map((t: EventProbe) => ([t.id, t.name , t.clazz , t.description , t.methodName + t.methodDescriptor ])), [templates] ); @@ -153,8 +151,8 @@ export const AgentLiveProbes = () => { React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.TargetProbesGet) - .subscribe(v => setTemplates(old => old.concat(v.message.probes))) + context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied) + .subscribe(v => refreshTemplates()) ); }, [addSubscription, context, context.notificationChannel, setTemplates]); @@ -166,7 +164,7 @@ export const AgentLiveProbes = () => { if (errorMessage != '') { return () diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index c98400395..48206a390 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -576,10 +576,12 @@ export class ApiService { getActiveProbes(): Observable { return this.target.target().pipe(concatMap(target => this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { - method: 'GET', - }).pipe(concatMap(resp => resp.json()), - map(response => response.data.result), - first()))); + method: 'GET' + }).pipe( + concatMap(resp => resp.json()), + map((response : EventProbesResponse)=> response.data.result), + first() + ))); } graphql(query: string): Observable { @@ -941,7 +943,13 @@ interface CredentialResponse extends ApiV2Response { interface ProbeTemplateResponse extends ApiV2Response { data: { result: ProbeTemplate[]; - } + }; +} + +interface EventProbesResponse extends ApiV2Response { + data: { + result: EventProbe[]; + }; } interface CredentialsResponse extends ApiV2Response { @@ -1025,13 +1033,21 @@ export interface ProbeTemplate { } export interface EventProbe { - label: string; + id: string; + name: string; + clazz: string; description: string; - class: string; - rethrow: string; - stacktrace: string; - methodname: string; + path: string; + recordStackTrace: boolean; + useRethrow: boolean; + methodName: string; + methodDescriptor: string; + location: string; + returnValue: string; + parameters: string; + fields: string; } + export interface MatchedCredential { matchExpression: string; targets: Target[]; diff --git a/src/app/Shared/Services/NotificationChannel.service.tsx b/src/app/Shared/Services/NotificationChannel.service.tsx index 080690912..db5033bd7 100644 --- a/src/app/Shared/Services/NotificationChannel.service.tsx +++ b/src/app/Shared/Services/NotificationChannel.service.tsx @@ -61,7 +61,7 @@ export enum NotificationCategory { TemplateDeleted = 'TemplateDeleted', ProbeTemplateUploaded = 'ProbeTemplateUploaded', ProbeTemplateDeleted = 'ProbeTemplateDeleted', - TargetProbesGet = 'TargetProbesGet', + ProbeTemplateApplied = 'ProbeTemplateApplied', RuleCreated = 'RuleCreated', RuleUpdated = 'RuleUpdated', RuleDeleted = 'RuleDeleted', @@ -205,6 +205,13 @@ export const messageKeys = new Map([ body: evt => `${evt.message.template.name} was created` } as NotificationMessageMapper ], + [ + NotificationCategory.ProbeTemplateApplied, { + variant: AlertVariant.success, + title: 'Probe Template Applied', + body: evt => `${evt.message.probeTemplate} was inserted` + } as NotificationMessageMapper + ], [ NotificationCategory.TemplateDeleted, { variant: AlertVariant.success, @@ -266,13 +273,6 @@ export const messageKeys = new Map([ body: (evt) => `Credentials deleted for target: ${evt.message.target}`, } as NotificationMessageMapper, ], - [ - NotificationCategory.TargetProbesGet, { - variant: AlertVariant.success, - title: 'Target Probes Fetched', - body: evt => `Probes Fetched: ${evt.message.probes}` - } as NotificationMessageMapper - ], [ NotificationCategory.CredentialsStored, { variant: AlertVariant.success, From 85bcccc30f5bacb215d532e5477fabb6d6861f89 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 11:29:17 -0400 Subject: [PATCH 08/43] Backing out unnecessary package.json change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9c946ed2..e86926513 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.9.2", "webpack-merge": "^5.8.0", - "yarn": "^1.22.18" + "yarn": "^1.22.13" }, "dependencies": { "@patternfly/react-core": "^4.224.1", From 6d48d48d3e461c1802a61ab8696eaa46292263b3 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 11:37:59 -0400 Subject: [PATCH 09/43] Running prettier to fix formatting --- src/app/Agent/Agent.tsx | 51 +-- src/app/Agent/AgentLiveProbes.tsx | 194 ++++++---- src/app/Agent/AgentProbeTemplates.tsx | 337 ++++++++++-------- src/app/Events/EventTemplates.tsx | 3 +- src/app/Shared/Services/Api.service.tsx | 115 +++--- .../Services/NotificationChannel.service.tsx | 30 +- src/test/Agent/Agent.test.tsx | 180 +++++----- 7 files changed, 519 insertions(+), 391 deletions(-) diff --git a/src/app/Agent/Agent.tsx b/src/app/Agent/Agent.tsx index 689e8f108..0cb423edb 100644 --- a/src/app/Agent/Agent.tsx +++ b/src/app/Agent/Agent.tsx @@ -1,8 +1,8 @@ /* * Copyright The Cryostat Authors - * + * * The Universal Permissive License (UPL), Version 1.0 - * + * * Subject to the condition set forth below, permission is hereby granted to any * person obtaining a copy of this software, associated documentation and/or data * (collectively the "Software"), free of charge and under any and all copyright @@ -10,23 +10,23 @@ * licensable by each licensor hereunder covering either (i) the unmodified * Software as contributed to or provided by such licensor, or (ii) the Larger * Works (as defined below), to deal in both - * + * * (a) the Software, and * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if * one is included with the Software (each a "Larger Work" to which the Software * is contributed by such licensors), - * + * * without restriction, including without limitation the rights to copy, create * derivative works of, display, perform, and distribute the Software and make, * use, sell, offer for sale, import, export, have made, and have sold the * Software and the Larger Work(s), and to sublicense the foregoing rights on * either these or other terms. - * + * * This license is subject to the following condition: * The above copyright notice and either this complete permission notice or at * a minimum a reference to the UPL must be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -46,23 +46,24 @@ export const Agent = () => { const handleTabSelect = (evt, idx) => { setActiveTab(idx); - } + }; - return (<> - - - - - - - - - - - - - - - ); - -} \ No newline at end of file + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index e3f10d641..147049cb1 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -40,9 +40,33 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { ActionGroup, Button, FileUpload, Form, FormGroup, Modal, ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, TextInput } from '@patternfly/react-core'; +import { + ActionGroup, + Button, + FileUpload, + Form, + FormGroup, + Modal, + ModalVariant, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + TextInput, +} from '@patternfly/react-core'; import { PlusIcon } from '@patternfly/react-icons'; -import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraData, ISortBy, SortByDirection, sortable } from '@patternfly/react-table'; +import { + Table, + TableBody, + TableHeader, + TableVariant, + IAction, + IRowData, + IExtraData, + ISortBy, + SortByDirection, + sortable, +} from '@patternfly/react-table'; import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; @@ -50,7 +74,6 @@ import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView import { EventProbe } from '@app/Shared/Services/Api.service'; export const AgentLiveProbes = () => { - const context = React.useContext(ServiceContext); const history = useHistory(); @@ -63,11 +86,11 @@ export const AgentLiveProbes = () => { const addSubscription = useSubscriptions(); const tableColumns = [ - { title: 'ID', transforms: [ sortable ] }, - { title: 'Label', transforms: [ sortable ] }, - { title: 'Class' , transforms: [ sortable ] }, - { title: 'Description' , transforms: [ sortable ] }, - { title: 'Method' , transforms: [ sortable ] } + { title: 'ID', transforms: [sortable] }, + { title: 'Label', transforms: [sortable] }, + { title: 'Class', transforms: [sortable] }, + { title: 'Description', transforms: [sortable] }, + { title: 'Method', transforms: [sortable] }, ]; React.useEffect(() => { @@ -76,8 +99,14 @@ export const AgentLiveProbes = () => { filtered = templates; } else { const ft = filterText.trim().toLowerCase(); - filtered = templates.filter((t: EventProbe) => t.name.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) - || t.clazz.toLowerCase().includes(ft) || t.methodDescriptor.toLowerCase().includes(ft) || t.methodName.toLowerCase().includes(ft)); + filtered = templates.filter( + (t: EventProbe) => + t.name.toLowerCase().includes(ft) || + t.description.toLowerCase().includes(ft) || + t.clazz.toLowerCase().includes(ft) || + t.methodDescriptor.toLowerCase().includes(ft) || + t.methodName.toLowerCase().includes(ft) + ); } const { index, direction } = sortBy; if (typeof index === 'number') { @@ -89,27 +118,38 @@ export const AgentLiveProbes = () => { setFilteredTemplates([...filtered]); }, [filterText, templates, sortBy]); - const handleTemplates = React.useCallback((templates) => { - console.log(templates); - setTemplates(templates); - setErrorMessage(''); - setIsLoading(false); - }, [setTemplates, setIsLoading, setErrorMessage]); + const handleTemplates = React.useCallback( + (templates) => { + console.log(templates); + setTemplates(templates); + setErrorMessage(''); + setIsLoading(false); + }, + [setTemplates, setIsLoading, setErrorMessage] + ); - const handleError = React.useCallback((error) => { - setErrorMessage(error.message); - setIsLoading(false); - }, [setIsLoading, setErrorMessage]); + const handleError = React.useCallback( + (error) => { + setErrorMessage(error.message); + setIsLoading(false); + }, + [setIsLoading, setErrorMessage] + ); const refreshTemplates = React.useCallback(() => { setIsLoading(true); addSubscription( - context.target.target() - .pipe( - filter(target => target !== NO_TARGET), - first(), - concatMap(target => context.api.getActiveProbes()), - ).subscribe(value => handleTemplates(value), err => handleError(err)) + context.target + .target() + .pipe( + filter((target) => target !== NO_TARGET), + first(), + concatMap((target) => context.api.getActiveProbes()) + ) + .subscribe( + (value) => handleTemplates(value), + (err) => handleError(err) + ) ); }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); @@ -118,83 +158,97 @@ export const AgentLiveProbes = () => { context.target.target().subscribe(() => { setFilterText(''); refreshTemplates(); - })); + }) + ); }, [context, context.target, addSubscription, refreshTemplates]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { return; } - const id = window.setInterval(() => refreshTemplates(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + const id = window.setInterval( + () => refreshTemplates(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); return () => window.clearInterval(id); }, []); React.useEffect(() => { const sub = context.target.authFailure().subscribe(() => { - setErrorMessage("Auth failure"); + setErrorMessage('Auth failure'); }); return () => sub.unsubscribe(); }, [context.target]); const displayTemplates = React.useMemo( - () => templates.map((t: EventProbe) => ([t.id, t.name , t.clazz , t.description , t.methodName + t.methodDescriptor ])), + () => templates.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName + t.methodDescriptor]), [templates] ); const handleModalToggle = () => { addSubscription( - context.api.removeProbes() - .pipe(first()) - .subscribe(() => {refreshTemplates();}) - ); + context.api + .removeProbes() + .pipe(first()) + .subscribe(() => { + refreshTemplates(); + }) + ); }; React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied) - .subscribe(v => refreshTemplates()) + context.notificationChannel + .messages(NotificationCategory.ProbeTemplateApplied) + .subscribe((v) => refreshTemplates()) ); }, [addSubscription, context, context.notificationChannel, setTemplates]); - const authRetry = React.useCallback(() => { context.target.setAuthRetry(); }, [context.target, context.target.setAuthRetry]); - if (errorMessage != '') { - return () + return ( + + ); } else if (isLoading) { - return () + return ; } else { - return (<> - - - - - - - - - - - - - - - - - -
- ); + return ( + <> + + + + + + + + + + + + + + + + + +
+ + ); } - -} +}; diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 33c11d97c..72f228fdc 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -40,9 +40,33 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { ActionGroup, Button, FileUpload, Form, FormGroup, Modal, ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, TextInput } from '@patternfly/react-core'; +import { + ActionGroup, + Button, + FileUpload, + Form, + FormGroup, + Modal, + ModalVariant, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + TextInput, +} from '@patternfly/react-core'; import { PlusIcon } from '@patternfly/react-icons'; -import { Table, TableBody, TableHeader, TableVariant, IAction, IRowData, IExtraData, ISortBy, SortByDirection, sortable } from '@patternfly/react-table'; +import { + Table, + TableBody, + TableHeader, + TableVariant, + IAction, + IRowData, + IExtraData, + ISortBy, + SortByDirection, + sortable, +} from '@patternfly/react-table'; import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; @@ -52,7 +76,6 @@ import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; export const AgentProbeTemplates = () => { - const context = React.useContext(ServiceContext); const history = useHistory(); @@ -73,9 +96,11 @@ export const AgentProbeTemplates = () => { const tableColumns = React.useMemo( () => [ - { title: 'name', transforms: [ sortable ] }, - { title: 'xml' , transforms: [ sortable ] }, - ], [sortable]); + { title: 'name', transforms: [sortable] }, + { title: 'xml', transforms: [sortable] }, + ], + [sortable] + ); React.useEffect(() => { let filtered; @@ -84,10 +109,8 @@ export const AgentProbeTemplates = () => { } else { const ft = filterText.trim().toLowerCase(); filtered = templates.filter( - (t: ProbeTemplate) => - t.name.toLowerCase().includes(ft) || - t.xml.toLowerCase().includes(ft) - ); + (t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft) + ); } const { index, direction } = sortBy; if (typeof index === 'number') { @@ -99,26 +122,37 @@ export const AgentProbeTemplates = () => { setFilteredTemplates([...filtered]); }, [filterText, templates, sortBy]); - const handleTemplates = React.useCallback((templates) => { - console.log(templates); - setTemplates(templates); - setIsLoading(false); - setErrorMessage(''); - }, [setTemplates, setIsLoading, setErrorMessage]); + const handleTemplates = React.useCallback( + (templates) => { + console.log(templates); + setTemplates(templates); + setIsLoading(false); + setErrorMessage(''); + }, + [setTemplates, setIsLoading, setErrorMessage] + ); - const handleError = React.useCallback((error) => { - setIsLoading(false); - setErrorMessage(error.message); - }, [setIsLoading, setErrorMessage]); + const handleError = React.useCallback( + (error) => { + setIsLoading(false); + setErrorMessage(error.message); + }, + [setIsLoading, setErrorMessage] + ); const refreshTemplates = React.useCallback(() => { - setIsLoading(true) + setIsLoading(true); addSubscription( - context.target.target() - .pipe( - concatMap((target) => context.api.getProbeTemplates()), - first(), - ).subscribe(value => handleTemplates(value), err => handleError(err)) + context.target + .target() + .pipe( + concatMap((target) => context.api.getProbeTemplates()), + first() + ) + .subscribe( + (value) => handleTemplates(value), + (err) => handleError(err) + ) ); }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); @@ -127,44 +161,51 @@ export const AgentProbeTemplates = () => { context.target.target().subscribe(() => { setFilterText(''); refreshTemplates(); - })); + }) + ); }, [context, context.target, addSubscription, refreshTemplates]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { return; } - const id = window.setInterval(() => refreshTemplates(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()); + const id = window.setInterval( + () => refreshTemplates(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); return () => window.clearInterval(id); }, []); React.useEffect(() => { const sub = context.target.authFailure().subscribe(() => { - setErrorMessage("Auth failure"); + setErrorMessage('Auth failure'); }); return () => sub.unsubscribe(); }, [context.target]); const displayTemplates = React.useMemo( - () => - filteredTemplates.map((t: ProbeTemplate) => ([ t.name , t.xml])), + () => filteredTemplates.map((t: ProbeTemplate) => [t.name, t.xml]), [filteredTemplates] ); const handleDelete = (rowData) => { addSubscription( - context.api.deleteCustomProbeTemplate(rowData[0]) - .pipe(first()) - .subscribe(() => {refreshTemplates();}) + context.api + .deleteCustomProbeTemplate(rowData[0]) + .pipe(first()) + .subscribe(() => { + refreshTemplates(); + }) ); }; const handleInsert = (rowData) => { - addSubscription( - context.api.insertProbes(rowData[0]) - .pipe(first()) - .subscribe(() => {}) - ); + addSubscription( + context.api + .insertProbes(rowData[0]) + .pipe(first()) + .subscribe(() => {}) + ); }; const actionResolver = (rowData: IRowData, extraData: IExtraData) => { @@ -174,23 +215,23 @@ export const AgentProbeTemplates = () => { let actions = [ { title: 'Insert Probes...', - onClick: (event, rowId, rowData) => handleInsert(rowData) + onClick: (event, rowId, rowData) => handleInsert(rowData), }, ] as IAction[]; actions = actions.concat([ - { - isSeparator: true, - }, - { - title: 'Delete', - onClick: (event, rowId, rowData) => handleDeleteButton(rowData) - } + { + isSeparator: true, + }, + { + title: 'Delete', + onClick: (event, rowId, rowData) => handleDeleteButton(rowData), + }, ]); return actions; }; const handleModalToggle = () => { - setModalOpen(v => { + setModalOpen((v) => { if (v) { setUploadFile(undefined); setUploadFilename(''); @@ -213,32 +254,33 @@ export const AgentProbeTemplates = () => { } setUploading(true); addSubscription( - context.api.addCustomProbeTemplate(uploadFile) - .pipe(first()) - .subscribe(success => { - setUploading(false); - if (success) { - setUploadFile(undefined); - setUploadFilename(''); - setModalOpen(false); - refreshTemplates(); - } - }) + context.api + .addCustomProbeTemplate(uploadFile) + .pipe(first()) + .subscribe((success) => { + setUploading(false); + if (success) { + setUploadFile(undefined); + setUploadFilename(''); + setModalOpen(false); + refreshTemplates(); + } + }) ); }; React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.ProbeTemplateUploaded) - .subscribe(v => refreshTemplates()) + context.notificationChannel + .messages(NotificationCategory.ProbeTemplateUploaded) + .subscribe((v) => refreshTemplates()) ); }, [addSubscription, context, context.notificationChannel, setTemplates]); React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.TemplateDeleted) - .subscribe(v => refreshTemplates()) - ) + context.notificationChannel.messages(NotificationCategory.TemplateDeleted).subscribe((v) => refreshTemplates()) + ); }, [addSubscription, context, context.notificationChannel, setTemplates]); const handleUploadCancel = () => { @@ -267,7 +309,6 @@ export const AgentProbeTemplates = () => { setWarningModalOpen(false); }, [setWarningModalOpen]); - const handleFileRejected = () => { setFileRejected(true); }; @@ -280,84 +321,100 @@ export const AgentProbeTemplates = () => { context.target.setAuthRetry(); }, [context.target, context.target.setAuthRetry]); - if (errorMessage != '') { - return () + return ( + + ); } else if (isLoading) { - return () + return ; } else { - return (<> - - - - - - - - - - - - - - - - - - -
- - + + + + + + + + + + + + + + + + + + +
+ + -
- - + - - - - - - -
- ); + > + + + + + + + +
+ + ); } - -} +}; diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index 2dd60bd1e..0e4a1af4d 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -129,7 +129,8 @@ export const EventTemplates = () => { const handleTemplates = React.useCallback( (templates) => { - setTemplates(templates); + console.log(templates); + setTemplates([]); setIsLoading(false); setErrorMessage(''); }, diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 48206a390..dcd6f755b 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -477,72 +477,81 @@ export class ApiService { } removeProbes(): Observable { - return this.target.target().pipe(concatMap(target => - this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { - method: 'DELETE', - }).pipe( - tap(resp => { - if (resp.status == 200) { - this.notifications.success('Probes Removed'); - } else if (resp.status == 400) { - this.notifications.warning('Failed to remove Probes', 'The probes failed to be removed from the target'); - } - }), - map(resp => resp.status == 200), - first(), + return this.target.target().pipe( + concatMap((target) => + this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { + method: 'DELETE', + }).pipe( + tap((resp) => { + if (resp.status == 200) { + this.notifications.success('Probes Removed'); + } else if (resp.status == 400) { + this.notifications.warning('Failed to remove Probes', 'The probes failed to be removed from the target'); + } + }), + map((resp) => resp.status == 200), + first() + ) ) - )); + ); } insertProbes(templateName: string): Observable { - return this.target.target().pipe(concatMap(target => - this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes/${encodeURIComponent(templateName)}`, { - method: 'POST', - }).pipe( - tap(resp => { - if (resp.status == 200) { - this.notifications.success('Probes Inserted'); - } else if (resp.status == 400) { - this.notifications.warning('Failed to Insert Probes', 'The probes failed to be injected. Check that the agent is present in the same container as the target JVM and the target is running with -javaagent:/path/to/agent'); + return this.target.target().pipe( + concatMap((target) => + this.sendRequest( + 'v2', + `targets/${encodeURIComponent(target.connectUrl)}/probes/${encodeURIComponent(templateName)}`, + { + method: 'POST', } - }), - map(resp => resp.status == 200), - first(), + ).pipe( + tap((resp) => { + if (resp.status == 200) { + this.notifications.success('Probes Inserted'); + } else if (resp.status == 400) { + this.notifications.warning( + 'Failed to Insert Probes', + 'The probes failed to be injected. Check that the agent is present in the same container as the target JVM and the target is running with -javaagent:/path/to/agent' + ); + } + }), + map((resp) => resp.status == 200), + first() + ) ) - )); + ); } addCustomProbeTemplate(file: File): Observable { const body = new window.FormData(); body.append('probeTemplate', file); - return this.sendRequest('v2', `probes/`+file.name, { + return this.sendRequest('v2', `probes/` + file.name, { method: 'POST', body, - }) - .pipe( - map(response => { + }).pipe( + map((response) => { if (!response.ok) { throw response.statusText; } return true; }), - catchError((): ObservableInput => of(false)), + catchError((): ObservableInput => of(false)) ); - } + } deleteCustomProbeTemplate(templateName: string): Observable { return this.sendRequest('v2', `probes/${encodeURIComponent(templateName)}`, { method: 'DELETE', body: null, - }) - .pipe( - map(response => { + }).pipe( + map((response) => { if (!response.ok) { throw response.statusText; } return true; }), - catchError((): ObservableInput => of(false)), + catchError((): ObservableInput => of(false)) ); } @@ -567,22 +576,26 @@ export class ApiService { } getProbeTemplates(): Observable { - return this.sendRequest('v2', 'probes', { method: 'GET' }). - pipe(concatMap(resp => resp.json()), - map((response : ProbeTemplateResponse) => response.data.result), - first()); + return this.sendRequest('v2', 'probes', { method: 'GET' }).pipe( + concatMap((resp) => resp.json()), + map((response: ProbeTemplateResponse) => response.data.result), + first() + ); } getActiveProbes(): Observable { - return this.target.target().pipe(concatMap(target => - this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { - method: 'GET' - }).pipe( - concatMap(resp => resp.json()), - map((response : EventProbesResponse)=> response.data.result), - first() - ))); - } + return this.target.target().pipe( + concatMap((target) => + this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { + method: 'GET', + }).pipe( + concatMap((resp) => resp.json()), + map((response: EventProbesResponse) => response.data.result), + first() + ) + ) + ); + } graphql(query: string): Observable { const headers = new Headers(); @@ -1030,7 +1043,7 @@ export interface StoredCredential { export interface ProbeTemplate { name: string; xml: string; -} +} export interface EventProbe { id: string; diff --git a/src/app/Shared/Services/NotificationChannel.service.tsx b/src/app/Shared/Services/NotificationChannel.service.tsx index db5033bd7..98b0616ed 100644 --- a/src/app/Shared/Services/NotificationChannel.service.tsx +++ b/src/app/Shared/Services/NotificationChannel.service.tsx @@ -199,35 +199,40 @@ export const messageKeys = new Map([ } as NotificationMessageMapper, ], [ - NotificationCategory.ProbeTemplateUploaded, { + NotificationCategory.ProbeTemplateUploaded, + { variant: AlertVariant.success, title: 'Probe Template Created', - body: evt => `${evt.message.template.name} was created` - } as NotificationMessageMapper + body: (evt) => `${evt.message.template.name} was created`, + } as NotificationMessageMapper, ], [ - NotificationCategory.ProbeTemplateApplied, { + NotificationCategory.ProbeTemplateApplied, + { variant: AlertVariant.success, title: 'Probe Template Applied', - body: evt => `${evt.message.probeTemplate} was inserted` - } as NotificationMessageMapper + body: (evt) => `${evt.message.probeTemplate} was inserted`, + } as NotificationMessageMapper, ], [ - NotificationCategory.TemplateDeleted, { + NotificationCategory.TemplateDeleted, + { variant: AlertVariant.success, title: 'Template Deleted', body: (evt) => `${evt.message.template.name} was deleted`, } as NotificationMessageMapper, ], [ - NotificationCategory.ProbeTemplateDeleted, { + NotificationCategory.ProbeTemplateDeleted, + { variant: AlertVariant.success, title: 'Probe Template Deleted', - body: evt => `${evt.message.template.name} was deleted` - } as NotificationMessageMapper + body: (evt) => `${evt.message.template.name} was deleted`, + } as NotificationMessageMapper, ], [ - NotificationCategory.RuleCreated, { + NotificationCategory.RuleCreated, + { variant: AlertVariant.success, title: 'Automated Rule Created', body: (evt) => `${evt.message.name} was created`, @@ -274,7 +279,8 @@ export const messageKeys = new Map([ } as NotificationMessageMapper, ], [ - NotificationCategory.CredentialsStored, { + NotificationCategory.CredentialsStored, + { variant: AlertVariant.success, title: 'Credentials Stored', body: (evt) => `Credentials stored for: ${evt.message.matchExpression}`, diff --git a/src/test/Agent/Agent.test.tsx b/src/test/Agent/Agent.test.tsx index fd894cfb9..420f90218 100644 --- a/src/test/Agent/Agent.test.tsx +++ b/src/test/Agent/Agent.test.tsx @@ -51,37 +51,35 @@ import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; const mockConnectUrl = 'service:jmx:rmi://someUrl'; const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; -const mockMessageType = {type: "application", subtype: "json"} as MessageType; +const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; const mockCustomEventTemplate: ProbeTemplate = { - name: 'someProbeTemplate', - xml: '' + name: 'someProbeTemplate', + xml: '', }; const mockAnotherTemplate: ProbeTemplate = { name: 'anotherProbeTemplate', - xml: '' + xml: '', }; -const mockData : ProbeTemplate[] = [ - mockCustomEventTemplate, mockAnotherTemplate -]; +const mockData: ProbeTemplate[] = [mockCustomEventTemplate, mockAnotherTemplate]; -const mockCreateTemplateNotification = { +const mockCreateTemplateNotification = { meta: { category: 'ProbeTemplateUploaded', - type: mockMessageType + type: mockMessageType, } as MessageMeta, - message: { - template: mockAnotherTemplate - } + message: { + template: mockAnotherTemplate, + }, } as NotificationMessage; -const mockDeleteTemplateNotification = -{...mockCreateTemplateNotification, +const mockDeleteTemplateNotification = { + ...mockCreateTemplateNotification, meta: { - category: 'ProbeTemplateDeleted', - type: mockMessageType - } + category: 'ProbeTemplateDeleted', + type: mockMessageType, + }, }; const mockHistoryPush = jest.fn(); @@ -94,22 +92,22 @@ jest.mock('react-router-dom', () => ({ }), })); -jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor') - .mockReturnValue(false); // don't ask again +jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor').mockReturnValue(false); // don't ask again jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); -jest.spyOn(defaultServices.api, 'getProbeTemplates') -.mockReturnValueOnce(of([mockCustomEventTemplate])) // Renders Correctly -.mockReturnValueOnce(of([mockCustomEventTemplate])) -.mockReturnValueOnce(of(mockData)) // Adds a probe template -.mockReturnValueOnce(of(mockData)) -.mockReturnValueOnce(of([])) // Removes a probe template -.mockReturnValueOnce(of([])) -.mockReturnValue(of([mockCustomEventTemplate])); // All other tests +jest + .spyOn(defaultServices.api, 'getProbeTemplates') + .mockReturnValueOnce(of([mockCustomEventTemplate])) // Renders Correctly + .mockReturnValueOnce(of([mockCustomEventTemplate])) + .mockReturnValueOnce(of(mockData)) // Adds a probe template + .mockReturnValueOnce(of(mockData)) + .mockReturnValueOnce(of([])) // Removes a probe template + .mockReturnValueOnce(of([])) + .mockReturnValue(of([mockCustomEventTemplate])); // All other tests jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); @@ -118,91 +116,89 @@ jest .spyOn(defaultServices.notificationChannel, 'messages') .mockReturnValueOnce(of()) // renders correctly .mockReturnValueOnce(of()) - + .mockReturnValueOnce(of(mockCreateTemplateNotification)) // adds a template after receiving a notification .mockReturnValueOnce(of(mockDeleteTemplateNotification)) // removes a template after receiving a notification .mockReturnValue(of()); // All Other tests - - - describe('', () => { - it('renders correctly', async () => { - let tree; - await act(async () => { - tree = renderer.create( - - - - ); - }); - expect(tree.toJSON()).toMatchSnapshot(); - }); - it('adds a recording after receiving a notification', () => { - render( +describe('', () => { + it('renders correctly', async () => { + let tree; + await act(async () => { + tree = renderer.create( ); - - expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); }); + expect(tree.toJSON()).toMatchSnapshot(); + }); - it('removes a recording after receiving a notification', () => { - render( - - - - ); - expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); - }); + it('adds a recording after receiving a notification', () => { + render( + + + + ); - it('displays the column header fields', () => { - render( - - - - ); - expect(screen.getByText('name')).toBeInTheDocument(); - expect(screen.getByText('xml')).toBeInTheDocument(); - }); + expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); + }); - it('shows a popup when uploading', () => { - render( - - - - ); - expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); + it('removes a recording after receiving a notification', () => { + render( + + + + ); + expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); + }); - const buttons = screen.getAllByRole('button'); - const uploadButton = buttons[0]; - userEvent.click(uploadButton); + it('displays the column header fields', () => { + render( + + + + ); + expect(screen.getByText('name')).toBeInTheDocument(); + expect(screen.getByText('xml')).toBeInTheDocument(); + }); - expect(screen.getByLabelText('Create Custom Probe Template')); + it('shows a popup when uploading', () => { + render( + + + + ); + expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); - }); + const buttons = screen.getAllByRole('button'); + const uploadButton = buttons[0]; + userEvent.click(uploadButton); - it('Tests that delete works correctly', () => { - render( - - - - ); - - userEvent.click(screen.getByLabelText('Actions')); + expect(screen.getByLabelText('Create Custom Probe Template')); + }); - expect(screen.getByText('Insert Probes...')); - expect(screen.getByText('Delete')); + it('Tests that delete works correctly', () => { + render( + + + + ); - const deleteAction = screen.getByText('Delete'); - userEvent.click(deleteAction); + userEvent.click(screen.getByLabelText('Actions')); - //expect(screen.getByLabelText('Event template delete warning')); + expect(screen.getByText('Insert Probes...')); + expect(screen.getByText('Delete')); - const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); + const deleteAction = screen.getByText('Delete'); + userEvent.click(deleteAction); - expect(deleteRequestSpy).toHaveBeenCalledTimes(1); - expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate');; - }); + //expect(screen.getByLabelText('Event template delete warning')); + + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); }); +}); From 898d6ead4cd9a51f2ac13efde45a86863f977d8d Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 11:42:31 -0400 Subject: [PATCH 10/43] Removing extraneous change --- src/app/Events/EventTemplates.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index 0e4a1af4d..c318ab470 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -129,8 +129,6 @@ export const EventTemplates = () => { const handleTemplates = React.useCallback( (templates) => { - console.log(templates); - setTemplates([]); setIsLoading(false); setErrorMessage(''); }, From bc06912d7dc3335385b59c23ef2b56c6ff7b43ca Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 11:43:35 -0400 Subject: [PATCH 11/43] Removing unnecessary change --- src/app/Events/EventTemplates.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index c318ab470..2dd60bd1e 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -129,6 +129,7 @@ export const EventTemplates = () => { const handleTemplates = React.useCallback( (templates) => { + setTemplates(templates); setIsLoading(false); setErrorMessage(''); }, From a6ffd942c459721b7b196dbbad4eab282ac1f8da Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 14:12:16 -0400 Subject: [PATCH 12/43] Fixing deletion warning and notifications --- src/app/Agent/AgentProbeTemplates.tsx | 5 ++--- src/app/Modal/DeleteWarningUtils.tsx | 10 ++++++++++ .../Shared/Services/NotificationChannel.service.tsx | 2 +- src/app/routes.tsx | 8 +++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 72f228fdc..79bcc3fc1 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -124,7 +124,6 @@ export const AgentProbeTemplates = () => { const handleTemplates = React.useCallback( (templates) => { - console.log(templates); setTemplates(templates); setIsLoading(false); setErrorMessage(''); @@ -324,7 +323,7 @@ export const AgentProbeTemplates = () => { if (errorMessage != '') { return ( @@ -356,7 +355,7 @@ export const AgentProbeTemplates = () => { `${evt.message.template.name} was created`, + body: (evt) => `${evt.message.probeTemplate} was created`, } as NotificationMessageMapper, ], [ diff --git a/src/app/routes.tsx b/src/app/routes.tsx index acbef47dd..85fb46bd1 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -150,9 +150,11 @@ const routes: IAppRoute[] = [ { component: Agent, exact: true, - label: 'Agent', - path: '/agent', - title: 'Agent', + label: 'JMC Agent', + path: '/jmcagent', + title: 'JMC Agent', + description: + 'View available JMC Agent probe templates for target JVMs, as well as upload custom templates and insert probes.', navGroup: CONSOLE, }, { From 2db1ec634c4001a390ac433ba8bdce71ba038dad Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 14:57:16 -0400 Subject: [PATCH 13/43] Moving Agent items under Events page, adding hints to error message when probes fail to load --- src/app/Agent/Agent.tsx | 69 ----------------------------- src/app/Agent/AgentLiveProbes.tsx | 5 ++- src/app/Events/Events.tsx | 73 +++++++++++++++++++++++++------ src/app/routes.tsx | 11 ----- 4 files changed, 64 insertions(+), 94 deletions(-) delete mode 100644 src/app/Agent/Agent.tsx diff --git a/src/app/Agent/Agent.tsx b/src/app/Agent/Agent.tsx deleted file mode 100644 index 0cb423edb..000000000 --- a/src/app/Agent/Agent.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright The Cryostat Authors - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or data - * (collectively the "Software"), free of charge and under any and all copyright - * rights in the Software, and any and all patent rights owned or freely - * licensable by each licensor hereunder covering either (i) the unmodified - * Software as contributed to or provided by such licensor, or (ii) the Larger - * Works (as defined below), to deal in both - * - * (a) the Software, and - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software (each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * The above copyright notice and either this complete permission notice or at - * a minimum a reference to the UPL must be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import * as React from 'react'; -import { TargetView } from '@app/TargetView/TargetView'; -import { Card, CardBody, Tab, Tabs } from '@patternfly/react-core'; -import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; -import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; - -export const Agent = () => { - const [activeTab, setActiveTab] = React.useState(0); - - const handleTabSelect = (evt, idx) => { - setActiveTab(idx); - }; - - return ( - <> - - - - - - - - - - - - - - - - ); -}; diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 147049cb1..aff1e291e 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -212,7 +212,10 @@ export const AgentLiveProbes = () => { return ( ); diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 07146ba7c..e12989cb8 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -37,8 +37,21 @@ */ import * as React from 'react'; import { TargetView } from '@app/TargetView/TargetView'; -import { Card, CardBody, Tab, Tabs } from '@patternfly/react-core'; +import { + Card, + CardBody, + CardHeaderMain, + CardHeader, + Stack, + StackItem, + Tab, + Tabs, + Text, + TextVariants, +} from '@patternfly/react-core'; import { EventTemplates } from './EventTemplates'; +import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; +import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; import { EventTypes } from './EventTypes'; export const Events = () => { @@ -51,18 +64,52 @@ export const Events = () => { 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/remove them from targets, as well as view + currently active probes. + + + + + + + + + + + + + + + + + + ); diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 85fb46bd1..4ca2ac69e 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -39,7 +39,6 @@ import * as React from 'react'; import { CreateRecording } from '@app/CreateRecording/CreateRecording'; import { Dashboard } from '@app/Dashboard/Dashboard'; import { Events } from '@app/Events/Events'; -import { Agent } from '@app/Agent/Agent'; import { Login } from '@app/Login/Login'; import { NotFound } from '@app/NotFound/NotFound'; import { Recordings } from '@app/Recordings/Recordings'; @@ -147,16 +146,6 @@ const routes: IAppRoute[] = [ description: 'View available JFR event templates and types for target JVMs, as well as upload custom templates.', navGroup: CONSOLE, }, - { - component: Agent, - exact: true, - label: 'JMC Agent', - path: '/jmcagent', - title: 'JMC Agent', - description: - 'View available JMC Agent probe templates for target JVMs, as well as upload custom templates and insert probes.', - navGroup: CONSOLE, - }, { component: SecurityPanel, exact: true, From beb17ec33f9fe312e98ed37308afd396cd9c07e7 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Thu, 20 Oct 2022 15:43:19 -0400 Subject: [PATCH 14/43] Moving Agent help into agent components, adding notification for probe template deletion --- src/app/Agent/AgentLiveProbes.tsx | 20 +++++++++++++ src/app/Agent/AgentProbeTemplates.tsx | 20 +++++++++++++ src/app/Events/Events.tsx | 16 ---------- .../Services/NotificationChannel.service.tsx | 2 +- .../Agent/__snapshots__/Agent.test.tsx.snap | 30 +++++++++++++++++++ 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index aff1e291e..69d5ad668 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -43,6 +43,10 @@ import { useSubscriptions } from '@app/utils/useSubscriptions'; import { ActionGroup, Button, + Card, + CardBody, + CardHeaderMain, + CardHeader, FileUpload, Form, FormGroup, @@ -53,6 +57,8 @@ import { ToolbarGroup, ToolbarItem, TextInput, + Text, + TextVariants, } from '@patternfly/react-core'; import { PlusIcon } from '@patternfly/react-icons'; import { @@ -224,6 +230,20 @@ export const AgentLiveProbes = () => { } else { 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/remove them from targets, as well as view currently active + probes. + + diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 79bcc3fc1..95c8a5848 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -43,6 +43,10 @@ import { useSubscriptions } from '@app/utils/useSubscriptions'; import { ActionGroup, Button, + Card, + CardBody, + CardHeaderMain, + CardHeader, FileUpload, Form, FormGroup, @@ -53,6 +57,8 @@ import { ToolbarGroup, ToolbarItem, TextInput, + Text, + TextVariants, } from '@patternfly/react-core'; import { PlusIcon } from '@patternfly/react-icons'; import { @@ -333,6 +339,20 @@ export const AgentProbeTemplates = () => { } else { 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/remove them from targets, as well as view currently active + probes. + + diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index e12989cb8..cdee1ee65 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -79,22 +79,6 @@ export const Events = () => { - - - - - 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/remove them from targets, as well as view - currently active probes. - - - diff --git a/src/app/Shared/Services/NotificationChannel.service.tsx b/src/app/Shared/Services/NotificationChannel.service.tsx index d30cc8f6d..c2d2ae5fa 100644 --- a/src/app/Shared/Services/NotificationChannel.service.tsx +++ b/src/app/Shared/Services/NotificationChannel.service.tsx @@ -227,7 +227,7 @@ export const messageKeys = new Map([ { variant: AlertVariant.success, title: 'Probe Template Deleted', - body: (evt) => `${evt.message.template.name} was deleted`, + body: (evt) => `${evt.message.probeTemplate} was deleted`, } as NotificationMessageMapper, ], [ diff --git a/src/test/Agent/__snapshots__/Agent.test.tsx.snap b/src/test/Agent/__snapshots__/Agent.test.tsx.snap index 4a5266fc0..5b51e9328 100644 --- a/src/test/Agent/__snapshots__/Agent.test.tsx.snap +++ b/src/test/Agent/__snapshots__/Agent.test.tsx.snap @@ -2,6 +2,36 @@ exports[` renders correctly 1`] = ` Array [ +
+
+
+

+ 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/remove them from targets, as well as view currently active probes. +
+
,
Date: Thu, 20 Oct 2022 16:41:46 -0400 Subject: [PATCH 15/43] Remove duplicate notification --- src/app/Shared/Services/Api.service.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index dcd6f755b..14f575adb 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -483,9 +483,7 @@ export class ApiService { method: 'DELETE', }).pipe( tap((resp) => { - if (resp.status == 200) { - this.notifications.success('Probes Removed'); - } else if (resp.status == 400) { + if (resp.status == 400) { this.notifications.warning('Failed to remove Probes', 'The probes failed to be removed from the target'); } }), @@ -507,9 +505,7 @@ export class ApiService { } ).pipe( tap((resp) => { - if (resp.status == 200) { - this.notifications.success('Probes Inserted'); - } else if (resp.status == 400) { + if (resp.status == 400) { this.notifications.warning( 'Failed to Insert Probes', 'The probes failed to be injected. Check that the agent is present in the same container as the target JVM and the target is running with -javaagent:/path/to/agent' From ea970a650a982e6ec2b43497dc9da6ea582409f7 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Fri, 21 Oct 2022 11:42:09 -0400 Subject: [PATCH 16/43] Add error hint for Probetemplate component --- src/app/Agent/AgentProbeTemplates.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 95c8a5848..be7405150 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -330,7 +330,10 @@ export const AgentProbeTemplates = () => { return ( ); From 6e3c9fd6370568c7da6fc58de6100d63c2e26cbb Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 21 Oct 2022 15:45:24 -0400 Subject: [PATCH 17/43] clean up, formatting --- src/app/Agent/AgentLiveProbes.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 69d5ad668..d29215884 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -41,17 +41,11 @@ import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.s import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { - ActionGroup, Button, Card, CardBody, CardHeaderMain, CardHeader, - FileUpload, - Form, - FormGroup, - Modal, - ModalVariant, Toolbar, ToolbarContent, ToolbarGroup, @@ -60,15 +54,11 @@ import { Text, TextVariants, } from '@patternfly/react-core'; -import { PlusIcon } from '@patternfly/react-icons'; import { Table, TableBody, TableHeader, TableVariant, - IAction, - IRowData, - IExtraData, ISortBy, SortByDirection, sortable, @@ -76,7 +66,7 @@ import { import { useHistory } from 'react-router-dom'; import { concatMap, filter, first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; -import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; +import { ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { EventProbe } from '@app/Shared/Services/Api.service'; export const AgentLiveProbes = () => { @@ -126,7 +116,6 @@ export const AgentLiveProbes = () => { const handleTemplates = React.useCallback( (templates) => { - console.log(templates); setTemplates(templates); setErrorMessage(''); setIsLoading(false); From ad908781e815847b7883dd2e0007827c87e6d6bb Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 21 Oct 2022 15:45:47 -0400 Subject: [PATCH 18/43] add parameter to suppress graphical notification on error --- src/app/Shared/Services/Api.service.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 14f575adb..f2ec30a74 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -857,7 +857,7 @@ export class ApiService { ); } - private sendRequest(apiVersion: ApiVersion, path: string, config?: RequestInit): Observable { + private sendRequest(apiVersion: ApiVersion, path: string, config?: RequestInit, suppressNotifications = false): Observable { const req = () => this.login.getHeaders().pipe( concatMap((headers) => { @@ -882,7 +882,7 @@ export class ApiService { if (resp.ok) return resp; throw new HttpError(resp); }), - catchError((err) => this.handleError(err, req)) + catchError((err) => this.handleError(err, req, suppressNotifications)) ); return req(); } @@ -899,7 +899,7 @@ export class ApiService { anchor.remove(); } - private handleError(error: Error, retry: () => Observable): ObservableInput { + private handleError(error: Error, retry: () => Observable, suppressNotifications = false): ObservableInput { if (isHttpError(error)) { if (error.httpResponse.status === 427) { const jmxAuthScheme = error.httpResponse.headers.get('X-JMX-Authenticate'); @@ -911,12 +911,16 @@ export class ApiService { this.target.setSslFailure(); } else { error.httpResponse.text().then((detail) => { - this.notifications.danger(`Request failed (${error.httpResponse.status} ${error.message})`, detail); + if (!suppressNotifications) { + this.notifications.danger(`Request failed (${error.httpResponse.status} ${error.message})`, detail); + } }); } throw error; } - this.notifications.danger(`Request failed`, error.message); + if (!suppressNotifications) { + this.notifications.danger(`Request failed`, error.message); + } throw error; } From 52054af5eaebaf5f47f28ff99cdcc5652ee22287 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 21 Oct 2022 15:46:04 -0400 Subject: [PATCH 19/43] test for and only display JMC Agent card if support is detected --- src/app/Events/Events.tsx | 74 +++++++++++++++---------- src/app/Shared/Services/Api.service.tsx | 4 +- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index cdee1ee65..bafdccaf5 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -36,30 +36,44 @@ * SOFTWARE. */ import * as React from 'react'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; import { TargetView } from '@app/TargetView/TargetView'; -import { - Card, - CardBody, - CardHeaderMain, - CardHeader, - Stack, - StackItem, - Tab, - Tabs, - Text, - TextVariants, -} from '@patternfly/react-core'; +import { Card, CardBody, Stack, StackItem, Tab, Tabs } from '@patternfly/react-core'; import { EventTemplates } from './EventTemplates'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; import { EventTypes } from './EventTypes'; +import { concatMap, filter } from 'rxjs/operators'; +import { NO_TARGET } from '@app/Shared/Services/Target.service'; export const Events = () => { + const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); const [activeTab, setActiveTab] = React.useState(0); + const [enabled, setEnabled] = React.useState(false); - const handleTabSelect = (evt, idx) => { - setActiveTab(idx); - }; + React.useEffect(() => { + addSubscription( + context.target + .target() + .pipe( + filter((t) => t !== NO_TARGET), + concatMap((_) => context.api.getActiveProbes(true)) + ) + .subscribe({ + next: (_) => setEnabled(true), + error: (_) => setEnabled(false), + }) + ); + }, [context.api, context.target, setEnabled]); + + const handleTabSelect = React.useCallback( + (evt, idx) => { + setActiveTab(idx); + }, + [setActiveTab] + ); return ( <> @@ -79,20 +93,22 @@ export const Events = () => { - - - - - - - - - - - - - - + {enabled ? ( + + + + + + + + + + + + + + + ) : null} diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index f2ec30a74..c8236ba32 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -579,12 +579,12 @@ export class ApiService { ); } - getActiveProbes(): Observable { + getActiveProbes(suppressNotifications = false): Observable { return this.target.target().pipe( concatMap((target) => this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { method: 'GET', - }).pipe( + }, suppressNotifications).pipe( concatMap((resp) => resp.json()), map((response: EventProbesResponse) => response.data.result), first() From 8f083ff83fa0196fe66f4be40dd9d9e413d6786d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 21 Oct 2022 15:47:19 -0400 Subject: [PATCH 20/43] fixup! test for and only display JMC Agent card if support is detected --- src/app/Events/Events.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index bafdccaf5..a11891868 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -66,7 +66,7 @@ export const Events = () => { error: (_) => setEnabled(false), }) ); - }, [context.api, context.target, setEnabled]); + }, [addSubscription, context.api, context.target, setEnabled]); const handleTabSelect = React.useCallback( (evt, idx) => { From a74db433295f7edb2c310486b484e03e41954c0a Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 21 Oct 2022 15:50:59 -0400 Subject: [PATCH 21/43] apply prettier --- src/app/Shared/Services/Api.service.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index c8236ba32..be4fb89c3 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -582,9 +582,14 @@ export class ApiService { getActiveProbes(suppressNotifications = false): Observable { return this.target.target().pipe( concatMap((target) => - this.sendRequest('v2', `targets/${encodeURIComponent(target.connectUrl)}/probes`, { - method: 'GET', - }, suppressNotifications).pipe( + this.sendRequest( + 'v2', + `targets/${encodeURIComponent(target.connectUrl)}/probes`, + { + method: 'GET', + }, + suppressNotifications + ).pipe( concatMap((resp) => resp.json()), map((response: EventProbesResponse) => response.data.result), first() @@ -857,7 +862,12 @@ export class ApiService { ); } - private sendRequest(apiVersion: ApiVersion, path: string, config?: RequestInit, suppressNotifications = false): Observable { + private sendRequest( + apiVersion: ApiVersion, + path: string, + config?: RequestInit, + suppressNotifications = false + ): Observable { const req = () => this.login.getHeaders().pipe( concatMap((headers) => { From d9916169cbda7123cef88828f82a6ec7663d529f Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 21 Oct 2022 19:32:15 -0400 Subject: [PATCH 22/43] chore(events): clean up api calls and use separate state for 2 tables --- src/app/Events/Events.tsx | 46 ++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index a11891868..974b0124a 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -44,36 +44,28 @@ import { EventTemplates } from './EventTemplates'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; import { EventTypes } from './EventTypes'; -import { concatMap, filter } from 'rxjs/operators'; -import { NO_TARGET } from '@app/Shared/Services/Target.service'; -export const Events = () => { +export interface EventsProps {} + +export const Events: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); - const [activeTab, setActiveTab] = React.useState(0); + const [eventActiveTab, setEventActiveTab] = React.useState(0); + const [probeActiveTab, setProbeActiveTab] = React.useState(0); const [enabled, setEnabled] = React.useState(false); React.useEffect(() => { addSubscription( - context.target - .target() - .pipe( - filter((t) => t !== NO_TARGET), - concatMap((_) => context.api.getActiveProbes(true)) - ) - .subscribe({ - next: (_) => setEnabled(true), - error: (_) => setEnabled(false), - }) + context.api.getActiveProbes(true).subscribe({ + next: (_) => setEnabled(true), + error: (_) => setEnabled(false), + }) ); - }, [addSubscription, context.api, context.target, setEnabled]); + }, [addSubscription, context.api, setEnabled]); - const handleTabSelect = React.useCallback( - (evt, idx) => { - setActiveTab(idx); - }, - [setActiveTab] - ); + const handleEventTabSelect = React.useCallback((evt, idx) => setEventActiveTab(idx), [setEventActiveTab]); + + const handleProbeTabSelect = React.useCallback((evt, idx) => setProbeActiveTab(idx), [setProbeActiveTab]); return ( <> @@ -82,7 +74,7 @@ export const Events = () => { - + @@ -93,22 +85,22 @@ export const Events = () => { - {enabled ? ( + {enabled && ( - - + + - + - ) : null} + )} From 8f8e1e97febaf445a7a14f63ed3430bc12b1c318 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 21 Oct 2022 20:10:09 -0400 Subject: [PATCH 23/43] chore(live-probes): clean up live probe views --- src/app/Agent/AgentLiveProbes.tsx | 235 ++++++++++++++++-------------- 1 file changed, 122 insertions(+), 113 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index d29215884..8b54e51ad 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -38,7 +38,6 @@ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; -import { NO_TARGET } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { Button, @@ -53,6 +52,8 @@ import { TextInput, Text, TextVariants, + Stack, + StackItem, } from '@patternfly/react-core'; import { Table, @@ -63,15 +64,13 @@ import { SortByDirection, sortable, } from '@patternfly/react-table'; -import { useHistory } from 'react-router-dom'; -import { concatMap, filter, first } from 'rxjs/operators'; +import { first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; -import { ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; +import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { EventProbe } from '@app/Shared/Services/Api.service'; export const AgentLiveProbes = () => { const context = React.useContext(ServiceContext); - const history = useHistory(); const [templates, setTemplates] = React.useState([] as EventProbe[]); const [filteredTemplates, setFilteredTemplates] = React.useState([] as EventProbe[]); @@ -83,12 +82,61 @@ export const AgentLiveProbes = () => { const tableColumns = [ { title: 'ID', transforms: [sortable] }, - { title: 'Label', transforms: [sortable] }, + { title: 'Name', transforms: [sortable] }, { title: 'Class', transforms: [sortable] }, - { title: 'Description', transforms: [sortable] }, + { title: 'Description' }, { title: 'Method', transforms: [sortable] }, ]; + const handleTemplates = React.useCallback( + (templates) => { + setTemplates(templates); + setErrorMessage(''); + setIsLoading(false); + }, + [setTemplates, setIsLoading, setErrorMessage] + ); + + const handleError = React.useCallback( + (error) => { + setErrorMessage(error.message); + setIsLoading(false); + }, + [setIsLoading, setErrorMessage] + ); + + const refreshTemplates = React.useCallback(() => { + setIsLoading(true); + addSubscription( + context.api.getActiveProbes().subscribe({ + next: (value) => handleTemplates(value), + error: (err) => handleError(err), + }) + ); + }, [addSubscription, context.api, setIsLoading, handleTemplates, handleError]); + + const handleDeleteAllProbes = React.useCallback(() => { + addSubscription( + context.api + .removeProbes() + .pipe(first()) + .subscribe(() => { + refreshTemplates(); + }) + ); + }, [addSubscription, context.api, refreshTemplates]); + + const handleSort = React.useCallback( + (evt, index, direction) => { + setSortBy({ index, direction }); + }, + [setSortBy] + ); + + const authRetry = React.useCallback(() => { + context.target.setAuthRetry(); + }, [context.target, context.target.setAuthRetry]); + React.useEffect(() => { let filtered; if (!filterText) { @@ -106,47 +154,13 @@ export const AgentLiveProbes = () => { } const { index, direction } = sortBy; if (typeof index === 'number') { - const keys = ['ID', 'Label', 'Description', 'Class', 'Method']; + const keys = ['id', 'name', 'description', 'clazz', 'methodName']; const key = keys[index]; const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); } setFilteredTemplates([...filtered]); - }, [filterText, templates, sortBy]); - - const handleTemplates = React.useCallback( - (templates) => { - setTemplates(templates); - setErrorMessage(''); - setIsLoading(false); - }, - [setTemplates, setIsLoading, setErrorMessage] - ); - - const handleError = React.useCallback( - (error) => { - setErrorMessage(error.message); - setIsLoading(false); - }, - [setIsLoading, setErrorMessage] - ); - - const refreshTemplates = React.useCallback(() => { - setIsLoading(true); - addSubscription( - context.target - .target() - .pipe( - filter((target) => target !== NO_TARGET), - first(), - concatMap((target) => context.api.getActiveProbes()) - ) - .subscribe( - (value) => handleTemplates(value), - (err) => handleError(err) - ) - ); - }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); + }, [filterText, templates, sortBy, setFilteredTemplates]); React.useEffect(() => { addSubscription( @@ -155,7 +169,7 @@ export const AgentLiveProbes = () => { refreshTemplates(); }) ); - }, [context, context.target, addSubscription, refreshTemplates]); + }, [context, context.target, addSubscription, setFilterText, refreshTemplates]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { @@ -166,30 +180,15 @@ export const AgentLiveProbes = () => { context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() ); return () => window.clearInterval(id); - }, []); + }, [context.settings, refreshTemplates]); React.useEffect(() => { - const sub = context.target.authFailure().subscribe(() => { - setErrorMessage('Auth failure'); - }); - return () => sub.unsubscribe(); - }, [context.target]); - - const displayTemplates = React.useMemo( - () => templates.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName + t.methodDescriptor]), - [templates] - ); - - const handleModalToggle = () => { addSubscription( - context.api - .removeProbes() - .pipe(first()) - .subscribe(() => { - refreshTemplates(); - }) + context.target.authFailure().subscribe(() => { + setErrorMessage(authFailMessage); + }) ); - }; + }, [addSubscription, context.target, setErrorMessage]); React.useEffect(() => { addSubscription( @@ -199,18 +198,16 @@ export const AgentLiveProbes = () => { ); }, [addSubscription, context, context.notificationChannel, setTemplates]); - const authRetry = React.useCallback(() => { - context.target.setAuthRetry(); - }, [context.target, context.target.setAuthRetry]); + const displayTemplates = React.useMemo( + () => filteredTemplates.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName]), + [filteredTemplates] + ); if (errorMessage != '') { return ( ); @@ -219,47 +216,59 @@ export const AgentLiveProbes = () => { } else { 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/remove them from targets, as well as view currently active - probes. - - - - - - - - - - - - - - - - - - - -
+ + + + + + 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. + + + + + + + + + + + + + + + + + + + + + +
+
+
); } From 582eb6ba38df389a03df8fbdb49c89dd1eda0451 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 21 Oct 2022 20:28:01 -0400 Subject: [PATCH 24/43] feat(agent): add delete modal for active probe view --- src/app/Agent/AgentLiveProbes.tsx | 49 ++++++++++++++++++------- src/app/Modal/DeleteWarningUtils.tsx | 12 +++++- src/app/Shared/Services/Api.service.tsx | 2 +- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 8b54e51ad..4e2ee917f 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -68,9 +68,12 @@ import { first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; import { EventProbe } from '@app/Shared/Services/Api.service'; +import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; +import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; export const AgentLiveProbes = () => { const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); const [templates, setTemplates] = React.useState([] as EventProbe[]); const [filteredTemplates, setFilteredTemplates] = React.useState([] as EventProbe[]); @@ -78,8 +81,8 @@ export const AgentLiveProbes = () => { const [sortBy, setSortBy] = React.useState({} as ISortBy); const [isLoading, setIsLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); - const addSubscription = useSubscriptions(); - + const [warningModalOpen, setWarningModalOpen] = React.useState(false); + const tableColumns = [ { title: 'ID', transforms: [sortable] }, { title: 'Name', transforms: [sortable] }, @@ -115,6 +118,17 @@ export const AgentLiveProbes = () => { ); }, [addSubscription, context.api, setIsLoading, handleTemplates, handleError]); + const handleSort = React.useCallback( + (evt, index, direction) => { + setSortBy({ index, direction }); + }, + [setSortBy] + ); + + const authRetry = React.useCallback(() => { + context.target.setAuthRetry(); + }, [context.target, context.target.setAuthRetry]); + const handleDeleteAllProbes = React.useCallback(() => { addSubscription( context.api @@ -126,16 +140,19 @@ export const AgentLiveProbes = () => { ); }, [addSubscription, context.api, refreshTemplates]); - const handleSort = React.useCallback( - (evt, index, direction) => { - setSortBy({ index, direction }); - }, - [setSortBy] - ); + const handleWarningModalAccept = React.useCallback(() => handleDeleteAllProbes(), [handleDeleteAllProbes]); - const authRetry = React.useCallback(() => { - context.target.setAuthRetry(); - }, [context.target, context.target.setAuthRetry]); + const handleWarningModalClose = React.useCallback(() => { + setWarningModalOpen(false); + }, [setWarningModalOpen]); + + const handleDeleteButton = React.useCallback(() => { + if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteActiveProbes)) { + setWarningModalOpen(true); + } else { + handleDeleteAllProbes(); + } + }, [context.settings, setWarningModalOpen, handleDeleteAllProbes]); React.useEffect(() => { let filtered; @@ -250,12 +267,18 @@ export const AgentLiveProbes = () => { - + { if (resp.status == 400) { this.notifications.warning( - 'Failed to Insert Probes', + 'Failed to insert Probes', 'The probes failed to be injected. Check that the agent is present in the same container as the target JVM and the target is running with -javaagent:/path/to/agent' ); } From 2048604d85f261965892c2abc685457a99dc5ed7 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 21 Oct 2022 21:49:46 -0400 Subject: [PATCH 25/43] chore(agent): clean up probe template view --- src/app/Agent/AgentLiveProbes.tsx | 66 ++-- src/app/Agent/AgentProbeTemplates.tsx | 479 +++++++++++++------------- 2 files changed, 271 insertions(+), 274 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 4e2ee917f..360e3cbf8 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -82,7 +82,7 @@ export const AgentLiveProbes = () => { const [isLoading, setIsLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); const [warningModalOpen, setWarningModalOpen] = React.useState(false); - + const tableColumns = [ { title: 'ID', transforms: [sortable] }, { title: 'Name', transforms: [sortable] }, @@ -154,31 +154,6 @@ export const AgentLiveProbes = () => { } }, [context.settings, setWarningModalOpen, handleDeleteAllProbes]); - React.useEffect(() => { - let filtered; - if (!filterText) { - filtered = templates; - } else { - const ft = filterText.trim().toLowerCase(); - filtered = templates.filter( - (t: EventProbe) => - t.name.toLowerCase().includes(ft) || - t.description.toLowerCase().includes(ft) || - t.clazz.toLowerCase().includes(ft) || - t.methodDescriptor.toLowerCase().includes(ft) || - t.methodName.toLowerCase().includes(ft) - ); - } - const { index, direction } = sortBy; - if (typeof index === 'number') { - const keys = ['id', 'name', 'description', 'clazz', 'methodName']; - const key = keys[index]; - const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); - filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); - } - setFilteredTemplates([...filtered]); - }, [filterText, templates, sortBy, setFilteredTemplates]); - React.useEffect(() => { addSubscription( context.target.target().subscribe(() => { @@ -215,6 +190,31 @@ export const AgentLiveProbes = () => { ); }, [addSubscription, context, context.notificationChannel, setTemplates]); + React.useEffect(() => { + let filtered: EventProbe[]; + if (!filterText) { + filtered = templates; + } else { + const ft = filterText.trim().toLowerCase(); + filtered = templates.filter( + (t: EventProbe) => + t.name.toLowerCase().includes(ft) || + t.description.toLowerCase().includes(ft) || + t.clazz.toLowerCase().includes(ft) || + t.methodDescriptor.toLowerCase().includes(ft) || + t.methodName.toLowerCase().includes(ft) + ); + } + const { index, direction } = sortBy; + if (typeof index === 'number') { + const keys = ['id', 'name', 'description', 'clazz', 'methodName']; + const key = keys[index]; + const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); + filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); + } + setFilteredTemplates([...filtered]); + }, [filterText, templates, sortBy, setFilteredTemplates]); + const displayTemplates = React.useMemo( () => filteredTemplates.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName]), [filteredTemplates] @@ -233,7 +233,7 @@ export const AgentLiveProbes = () => { } else { return ( <> - + @@ -251,7 +251,7 @@ export const AgentLiveProbes = () => { - + @@ -274,11 +274,11 @@ export const AgentLiveProbes = () => { + warningType={DeleteWarningType.DeleteActiveProbes} + visible={warningModalOpen} + onAccept={handleWarningModalAccept} + onClose={handleWarningModalClose} + />
{ const context = React.useContext(ServiceContext); - const history = useHistory(); + const addSubscription = useSubscriptions(); const [templates, setTemplates] = React.useState([] as ProbeTemplate[]); const [filteredTemplates, setFilteredTemplates] = React.useState([] as ProbeTemplate[]); @@ -98,35 +98,8 @@ export const AgentProbeTemplates = () => { const [errorMessage, setErrorMessage] = React.useState(''); const [rowDeleteData, setRowDeleteData] = React.useState({} as IRowData); const [warningModalOpen, setWarningModalOpen] = React.useState(false); - const addSubscription = useSubscriptions(); - - const tableColumns = React.useMemo( - () => [ - { title: 'name', transforms: [sortable] }, - { title: 'xml', transforms: [sortable] }, - ], - [sortable] - ); - React.useEffect(() => { - let filtered; - if (!filterText) { - filtered = templates; - } else { - const ft = filterText.trim().toLowerCase(); - filtered = templates.filter( - (t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft) - ); - } - const { index, direction } = sortBy; - if (typeof index === 'number') { - const keys = ['name', 'xml']; - const key = keys[index]; - const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); - filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); - } - setFilteredTemplates([...filtered]); - }, [filterText, templates, sortBy]); + const tableColumns = React.useMemo(() => [{ title: 'Name', transforms: [sortable] }, { title: 'XML' }], [sortable]); const handleTemplates = React.useCallback( (templates) => { @@ -136,7 +109,6 @@ export const AgentProbeTemplates = () => { }, [setTemplates, setIsLoading, setErrorMessage] ); - const handleError = React.useCallback( (error) => { setIsLoading(false); @@ -148,70 +120,79 @@ export const AgentProbeTemplates = () => { const refreshTemplates = React.useCallback(() => { setIsLoading(true); addSubscription( - context.target - .target() - .pipe( - concatMap((target) => context.api.getProbeTemplates()), - first() - ) - .subscribe( - (value) => handleTemplates(value), - (err) => handleError(err) - ) - ); - }, [addSubscription, context, context.target, context.api, setIsLoading, handleTemplates, handleError]); - - React.useEffect(() => { - addSubscription( - context.target.target().subscribe(() => { - setFilterText(''); - refreshTemplates(); + context.api.getProbeTemplates().subscribe({ + next: (value) => handleTemplates(value), + error: (err) => handleError(err), }) ); - }, [context, context.target, addSubscription, refreshTemplates]); + }, [addSubscription, context.api, setIsLoading, handleTemplates, handleError]); - React.useEffect(() => { - if (!context.settings.autoRefreshEnabled()) { - return; - } - const id = window.setInterval( - () => refreshTemplates(), - context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() - ); - return () => window.clearInterval(id); - }, []); + const handleDelete = React.useCallback( + (rowData) => { + addSubscription( + context.api + .deleteCustomProbeTemplate(rowData[0]) + .pipe(first()) + .subscribe(() => { + /** Do nothing. Notifications hook will handle */ + }) + ); + }, + [addSubscription, context.api, refreshTemplates] + ); - React.useEffect(() => { - const sub = context.target.authFailure().subscribe(() => { - setErrorMessage('Auth failure'); - }); - return () => sub.unsubscribe(); - }, [context.target]); + const handleUploadCancel = React.useCallback(() => { + setUploadFile(undefined); + setUploadFilename(''); + setModalOpen(false); + }, [setUploadFile, setUploadFilename, setModalOpen]); - const displayTemplates = React.useMemo( - () => filteredTemplates.map((t: ProbeTemplate) => [t.name, t.xml]), - [filteredTemplates] + const handleDeleteButton = React.useCallback( + (rowData) => { + if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) { + setRowDeleteData(rowData); + setWarningModalOpen(true); + } else { + handleDelete(rowData); + } + }, + [context.settings, setWarningModalOpen, setRowDeleteData, handleDelete] ); - const handleDelete = (rowData) => { - addSubscription( - context.api - .deleteCustomProbeTemplate(rowData[0]) - .pipe(first()) - .subscribe(() => { - refreshTemplates(); - }) - ); - }; + const handleWarningModalAccept = React.useCallback(() => { + handleDelete(rowDeleteData); + }, [handleDelete, rowDeleteData]); - const handleInsert = (rowData) => { - addSubscription( - context.api - .insertProbes(rowData[0]) - .pipe(first()) - .subscribe(() => {}) - ); - }; + const handleWarningModalClose = React.useCallback(() => { + setWarningModalOpen(false); + }, [setWarningModalOpen]); + + const handleFileRejected = React.useCallback(() => { + setFileRejected(true); + }, [setFileRejected]); + + const handleSort = React.useCallback( + (event, index, direction) => { + setSortBy({ index, direction }); + }, + [setSortBy] + ); + + const authRetry = React.useCallback(() => { + context.target.setAuthRetry(); + }, [context.target, context.target.setAuthRetry]); + + const handleInsert = React.useCallback( + (rowData) => { + addSubscription( + context.api + .insertProbes(rowData[0]) + .pipe(first()) + .subscribe(() => {}) + ); + }, + [addSubscription, context.api] + ); const actionResolver = (rowData: IRowData, extraData: IExtraData) => { if (typeof extraData.rowIndex == 'undefined') { @@ -235,24 +216,24 @@ export const AgentProbeTemplates = () => { return actions; }; - const handleModalToggle = () => { - setModalOpen((v) => { - if (v) { - setUploadFile(undefined); - setUploadFilename(''); - setUploading(false); - } - return !v; - }); - }; + const handleTemplateUpload = React.useCallback(() => { + setModalOpen(true); + }, [setModalOpen]); - const handleFileChange = (value, filename) => { - setFileRejected(false); - setUploadFile(value); - setUploadFilename(filename); - }; + const handleUploadModalClose = React.useCallback(() => { + setModalOpen(false); + }, [setModalOpen]); - const handleUploadSubmit = () => { + const handleFileChange = React.useCallback( + (value, filename) => { + setFileRejected(false); + setUploadFile(value); + setUploadFilename(filename); + }, + [setFileRejected, setUploadFile, setUploadFilename] + ); + + const handleUploadSubmit = React.useCallback(() => { if (!uploadFile) { window.console.error('Attempted to submit template upload without a file selected'); return; @@ -268,11 +249,38 @@ export const AgentProbeTemplates = () => { setUploadFile(undefined); setUploadFilename(''); setModalOpen(false); - refreshTemplates(); } }) ); - }; + }, [setUploading, addSubscription, context.api, setUploadFile, setUploadFilename, setModalOpen]); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe(() => { + setFilterText(''); + refreshTemplates(); + }) + ); + }, [context.target, addSubscription, setFilterText, refreshTemplates]); + + React.useEffect(() => { + if (!context.settings.autoRefreshEnabled()) { + return; + } + const id = window.setInterval( + () => refreshTemplates(), + context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() + ); + return () => window.clearInterval(id); + }, [context.settings, refreshTemplates]); + + React.useEffect(() => { + addSubscription( + context.target.authFailure().subscribe(() => { + setErrorMessage(authFailMessage); + }) + ); + }, [context.target, addSubscription, setErrorMessage]); React.useEffect(() => { addSubscription( @@ -280,60 +288,44 @@ export const AgentProbeTemplates = () => { .messages(NotificationCategory.ProbeTemplateUploaded) .subscribe((v) => refreshTemplates()) ); - }, [addSubscription, context, context.notificationChannel, setTemplates]); + }, [addSubscription, context.notificationChannel, refreshTemplates]); React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.TemplateDeleted).subscribe((v) => refreshTemplates()) ); - }, [addSubscription, context, context.notificationChannel, setTemplates]); + }, [addSubscription, context.notificationChannel, refreshTemplates]); - const handleUploadCancel = () => { - setUploadFile(undefined); - setUploadFilename(''); - setModalOpen(false); - }; + React.useEffect(() => { + let filtered: ProbeTemplate[]; + if (!filterText) { + filtered = templates; + } else { + const ft = filterText.trim().toLowerCase(); + filtered = templates.filter( + (t: ProbeTemplate) => t.name.toLowerCase().includes(ft) || t.xml.toLowerCase().includes(ft) + ); + } + const { index, direction } = sortBy; + if (typeof index === 'number') { + const keys = ['name', 'xml']; + const key = keys[index]; + const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); + filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); + } + setFilteredTemplates([...filtered]); + }, [filterText, templates, sortBy, setFilteredTemplates]); - const handleDeleteButton = React.useCallback( - (rowData) => { - if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) { - setRowDeleteData(rowData); - setWarningModalOpen(true); - } else { - handleDelete(rowData); - } - }, - [context, context.settings, setWarningModalOpen, setRowDeleteData, handleDelete] + const displayTemplates = React.useMemo( + () => filteredTemplates.map((t: ProbeTemplate) => [t.name, t.xml]), + [filteredTemplates] ); - const handleWarningModalAccept = React.useCallback(() => { - handleDelete(rowDeleteData); - }, [handleDelete, rowDeleteData]); - - const handleWarningModalClose = React.useCallback(() => { - setWarningModalOpen(false); - }, [setWarningModalOpen]); - - const handleFileRejected = () => { - setFileRejected(true); - }; - - const handleSort = (event, index, direction) => { - setSortBy({ index, direction }); - }; - - const authRetry = React.useCallback(() => { - context.target.setAuthRetry(); - }, [context.target, context.target.setAuthRetry]); - if (errorMessage != '') { return ( ); @@ -342,100 +334,105 @@ export const AgentProbeTemplates = () => { } else { 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/remove them from targets, as well as view currently active - probes. - - - - - - - + + + + + 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. + + + + + + + + + + + + + + + + + - - - - - - - - - - -
- - -
- - -
- + + + + +
+ - -
- - - - -
-
+
+ + + + + + + +
+ + + ); } From 7e4d03f139b276bb077b3109d6b557e9b7a2e769 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 23 Oct 2022 22:05:58 -0400 Subject: [PATCH 26/43] chore(agent): continue to clean up --- src/app/Agent/AgentProbeTemplates.tsx | 76 +++----- src/app/Events/EventTemplates.tsx | 11 +- src/app/Events/Events.tsx | 15 +- src/app/Shared/Services/Api.service.tsx | 21 +- src/test/Agent/AgentLiveProbes.test.tsx | 50 +++++ ....test.tsx => AgentProbeTemplates.test.tsx} | 183 +++++++++--------- 6 files changed, 193 insertions(+), 163 deletions(-) create mode 100644 src/test/Agent/AgentLiveProbes.test.tsx rename src/test/Agent/{Agent.test.tsx => AgentProbeTemplates.test.tsx} (58%) diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index c9d724b18..f15a8d6c3 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -76,7 +76,7 @@ import { } from '@patternfly/react-table'; import { first } from 'rxjs/operators'; import { LoadingView } from '@app/LoadingView/LoadingView'; -import { authFailMessage, ErrorView, isAuthFail } from '@app/ErrorView/ErrorView'; +import { ErrorView } from '@app/ErrorView/ErrorView'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; @@ -138,7 +138,7 @@ export const AgentProbeTemplates = () => { }) ); }, - [addSubscription, context.api, refreshTemplates] + [addSubscription, context.api] ); const handleUploadCancel = React.useCallback(() => { @@ -178,10 +178,6 @@ export const AgentProbeTemplates = () => { [setSortBy] ); - const authRetry = React.useCallback(() => { - context.target.setAuthRetry(); - }, [context.target, context.target.setAuthRetry]); - const handleInsert = React.useCallback( (rowData) => { addSubscription( @@ -194,27 +190,27 @@ export const AgentProbeTemplates = () => { [addSubscription, context.api] ); - const actionResolver = (rowData: IRowData, extraData: IExtraData) => { - if (typeof extraData.rowIndex == 'undefined') { - return []; - } - let actions = [ - { - title: 'Insert Probes...', - onClick: (event, rowId, rowData) => handleInsert(rowData), - }, - ] as IAction[]; - actions = actions.concat([ - { - isSeparator: true, - }, - { - title: 'Delete', - onClick: (event, rowId, rowData) => handleDeleteButton(rowData), - }, - ]); - return actions; - }; + const actionResolver = React.useCallback( + (rowData: IRowData, extraData: IExtraData) => { + if (typeof extraData.rowIndex == 'undefined') { + return []; + } + return [ + { + title: 'Insert Probes...', + onClick: (event, rowId, rowData) => handleInsert(rowData), + }, + { + isSeparator: true, + }, + { + title: 'Delete', + onClick: (event, rowId, rowData) => handleDeleteButton(rowData), + }, + ] as IAction[]; + }, + [handleInsert, handleDeleteButton] + ); const handleTemplateUpload = React.useCallback(() => { setModalOpen(true); @@ -235,7 +231,7 @@ export const AgentProbeTemplates = () => { const handleUploadSubmit = React.useCallback(() => { if (!uploadFile) { - window.console.error('Attempted to submit template upload without a file selected'); + window.console.error('Attempted to submit probe template upload without a file selected'); return; } setUploading(true); @@ -252,16 +248,11 @@ export const AgentProbeTemplates = () => { } }) ); - }, [setUploading, addSubscription, context.api, setUploadFile, setUploadFilename, setModalOpen]); + }, [uploadFile, setUploading, addSubscription, context.api, setUploadFile, setUploadFilename, setModalOpen]); React.useEffect(() => { - addSubscription( - context.target.target().subscribe(() => { - setFilterText(''); - refreshTemplates(); - }) - ); - }, [context.target, addSubscription, setFilterText, refreshTemplates]); + refreshTemplates(); + }, [refreshTemplates]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { @@ -274,14 +265,6 @@ export const AgentProbeTemplates = () => { return () => window.clearInterval(id); }, [context.settings, refreshTemplates]); - React.useEffect(() => { - addSubscription( - context.target.authFailure().subscribe(() => { - setErrorMessage(authFailMessage); - }) - ); - }, [context.target, addSubscription, setErrorMessage]); - React.useEffect(() => { addSubscription( context.notificationChannel @@ -292,7 +275,9 @@ export const AgentProbeTemplates = () => { React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.TemplateDeleted).subscribe((v) => refreshTemplates()) + context.notificationChannel + .messages(NotificationCategory.ProbeTemplateDeleted) + .subscribe((v) => refreshTemplates()) ); }, [addSubscription, context.notificationChannel, refreshTemplates]); @@ -326,7 +311,6 @@ export const AgentProbeTemplates = () => { ); } else if (isLoading) { diff --git a/src/app/Events/EventTemplates.tsx b/src/app/Events/EventTemplates.tsx index 2dd60bd1e..5313b68e6 100644 --- a/src/app/Events/EventTemplates.tsx +++ b/src/app/Events/EventTemplates.tsx @@ -204,11 +204,12 @@ export const EventTemplates = () => { }, []); React.useEffect(() => { - const sub = context.target.authFailure().subscribe(() => { - setErrorMessage(authFailMessage); - }); - return () => sub.unsubscribe(); - }, [context.target]); + addSubscription( + context.target.authFailure().subscribe(() => { + setErrorMessage(authFailMessage); + }) + ); + }, [addSubscription, context.target, setErrorMessage]); const displayTemplates = React.useMemo( () => diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 974b0124a..888832d27 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -44,6 +44,8 @@ import { EventTemplates } from './EventTemplates'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; import { EventTypes } from './EventTypes'; +import { concatMap, filter } from 'rxjs'; +import { NO_TARGET } from '@app/Shared/Services/Target.service'; export interface EventsProps {} @@ -56,12 +58,15 @@ export const Events: React.FunctionComponent = (props) => { React.useEffect(() => { addSubscription( - context.api.getActiveProbes(true).subscribe({ - next: (_) => setEnabled(true), - error: (_) => setEnabled(false), - }) + context.target + .target() + .pipe( + filter((target) => target !== NO_TARGET), + concatMap((_) => context.api.isProbeEnabled()) + ) + .subscribe(setEnabled) ); - }, [addSubscription, context.api, setEnabled]); + }, [addSubscription, context.target, context.api, setEnabled]); const handleEventTabSelect = React.useCallback((evt, idx) => setEventActiveTab(idx), [setEventActiveTab]); diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index e1e8608bd..86874b046 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -35,19 +35,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { - from, - Observable, - ObservableInput, - of, - ReplaySubject, - forkJoin, - throwError, - EMPTY, - shareReplay, - Subject, - BehaviorSubject, -} from 'rxjs'; +import { from, Observable, ObservableInput, of, ReplaySubject, forkJoin, throwError, EMPTY, shareReplay } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; import { catchError, concatMap, filter, first, map, mergeMap, tap } from 'rxjs/operators'; import { NO_TARGET, Target, TargetService } from './Target.service'; @@ -444,6 +432,13 @@ export class ApiService { ); } + isProbeEnabled(): Observable { + return this.getActiveProbes(true).pipe( + concatMap((_) => of(true)), + catchError((_) => of(false)) + ); + } + deleteCustomEventTemplate(templateName: string): Observable { return this.sendRequest('v1', `templates/${encodeURIComponent(templateName)}`, { method: 'DELETE', diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx new file mode 100644 index 000000000..8db35daa0 --- /dev/null +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import { render, screen, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { of } from 'rxjs'; +import { EventTemplate, ProbeTemplate } from '@app/Shared/Services/Api.service'; +import { MessageMeta, MessageType, NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; +import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; +import { EventTemplates } from '@app/Events/EventTemplates'; +import userEvent from '@testing-library/user-event'; +import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; +import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; diff --git a/src/test/Agent/Agent.test.tsx b/src/test/Agent/AgentProbeTemplates.test.tsx similarity index 58% rename from src/test/Agent/Agent.test.tsx rename to src/test/Agent/AgentProbeTemplates.test.tsx index 420f90218..2910aee27 100644 --- a/src/test/Agent/Agent.test.tsx +++ b/src/test/Agent/AgentProbeTemplates.test.tsx @@ -40,10 +40,14 @@ import renderer, { act } from 'react-test-renderer'; import { render, screen, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import { of } from 'rxjs'; -import { EventTemplate, ProbeTemplate } from '@app/Shared/Services/Api.service'; -import { MessageMeta, MessageType, NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; +import { ProbeTemplate } from '@app/Shared/Services/Api.service'; +import { + MessageMeta, + MessageType, + NotificationCategory, + NotificationMessage, +} from '@app/Shared/Services/NotificationChannel.service'; import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; -import { EventTemplates } from '@app/Events/EventTemplates'; import userEvent from '@testing-library/user-event'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; @@ -53,46 +57,37 @@ const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; -const mockCustomEventTemplate: ProbeTemplate = { +const mockProbeTemplate: ProbeTemplate = { name: 'someProbeTemplate', xml: '', }; -const mockAnotherTemplate: ProbeTemplate = { +const mockAnotherProbeTemplate: ProbeTemplate = { name: 'anotherProbeTemplate', xml: '', }; -const mockData: ProbeTemplate[] = [mockCustomEventTemplate, mockAnotherTemplate]; - const mockCreateTemplateNotification = { meta: { - category: 'ProbeTemplateUploaded', + category: NotificationCategory.ProbeTemplateUploaded, type: mockMessageType, } as MessageMeta, message: { - template: mockAnotherTemplate, + template: mockAnotherProbeTemplate, }, } as NotificationMessage; + const mockDeleteTemplateNotification = { - ...mockCreateTemplateNotification, meta: { - category: 'ProbeTemplateDeleted', + category: NotificationCategory.ProbeTemplateDeleted, type: mockMessageType, }, -}; - -const mockHistoryPush = jest.fn(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useRouteMatch: () => ({ url: '/baseUrl' }), - useHistory: () => ({ - push: mockHistoryPush, - }), -})); + message: { + template: mockProbeTemplate, + }, +} as NotificationMessage; -jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor').mockReturnValue(false); // don't ask again +jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor').mockReturnValue(false); jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); @@ -101,13 +96,13 @@ jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); jest .spyOn(defaultServices.api, 'getProbeTemplates') - .mockReturnValueOnce(of([mockCustomEventTemplate])) // Renders Correctly - .mockReturnValueOnce(of([mockCustomEventTemplate])) - .mockReturnValueOnce(of(mockData)) // Adds a probe template - .mockReturnValueOnce(of(mockData)) + .mockReturnValueOnce(of([mockProbeTemplate])) // Renders Correctly + .mockReturnValueOnce(of([mockProbeTemplate])) + .mockReturnValueOnce(of([mockProbeTemplate, mockAnotherProbeTemplate])) // Adds a probe template + .mockReturnValueOnce(of([mockProbeTemplate, mockAnotherProbeTemplate])) .mockReturnValueOnce(of([])) // Removes a probe template .mockReturnValueOnce(of([])) - .mockReturnValue(of([mockCustomEventTemplate])); // All other tests + .mockReturnValue(of([mockProbeTemplate])); // All other tests jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); @@ -135,70 +130,70 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); - it('adds a recording after receiving a notification', () => { - render( - - - - ); - - expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); - }); - - it('removes a recording after receiving a notification', () => { - render( - - - - ); - expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); - }); - - it('displays the column header fields', () => { - render( - - - - ); - expect(screen.getByText('name')).toBeInTheDocument(); - expect(screen.getByText('xml')).toBeInTheDocument(); - }); - - it('shows a popup when uploading', () => { - render( - - - - ); - expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); - - const buttons = screen.getAllByRole('button'); - const uploadButton = buttons[0]; - userEvent.click(uploadButton); - - expect(screen.getByLabelText('Create Custom Probe Template')); - }); - - it('Tests that delete works correctly', () => { - render( - - - - ); - - userEvent.click(screen.getByLabelText('Actions')); - - expect(screen.getByText('Insert Probes...')); - expect(screen.getByText('Delete')); - - const deleteAction = screen.getByText('Delete'); - userEvent.click(deleteAction); - - //expect(screen.getByLabelText('Event template delete warning')); - - const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); - - expect(deleteRequestSpy).toHaveBeenCalledTimes(1); - expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); - }); + // it('adds a recording after receiving a notification', () => { + // render( + // + // + // + // ); + + // expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); + // }); + + // it('removes a recording after receiving a notification', () => { + // render( + // + // + // + // ); + // expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); + // }); + + // it('displays the column header fields', () => { + // render( + // + // + // + // ); + // expect(screen.getByText('name')).toBeInTheDocument(); + // expect(screen.getByText('xml')).toBeInTheDocument(); + // }); + + // it('shows a popup when uploading', () => { + // render( + // + // + // + // ); + // expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); + + // const buttons = screen.getAllByRole('button'); + // const uploadButton = buttons[0]; + // userEvent.click(uploadButton); + + // expect(screen.getByLabelText('Create Custom Probe Template')); + // }); + + // it('Tests that delete works correctly', () => { + // render( + // + // + // + // ); + + // userEvent.click(screen.getByLabelText('Actions')); + + // expect(screen.getByText('Insert Probes...')); + // expect(screen.getByText('Delete')); + + // const deleteAction = screen.getByText('Delete'); + // userEvent.click(deleteAction); + + // //expect(screen.getByLabelText('Event template delete warning')); + + // const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); + + // expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + // expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); + // }); }); From 13baf3bf153b34eecea4a76f0303a283ca55d5a1 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 24 Oct 2022 03:35:43 -0400 Subject: [PATCH 27/43] tests(agent): fix tests --- src/app/Agent/AgentLiveProbes.tsx | 52 +-- src/app/Agent/AgentProbeTemplates.tsx | 4 +- src/test/Agent/AgentLiveProbes.test.tsx | 168 +++++++- src/test/Agent/AgentProbeTemplates.test.tsx | 266 ++++++++---- .../Agent/__snapshots__/Agent.test.tsx.snap | 320 -------------- .../AgentLiveProbes.test.tsx.snap | 404 ++++++++++++++++++ .../AgentProbeTemplates.test.tsx.snap | 314 ++++++++++++++ 7 files changed, 1091 insertions(+), 437 deletions(-) delete mode 100644 src/test/Agent/__snapshots__/Agent.test.tsx.snap create mode 100644 src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap create mode 100644 src/test/Agent/__snapshots__/AgentProbeTemplates.test.tsx.snap diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 360e3cbf8..2feef43f5 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -71,12 +71,14 @@ import { EventProbe } from '@app/Shared/Services/Api.service'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; -export const AgentLiveProbes = () => { +export interface AgentLiveProbesProps {} + +export const AgentLiveProbes: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); - const [templates, setTemplates] = React.useState([] as EventProbe[]); - const [filteredTemplates, setFilteredTemplates] = React.useState([] as EventProbe[]); + const [probes, setProbes] = React.useState([] as EventProbe[]); + const [filteredProbes, setFilteredProbes] = React.useState([] as EventProbe[]); const [filterText, setFilterText] = React.useState(''); const [sortBy, setSortBy] = React.useState({} as ISortBy); const [isLoading, setIsLoading] = React.useState(false); @@ -91,13 +93,13 @@ export const AgentLiveProbes = () => { { title: 'Method', transforms: [sortable] }, ]; - const handleTemplates = React.useCallback( - (templates) => { - setTemplates(templates); + const handleProbes = React.useCallback( + (probes) => { + setProbes(probes); setErrorMessage(''); setIsLoading(false); }, - [setTemplates, setIsLoading, setErrorMessage] + [setProbes, setIsLoading, setErrorMessage] ); const handleError = React.useCallback( @@ -108,15 +110,15 @@ export const AgentLiveProbes = () => { [setIsLoading, setErrorMessage] ); - const refreshTemplates = React.useCallback(() => { + const refreshProbes = React.useCallback(() => { setIsLoading(true); addSubscription( context.api.getActiveProbes().subscribe({ - next: (value) => handleTemplates(value), + next: (value) => handleProbes(value), error: (err) => handleError(err), }) ); - }, [addSubscription, context.api, setIsLoading, handleTemplates, handleError]); + }, [addSubscription, context.api, setIsLoading, handleProbes, handleError]); const handleSort = React.useCallback( (evt, index, direction) => { @@ -135,10 +137,10 @@ export const AgentLiveProbes = () => { .removeProbes() .pipe(first()) .subscribe(() => { - refreshTemplates(); + refreshProbes(); }) ); - }, [addSubscription, context.api, refreshTemplates]); + }, [addSubscription, context.api, refreshProbes]); const handleWarningModalAccept = React.useCallback(() => handleDeleteAllProbes(), [handleDeleteAllProbes]); @@ -158,21 +160,21 @@ export const AgentLiveProbes = () => { addSubscription( context.target.target().subscribe(() => { setFilterText(''); - refreshTemplates(); + refreshProbes(); }) ); - }, [context, context.target, addSubscription, setFilterText, refreshTemplates]); + }, [context, context.target, addSubscription, setFilterText, refreshProbes]); React.useEffect(() => { if (!context.settings.autoRefreshEnabled()) { return; } const id = window.setInterval( - () => refreshTemplates(), + () => refreshProbes(), context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits() ); return () => window.clearInterval(id); - }, [context.settings, refreshTemplates]); + }, [context.settings, refreshProbes]); React.useEffect(() => { addSubscription( @@ -184,19 +186,17 @@ export const AgentLiveProbes = () => { React.useEffect(() => { addSubscription( - context.notificationChannel - .messages(NotificationCategory.ProbeTemplateApplied) - .subscribe((v) => refreshTemplates()) + context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied).subscribe((v) => refreshProbes()) ); - }, [addSubscription, context, context.notificationChannel, setTemplates]); + }, [addSubscription, context, context.notificationChannel, setProbes]); React.useEffect(() => { let filtered: EventProbe[]; if (!filterText) { - filtered = templates; + filtered = probes; } else { const ft = filterText.trim().toLowerCase(); - filtered = templates.filter( + filtered = probes.filter( (t: EventProbe) => t.name.toLowerCase().includes(ft) || t.description.toLowerCase().includes(ft) || @@ -212,12 +212,12 @@ export const AgentLiveProbes = () => { const sorted = filtered.sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)); filtered = direction === SortByDirection.asc ? sorted : sorted.reverse(); } - setFilteredTemplates([...filtered]); - }, [filterText, templates, sortBy, setFilteredTemplates]); + setFilteredProbes([...filtered]); + }, [filterText, probes, sortBy, setFilteredProbes]); const displayTemplates = React.useMemo( - () => filteredTemplates.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName]), - [filteredTemplates] + () => filteredProbes.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName]), + [filteredProbes] ); if (errorMessage != '') { diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index f15a8d6c3..694f493ca 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -81,7 +81,9 @@ import { ProbeTemplate } from '@app/Shared/Services/Api.service'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; -export const AgentProbeTemplates = () => { +export interface AgentProbeTemplatesProps {} + +export const AgentProbeTemplates: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index 8db35daa0..afdd8c691 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -35,16 +35,170 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - import * as React from 'react'; import renderer, { act } from 'react-test-renderer'; -import { render, screen, within } from '@testing-library/react'; +import { cleanup, render, screen, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import { of } from 'rxjs'; -import { EventTemplate, ProbeTemplate } from '@app/Shared/Services/Api.service'; -import { MessageMeta, MessageType, NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; +import { EventProbe } from '@app/Shared/Services/Api.service'; +import { + MessageMeta, + MessageType, + NotificationCategory, + NotificationMessage, +} from '@app/Shared/Services/NotificationChannel.service'; import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; -import { EventTemplates } from '@app/Events/EventTemplates'; import userEvent from '@testing-library/user-event'; -import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; -import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; +import { DeleteActiveProbes } from '@app/Modal/DeleteWarningUtils'; +import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; + +const mockConnectUrl = 'service:jmx:rmi://someUrl'; +const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; + +const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; + +const mockProbe: EventProbe = { + id: 'some_id', + name: 'some_name', + clazz: 'some_clazz', + description: 'some_desc', + recordStackTrace: true, + useRethrow: true, + methodName: 'a_method', + methodDescriptor: 'method_desc', + location: 'some_loc', + returnValue: 'a_value', + parameters: 'some_params', + fields: 'some_fields', + path: 'some_path', +}; + +const mockAnotherProbe: EventProbe = { + ...mockProbe, + id: 'another_id', + name: 'another_name', +}; + +const mockApplyTemplateNotification = { + meta: { + category: NotificationCategory.ProbeTemplateApplied, + type: mockMessageType, + } as MessageMeta, + message: { + template: mockProbe, + }, +} as NotificationMessage; + +jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); +jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); + +jest + .spyOn(defaultServices.settings, 'deletionDialogsEnabledFor') + .mockReturnValueOnce(false) // should remove all probes when Remove All Probe is clicked + .mockReturnValue(true); // should show warning modal and remove all probes when confirmed + +jest + .spyOn(defaultServices.api, 'getActiveProbes') + .mockReturnValueOnce(of([mockProbe])) // renders correctly + + .mockReturnValueOnce(of([mockProbe])) // should add a probe after receiving a notification + .mockReturnValueOnce(of([mockProbe, mockAnotherProbe])) + + .mockReturnValue(of([mockProbe])); // All other tests + +jest + .spyOn(defaultServices.notificationChannel, 'messages') + .mockReturnValueOnce(of()) // renders correctly + .mockReturnValueOnce(of(mockApplyTemplateNotification)) // should add a probe after receiving a notification + .mockReturnValue(of()); // All other tests + +describe('', () => { + afterEach(cleanup); + + it('renders correctly', async () => { + let tree; + await act(async () => { + tree = renderer.create( + + + + ); + }); + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('should add a probe after receiving a notification', () => { + render( + + + + ); + + const addTemplateName = screen.getByText('another_name'); + expect(addTemplateName).toBeInTheDocument(); + expect(addTemplateName).toBeVisible(); + }); + + it('should display the column header fields', () => { + render( + + + + ); + + const headers = ['ID', 'Name', 'Class', 'Description', 'Method']; + headers.forEach((header) => { + const nameHeader = screen.getByText(header); + expect(nameHeader).toBeInTheDocument(); + expect(nameHeader).toBeVisible(); + }); + }); + + it('should remove all probes when Remove All Probe is clicked', () => { + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); + render( + + + + ); + + const removeButton = screen.getByText('Remove All Probes'); + expect(removeButton).toBeInTheDocument(); + expect(removeButton).toBeInTheDocument(); + + userEvent.click(removeButton); + + expect(deleteRequestSpy).toBeCalledTimes(1); + }); + + it.skip('should show warning modal and remove all probes when confirmed', async () => { + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); + render( + + + + ); + + const removeButton = screen.getByText('Remove All Probes'); + expect(removeButton).toBeInTheDocument(); + expect(removeButton).toBeInTheDocument(); + + userEvent.click(removeButton); + + const warningModal = await screen.findByRole('dialog'); + expect(warningModal).toBeInTheDocument(); + expect(warningModal).toBeVisible(); + + const modalTitle = within(warningModal).getByText(DeleteActiveProbes.title); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); + + const confirmButton = within(warningModal).getByText('Delete'); + expect(confirmButton).toBeInTheDocument(); + expect(confirmButton).toBeVisible(); + + userEvent.click(confirmButton); + + expect(deleteRequestSpy).toBeCalledTimes(1); + }); +}); diff --git a/src/test/Agent/AgentProbeTemplates.test.tsx b/src/test/Agent/AgentProbeTemplates.test.tsx index 2910aee27..80375b152 100644 --- a/src/test/Agent/AgentProbeTemplates.test.tsx +++ b/src/test/Agent/AgentProbeTemplates.test.tsx @@ -37,7 +37,7 @@ */ import * as React from 'react'; import renderer, { act } from 'react-test-renderer'; -import { render, screen, within } from '@testing-library/react'; +import { act as doAct, cleanup, render, screen, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import { of } from 'rxjs'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; @@ -49,12 +49,9 @@ import { } from '@app/Shared/Services/NotificationChannel.service'; import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; import userEvent from '@testing-library/user-event'; -import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; +import { DeleteProbeTemplates } from '@app/Modal/DeleteWarningUtils'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; -const mockConnectUrl = 'service:jmx:rmi://someUrl'; -const mockTarget = { connectUrl: mockConnectUrl, alias: 'fooTarget' }; - const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; const mockProbeTemplate: ProbeTemplate = { @@ -67,6 +64,8 @@ const mockAnotherProbeTemplate: ProbeTemplate = { xml: '', }; +const mockFileUpload = new File([mockProbeTemplate.xml], 'probe_template.xml', { type: 'xml' }); + const mockCreateTemplateNotification = { meta: { category: NotificationCategory.ProbeTemplateUploaded, @@ -87,25 +86,25 @@ const mockDeleteTemplateNotification = { }, } as NotificationMessage; -jest.spyOn(defaultServices.settings, 'deletionDialogsEnabledFor').mockReturnValue(false); +jest + .spyOn(defaultServices.settings, 'deletionDialogsEnabledFor') + .mockReturnValueOnce(false) // should delete a probe template when Delete is clicked + .mockReturnValue(true); // should show warning modal and delete a probe template when confirmed -jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); -jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); +const uploadRequestSpy = jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); -jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); jest .spyOn(defaultServices.api, 'getProbeTemplates') - .mockReturnValueOnce(of([mockProbeTemplate])) // Renders Correctly - .mockReturnValueOnce(of([mockProbeTemplate])) - .mockReturnValueOnce(of([mockProbeTemplate, mockAnotherProbeTemplate])) // Adds a probe template + .mockReturnValueOnce(of([mockProbeTemplate])) // renders Correctly + + .mockReturnValueOnce(of([mockProbeTemplate])) // should add a probe template after receiving a notification .mockReturnValueOnce(of([mockProbeTemplate, mockAnotherProbeTemplate])) - .mockReturnValueOnce(of([])) // Removes a probe template + + .mockReturnValueOnce(of([mockProbeTemplate])) // should remove a probe template after receiving a notification .mockReturnValueOnce(of([])) - .mockReturnValue(of([mockProbeTemplate])); // All other tests -jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); -jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); + .mockReturnValue(of([mockProbeTemplate])); // All other tests jest .spyOn(defaultServices.notificationChannel, 'messages') @@ -113,11 +112,16 @@ jest .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockCreateTemplateNotification)) // adds a template after receiving a notification + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockDeleteTemplateNotification)) // removes a template after receiving a notification - .mockReturnValue(of()); // All Other tests + .mockReturnValue(of()); // All other tests describe('', () => { + afterEach(cleanup); + it('renders correctly', async () => { let tree; await act(async () => { @@ -130,70 +134,166 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); - // it('adds a recording after receiving a notification', () => { - // render( - // - // - // - // ); - - // expect(screen.getByText('someProbeTemplate')).toBeInTheDocument(); - // }); - - // it('removes a recording after receiving a notification', () => { - // render( - // - // - // - // ); - // expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); - // }); - - // it('displays the column header fields', () => { - // render( - // - // - // - // ); - // expect(screen.getByText('name')).toBeInTheDocument(); - // expect(screen.getByText('xml')).toBeInTheDocument(); - // }); - - // it('shows a popup when uploading', () => { - // render( - // - // - // - // ); - // expect(screen.queryByLabelText('Create Custom Probe Template')).not.toBeInTheDocument(); - - // const buttons = screen.getAllByRole('button'); - // const uploadButton = buttons[0]; - // userEvent.click(uploadButton); - - // expect(screen.getByLabelText('Create Custom Probe Template')); - // }); - - // it('Tests that delete works correctly', () => { - // render( - // - // - // - // ); - - // userEvent.click(screen.getByLabelText('Actions')); - - // expect(screen.getByText('Insert Probes...')); - // expect(screen.getByText('Delete')); - - // const deleteAction = screen.getByText('Delete'); - // userEvent.click(deleteAction); - - // //expect(screen.getByLabelText('Event template delete warning')); - - // const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate'); - - // expect(deleteRequestSpy).toHaveBeenCalledTimes(1); - // expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); - // }); + it('should add a probe template after receiving a notification', () => { + render( + + + + ); + + const addTemplateName = screen.getByText('anotherProbeTemplate'); + expect(addTemplateName).toBeInTheDocument(); + expect(addTemplateName).toBeVisible(); + }); + + it('should remove a probe template after receiving a notification', () => { + render( + + + + ); + expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); + }); + + it('should display the column header fields', () => { + render( + + + + ); + const nameHeader = screen.getByText('Name'); + expect(nameHeader).toBeInTheDocument(); + expect(nameHeader).toBeVisible(); + + const xmlHeader = screen.getByText('XML'); + expect(xmlHeader).toBeInTheDocument(); + expect(xmlHeader).toBeVisible(); + }); + + it('should show modal when uploading', async () => { + render( + + + + ); + + const uploadButton = screen.getByRole('button', { name: 'Upload' }); + expect(uploadButton).toBeInTheDocument(); + expect(uploadButton).toBeVisible(); + + userEvent.click(uploadButton); + + const uploadModal = await screen.findByRole('dialog'); + expect(uploadModal).toBeInTheDocument(); + expect(uploadModal).toBeVisible(); + + const modalTitle = within(uploadModal).getByText('Create Custom Probe Template'); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); + }); + + it('should upload a probe template when form is filled and Submit is clicked', async () => { + render( + + + + ); + + const uploadButton = screen.getByRole('button', { name: 'Upload' }); + expect(uploadButton).toBeInTheDocument(); + expect(uploadButton).toBeVisible(); + + userEvent.click(uploadButton); + + const uploadModal = await screen.findByRole('dialog'); + expect(uploadModal).toBeInTheDocument(); + expect(uploadModal).toBeVisible(); + + const modalTitle = within(uploadModal).getByText('Create Custom Probe Template'); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); + + const fileUploadDropZone = within(uploadModal).getByLabelText( + 'Drag a file here or browse to upload' + ) as HTMLInputElement; + expect(fileUploadDropZone).toBeInTheDocument(); + expect(fileUploadDropZone).toBeVisible(); + + const browseButton = within(uploadModal).getByRole('button', { name: 'Browse...' }); + expect(browseButton).toBeInTheDocument(); + expect(browseButton).toBeVisible(); + + const uploadInput = uploadModal.querySelector("input[accept='.xml'][type='file']") as HTMLInputElement; + expect(uploadInput).toBeInTheDocument(); + expect(uploadInput).not.toBeVisible(); + + userEvent.click(browseButton); + userEvent.upload(uploadInput, mockFileUpload); + + expect(uploadInput.files).not.toBe(null); + expect(uploadInput.files![0]).toStrictEqual(mockFileUpload); + + const submitButton = screen.getByRole('button', { name: 'Submit' }) as HTMLButtonElement; + await waitFor(() => expect(submitButton).not.toBeDisabled()); + await doAct(async () => { + userEvent.click(submitButton); + }); + + expect(uploadRequestSpy).toHaveBeenCalledTimes(1); + expect(uploadRequestSpy).toHaveBeenCalledWith(mockFileUpload); + }); + + it('should delete a probe template when Delete is clicked', async () => { + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); + render( + + + + ); + + userEvent.click(screen.getByLabelText('Actions')); + + const deleteButton = await screen.findByText('Delete'); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toBeVisible(); + + userEvent.click(deleteButton); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); + }); + + it('should show warning modal and delete a probe template when confirmed', async () => { + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); + render( + + + + ); + + userEvent.click(screen.getByLabelText('Actions')); + + const deleteButton = await screen.findByText('Delete'); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toBeVisible(); + + userEvent.click(deleteButton); + + const warningModal = await screen.findByRole('dialog'); + expect(warningModal).toBeInTheDocument(); + expect(warningModal).toBeVisible(); + + const modalTitle = within(warningModal).getByText(DeleteProbeTemplates.title); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); + + const confirmButton = within(warningModal).getByText('Delete'); + expect(confirmButton).toBeInTheDocument(); + expect(confirmButton).toBeVisible(); + + userEvent.click(confirmButton); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); + }); }); diff --git a/src/test/Agent/__snapshots__/Agent.test.tsx.snap b/src/test/Agent/__snapshots__/Agent.test.tsx.snap deleted file mode 100644 index 5b51e9328..000000000 --- a/src/test/Agent/__snapshots__/Agent.test.tsx.snap +++ /dev/null @@ -1,320 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders correctly 1`] = ` -Array [ -
-
-
-

- 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/remove them from targets, as well as view currently active probes. -
-
, -
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
- , - - - - - - - - - - - - - - - -
, -] -`; diff --git a/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap b/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap new file mode 100644 index 000000000..6b2458466 --- /dev/null +++ b/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap @@ -0,0 +1,404 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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. +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+`; diff --git a/src/test/Agent/__snapshots__/AgentProbeTemplates.test.tsx.snap b/src/test/Agent/__snapshots__/AgentProbeTemplates.test.tsx.snap new file mode 100644 index 000000000..38e332b3b --- /dev/null +++ b/src/test/Agent/__snapshots__/AgentProbeTemplates.test.tsx.snap @@ -0,0 +1,314 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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. +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+`; From 1d86c1aecfe137f296fad82660c8175ef67e4db3 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 24 Oct 2022 15:13:33 -0400 Subject: [PATCH 28/43] Suppress notifications on refreshing probes --- src/app/Agent/AgentLiveProbes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 2feef43f5..a17fc95c8 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -113,7 +113,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p const refreshProbes = React.useCallback(() => { setIsLoading(true); addSubscription( - context.api.getActiveProbes().subscribe({ + context.api.getActiveProbes(true).subscribe({ next: (value) => handleProbes(value), error: (err) => handleError(err), }) From cf62bf31ee3ae75620ac866082a9e93248900cdf Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 24 Oct 2022 15:51:07 -0400 Subject: [PATCH 29/43] Fix notification handling for removing probes --- src/app/Shared/Services/NotificationChannel.service.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/Shared/Services/NotificationChannel.service.tsx b/src/app/Shared/Services/NotificationChannel.service.tsx index c2d2ae5fa..874e4ecac 100644 --- a/src/app/Shared/Services/NotificationChannel.service.tsx +++ b/src/app/Shared/Services/NotificationChannel.service.tsx @@ -62,6 +62,7 @@ export enum NotificationCategory { ProbeTemplateUploaded = 'ProbeTemplateUploaded', ProbeTemplateDeleted = 'ProbeTemplateDeleted', ProbeTemplateApplied = 'ProbeTemplateApplied', + ProbesRemoved = 'ProbesRemoved', RuleCreated = 'RuleCreated', RuleUpdated = 'RuleUpdated', RuleDeleted = 'RuleDeleted', @@ -230,6 +231,14 @@ export const messageKeys = new Map([ body: (evt) => `${evt.message.probeTemplate} was deleted`, } as NotificationMessageMapper, ], + [ + NotificationCategory.ProbesRemoved, + { + variant: AlertVariant.success, + title: 'Probes Removed from Target', + body: (evt) => `Probes successfully removed from ${evt.message.target}`, + } as NotificationMessageMapper, + ], [ NotificationCategory.RuleCreated, { From 96ca5f63c208f593b08e6d8a092e64bce4c9764c Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 16:03:55 -0400 Subject: [PATCH 30/43] leave card visible but disable tab if Agent not detected --- src/app/Events/Events.tsx | 47 ++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 888832d27..8c6531323 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -39,7 +39,7 @@ import * as React from 'react'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { TargetView } from '@app/TargetView/TargetView'; -import { Card, CardBody, Stack, StackItem, Tab, Tabs } from '@patternfly/react-core'; +import { Card, CardBody, Stack, StackItem, Tab, Tabs, Tooltip } from '@patternfly/react-core'; import { EventTemplates } from './EventTemplates'; import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates'; import { AgentLiveProbes } from '@app/Agent/AgentLiveProbes'; @@ -54,7 +54,7 @@ export const Events: React.FunctionComponent = (props) => { const addSubscription = useSubscriptions(); const [eventActiveTab, setEventActiveTab] = React.useState(0); const [probeActiveTab, setProbeActiveTab] = React.useState(0); - const [enabled, setEnabled] = React.useState(false); + const [agentDetected, setAgentDetected] = React.useState(false); React.useEffect(() => { addSubscription( @@ -64,9 +64,9 @@ export const Events: React.FunctionComponent = (props) => { filter((target) => target !== NO_TARGET), concatMap((_) => context.api.isProbeEnabled()) ) - .subscribe(setEnabled) + .subscribe(setAgentDetected) ); - }, [addSubscription, context.target, context.api, setEnabled]); + }, [addSubscription, context.target, context.api, setAgentDetected]); const handleEventTabSelect = React.useCallback((evt, idx) => setEventActiveTab(idx), [setEventActiveTab]); @@ -90,22 +90,29 @@ export const Events: React.FunctionComponent = (props) => { - {enabled && ( - - - - - - - - - - - - - - - )} + + + + + + + + + ) + } + > + + + + + + From b234ed71b62fae678ff50606f5fc2878c2d8cb18 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 24 Oct 2022 16:38:42 -0400 Subject: [PATCH 31/43] Refresh probes table when receiving a removal notification, adjust tests accordingly --- src/app/Agent/AgentLiveProbes.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index a17fc95c8..65312a524 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -190,6 +190,12 @@ export const AgentLiveProbes: React.FunctionComponent = (p ); }, [addSubscription, context, context.notificationChannel, setProbes]); + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((v) => refreshProbes()) + ); + }, [addSubscription, context, context.notificationChannel, setProbes]); + React.useEffect(() => { let filtered: EventProbe[]; if (!filterText) { From b41291c964a3db2056dba607a9b1e1e0b39c2f86 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 16:40:53 -0400 Subject: [PATCH 32/43] correct hook dep --- src/app/Agent/AgentLiveProbes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 65312a524..f7a75a985 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -194,7 +194,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p addSubscription( context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((v) => refreshProbes()) ); - }, [addSubscription, context, context.notificationChannel, setProbes]); + }, [addSubscription, context, context.notificationChannel, refreshProbes]); React.useEffect(() => { let filtered: EventProbe[]; From a2cc9589c047b83e5d7c9c326714ad647b2c7b40 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 16:43:09 -0400 Subject: [PATCH 33/43] don't re-query after successful deletion request, allow notification to prompt client-side model update --- src/app/Agent/AgentLiveProbes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index f7a75a985..1f1913674 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -137,7 +137,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p .removeProbes() .pipe(first()) .subscribe(() => { - refreshProbes(); + // do nothing - notification updates state }) ); }, [addSubscription, context.api, refreshProbes]); From 6045b0b5f5618739ca95b5d2198de4c41c548273 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 16:43:21 -0400 Subject: [PATCH 34/43] skip query on notification, just clear client-side model --- src/app/Agent/AgentLiveProbes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 1f1913674..0d3954ac3 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -192,9 +192,9 @@ export const AgentLiveProbes: React.FunctionComponent = (p React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((v) => refreshProbes()) + context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((v) => setProbes([])) ); - }, [addSubscription, context, context.notificationChannel, refreshProbes]); + }, [addSubscription, context, context.notificationChannel, setProbes]); React.useEffect(() => { let filtered: EventProbe[]; From 65fb096c5d74d3165acf2776203562a88bb35a60 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 17:07:47 -0400 Subject: [PATCH 35/43] test correction --- src/app/Agent/AgentLiveProbes.tsx | 4 ++-- src/test/Agent/AgentLiveProbes.test.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 0d3954ac3..aa815f676 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -186,13 +186,13 @@ export const AgentLiveProbes: React.FunctionComponent = (p React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied).subscribe((v) => refreshProbes()) + context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied).subscribe((_) => refreshProbes()) ); }, [addSubscription, context, context.notificationChannel, setProbes]); React.useEffect(() => { addSubscription( - context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((v) => setProbes([])) + context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((_) => setProbes([])) ); }, [addSubscription, context, context.notificationChannel, setProbes]); diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index afdd8c691..1143de9d1 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -109,6 +109,7 @@ jest jest .spyOn(defaultServices.notificationChannel, 'messages') .mockReturnValueOnce(of()) // renders correctly + .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockApplyTemplateNotification)) // should add a probe after receiving a notification .mockReturnValue(of()); // All other tests From 1c589132d549a7e056f96e3780cf2c06698e334f Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 24 Oct 2022 17:29:59 -0400 Subject: [PATCH 36/43] Actually commit tests this time --- src/test/Agent/AgentLiveProbes.test.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index 1143de9d1..aa4430041 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -89,6 +89,16 @@ const mockApplyTemplateNotification = { }, } as NotificationMessage; +const mockRemoveProbesNotification = { + meta: { + category: NotificationCategory.ProbesRemoved, + type: mockMessageType, + } as MessageMeta, + message: { + target: mockTarget, + }, +} as NotificationMessage; + jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); @@ -111,6 +121,8 @@ jest .mockReturnValueOnce(of()) // renders correctly .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockApplyTemplateNotification)) // should add a probe after receiving a notification + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockRemoveProbesNotification)) .mockReturnValue(of()); // All other tests describe('', () => { @@ -140,6 +152,15 @@ describe('', () => { expect(addTemplateName).toBeVisible(); }); + it('should remove a probe after receiving a notification', () => { + render( + + + + ); + expect(screen.queryByText('another_name')).not.toBeInTheDocument(); + }); + it('should display the column header fields', () => { render( From 1d1c0345a013a3bcfbe6720742dc255b97804196 Mon Sep 17 00:00:00 2001 From: Joshua Matsuoka Date: Mon, 24 Oct 2022 17:47:57 -0400 Subject: [PATCH 37/43] Fix test mocks, adjust test to check for both probes, fix dependency array --- src/app/Agent/AgentLiveProbes.tsx | 2 +- src/test/Agent/AgentLiveProbes.test.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index aa815f676..45edbf5f9 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -188,7 +188,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p addSubscription( context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied).subscribe((_) => refreshProbes()) ); - }, [addSubscription, context, context.notificationChannel, setProbes]); + }, [addSubscription, context, context.notificationChannel, refreshProbes]); React.useEffect(() => { addSubscription( diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index aa4430041..b53bb889c 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -120,9 +120,13 @@ jest .spyOn(defaultServices.notificationChannel, 'messages') .mockReturnValueOnce(of()) // renders correctly .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockApplyTemplateNotification)) // should add a probe after receiving a notification .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockRemoveProbesNotification)) + + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockRemoveProbesNotification)) // should remove a probe after receiving a notification + .mockReturnValue(of()); // All other tests describe('', () => { @@ -152,12 +156,13 @@ describe('', () => { expect(addTemplateName).toBeVisible(); }); - it('should remove a probe after receiving a notification', () => { + it('should remove all probes after receiving a notification', () => { render( ); + expect(screen.queryByText('some_name')).not.toBeInTheDocument(); expect(screen.queryByText('another_name')).not.toBeInTheDocument(); }); From 23aba8f1e5704e07087c934ff46986cbcdece0b0 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 24 Oct 2022 18:26:41 -0400 Subject: [PATCH 38/43] fix(agent): disable remove button if no probes --- src/app/Agent/AgentLiveProbes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 45edbf5f9..b4b69bf46 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -273,7 +273,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p - From d2d00eb5bf1d8ac52fca2210ca670499d3dd3aa2 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 24 Oct 2022 18:42:40 -0400 Subject: [PATCH 39/43] fix(agent): add more tests --- src/app/Agent/AgentLiveProbes.tsx | 2 +- src/test/Agent/AgentLiveProbes.test.tsx | 28 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index b4b69bf46..903a13e2a 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -140,7 +140,7 @@ export const AgentLiveProbes: React.FunctionComponent = (p // do nothing - notification updates state }) ); - }, [addSubscription, context.api, refreshProbes]); + }, [addSubscription, context.api]); const handleWarningModalAccept = React.useCallback(() => handleDeleteAllProbes(), [handleDeleteAllProbes]); diff --git a/src/test/Agent/AgentLiveProbes.test.tsx b/src/test/Agent/AgentLiveProbes.test.tsx index b53bb889c..cfbfe5d84 100644 --- a/src/test/Agent/AgentLiveProbes.test.tsx +++ b/src/test/Agent/AgentLiveProbes.test.tsx @@ -37,7 +37,7 @@ */ import * as React from 'react'; import renderer, { act } from 'react-test-renderer'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import { of } from 'rxjs'; import { EventProbe } from '@app/Shared/Services/Api.service'; @@ -111,6 +111,8 @@ jest .spyOn(defaultServices.api, 'getActiveProbes') .mockReturnValueOnce(of([mockProbe])) // renders correctly + .mockReturnValueOnce(of([])) // should disable remove button if there is no probe + .mockReturnValueOnce(of([mockProbe])) // should add a probe after receiving a notification .mockReturnValueOnce(of([mockProbe, mockAnotherProbe])) @@ -121,6 +123,9 @@ jest .mockReturnValueOnce(of()) // renders correctly .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // should disable remove button if there is no probe + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockApplyTemplateNotification)) // should add a probe after receiving a notification .mockReturnValueOnce(of()) @@ -144,6 +149,19 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); + it('should disable remove button if there is no probe', async () => { + render( + + + + ); + + const removeButton = screen.getByText('Remove All Probes'); + expect(removeButton).toBeInTheDocument(); + expect(removeButton).toBeVisible(); + expect(removeButton).toBeDisabled(); + }); + it('should add a probe after receiving a notification', () => { render( @@ -181,7 +199,7 @@ describe('', () => { }); }); - it('should remove all probes when Remove All Probe is clicked', () => { + it('should remove all probes when Remove All Probe is clicked', async () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); render( @@ -191,14 +209,14 @@ describe('', () => { const removeButton = screen.getByText('Remove All Probes'); expect(removeButton).toBeInTheDocument(); - expect(removeButton).toBeInTheDocument(); + expect(removeButton).toBeVisible(); userEvent.click(removeButton); expect(deleteRequestSpy).toBeCalledTimes(1); }); - it.skip('should show warning modal and remove all probes when confirmed', async () => { + it('should show warning modal and remove all probes when confirmed', async () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'removeProbes').mockReturnValue(of(true)); render( @@ -208,7 +226,7 @@ describe('', () => { const removeButton = screen.getByText('Remove All Probes'); expect(removeButton).toBeInTheDocument(); - expect(removeButton).toBeInTheDocument(); + expect(removeButton).toBeVisible(); userEvent.click(removeButton); From 4a48bd5f5dd86d91ce3a8e6e575c4c8eae61d9dd Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 24 Oct 2022 18:44:24 -0400 Subject: [PATCH 40/43] chore(agent): apply prettier --- src/app/Agent/AgentLiveProbes.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index 903a13e2a..e2f4ee21c 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -273,7 +273,12 @@ export const AgentLiveProbes: React.FunctionComponent = (p - From 0246a64273e8c076feb0a55584475a1ca808741d Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 24 Oct 2022 19:53:31 -0400 Subject: [PATCH 41/43] disable probe insertion button if agent not detected --- src/app/Agent/AgentProbeTemplates.tsx | 7 +++++-- src/app/Events/Events.tsx | 2 +- src/test/Agent/AgentProbeTemplates.test.tsx | 16 ++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 694f493ca..c4753879b 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -81,7 +81,9 @@ import { ProbeTemplate } from '@app/Shared/Services/Api.service'; import { DeleteWarningType } from '@app/Modal/DeleteWarningUtils'; import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal'; -export interface AgentProbeTemplatesProps {} +export interface AgentProbeTemplatesProps { + agentDetected: boolean; +} export const AgentProbeTemplates: React.FunctionComponent = (props) => { const context = React.useContext(ServiceContext); @@ -201,6 +203,7 @@ export const AgentProbeTemplates: React.FunctionComponent handleInsert(rowData), + isDisabled: !props.agentDetected, }, { isSeparator: true, @@ -211,7 +214,7 @@ export const AgentProbeTemplates: React.FunctionComponent { diff --git a/src/app/Events/Events.tsx b/src/app/Events/Events.tsx index 8c6531323..2ba4b4742 100644 --- a/src/app/Events/Events.tsx +++ b/src/app/Events/Events.tsx @@ -95,7 +95,7 @@ export const Events: React.FunctionComponent = (props) => { - + ', () => { await act(async () => { tree = renderer.create( - + ); }); @@ -137,7 +137,7 @@ describe('', () => { it('should add a probe template after receiving a notification', () => { render( - + ); @@ -149,7 +149,7 @@ describe('', () => { it('should remove a probe template after receiving a notification', () => { render( - + ); expect(screen.queryByText('someProbeTemplate')).not.toBeInTheDocument(); @@ -158,7 +158,7 @@ describe('', () => { it('should display the column header fields', () => { render( - + ); const nameHeader = screen.getByText('Name'); @@ -173,7 +173,7 @@ describe('', () => { it('should show modal when uploading', async () => { render( - + ); @@ -195,7 +195,7 @@ describe('', () => { it('should upload a probe template when form is filled and Submit is clicked', async () => { render( - + ); @@ -247,7 +247,7 @@ describe('', () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); render( - + ); @@ -267,7 +267,7 @@ describe('', () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteCustomProbeTemplate').mockReturnValue(of(true)); render( - + ); From fcfcd732e2ed5c37aaf9030837b0d6330c884f8e Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 25 Oct 2022 11:56:35 -0400 Subject: [PATCH 42/43] tests(agent): add tests for inserting probes --- src/test/Agent/AgentProbeTemplates.test.tsx | 38 +++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/test/Agent/AgentProbeTemplates.test.tsx b/src/test/Agent/AgentProbeTemplates.test.tsx index 171cf1d25..38a4538d1 100644 --- a/src/test/Agent/AgentProbeTemplates.test.tsx +++ b/src/test/Agent/AgentProbeTemplates.test.tsx @@ -39,7 +39,7 @@ import * as React from 'react'; import renderer, { act } from 'react-test-renderer'; import { act as doAct, cleanup, render, screen, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { of } from 'rxjs'; +import { async, of } from 'rxjs'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; import { MessageMeta, @@ -92,7 +92,6 @@ jest .mockReturnValue(true); // should show warning modal and delete a probe template when confirmed const uploadRequestSpy = jest.spyOn(defaultServices.api, 'addCustomProbeTemplate').mockReturnValue(of(true)); -jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); jest .spyOn(defaultServices.api, 'getProbeTemplates') @@ -296,4 +295,39 @@ describe('', () => { expect(deleteRequestSpy).toHaveBeenCalledTimes(1); expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); }); + + it('should insert probes if agent is enabled', async () => { + const insertProbesSpy = jest.spyOn(defaultServices.api, 'insertProbes').mockReturnValue(of(true)); + render( + + + + ); + + userEvent.click(screen.getByLabelText('Actions')); + + const insertButton = await screen.findByText('Insert Probes...'); + expect(insertButton).toBeInTheDocument(); + expect(insertButton).toBeVisible(); + expect(insertButton.getAttribute('aria-disabled')).toBe('false'); + + userEvent.click(insertButton); + + expect(insertProbesSpy).toHaveBeenCalledTimes(1); + expect(insertProbesSpy).toHaveBeenCalledWith(mockProbeTemplate.name); + }); + + it('should disable inserting probes if agent is not enabled', async () => { + render( + + + + ); + userEvent.click(screen.getByLabelText('Actions')); + + const insertButton = await screen.findByText('Insert Probes...'); + expect(insertButton).toBeInTheDocument(); + expect(insertButton).toBeVisible(); + expect(insertButton.getAttribute('aria-disabled')).toBe('true'); + }); }); From 5624cfb6df1454548b306c65aedac5a10178b47c Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 25 Oct 2022 11:57:57 -0400 Subject: [PATCH 43/43] chore(agent): remove unused imports --- src/test/Agent/AgentProbeTemplates.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/Agent/AgentProbeTemplates.test.tsx b/src/test/Agent/AgentProbeTemplates.test.tsx index 38a4538d1..df68e772b 100644 --- a/src/test/Agent/AgentProbeTemplates.test.tsx +++ b/src/test/Agent/AgentProbeTemplates.test.tsx @@ -39,7 +39,7 @@ import * as React from 'react'; import renderer, { act } from 'react-test-renderer'; import { act as doAct, cleanup, render, screen, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { async, of } from 'rxjs'; +import { of } from 'rxjs'; import { ProbeTemplate } from '@app/Shared/Services/Api.service'; import { MessageMeta,