Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jmc-agent): add JMC Agent probes UI #558

Merged
merged 46 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
850fad4
Agent Plugin Changes
Josh-Matsuoka Aug 15, 2022
aff2c4f
tmp
Josh-Matsuoka Aug 15, 2022
acd72c3
Syncing with upstream
Josh-Matsuoka Aug 15, 2022
9776154
Fix refreshing of ProbeTemplate table, add notifications
Josh-Matsuoka Oct 12, 2022
e9a53db
Adding Agent Tests
Josh-Matsuoka Oct 12, 2022
d39a8d7
Merging changes from upstream
Josh-Matsuoka Oct 13, 2022
8a5a5b3
Fix ErrorView and AuthRetry
Josh-Matsuoka Oct 13, 2022
960ef73
Merge remote-tracking branch 'upstream/main' into agent-plugin
Josh-Matsuoka Oct 19, 2022
fbfb31b
Fix Agent Probe Templates page tests, fix probe templates table, add …
Josh-Matsuoka Oct 19, 2022
cd26d77
Fixing Agent Live Probes page to work with new API implementation
Josh-Matsuoka Oct 20, 2022
85bcccc
Backing out unnecessary package.json change
Josh-Matsuoka Oct 20, 2022
6d48d48
Running prettier to fix formatting
Josh-Matsuoka Oct 20, 2022
898d6ea
Removing extraneous change
Josh-Matsuoka Oct 20, 2022
bc06912
Removing unnecessary change
Josh-Matsuoka Oct 20, 2022
a6ffd94
Fixing deletion warning and notifications
Josh-Matsuoka Oct 20, 2022
2db1ec6
Moving Agent items under Events page, adding hints to error message w…
Josh-Matsuoka Oct 20, 2022
beb17ec
Moving Agent help into agent components, adding notification for prob…
Josh-Matsuoka Oct 20, 2022
ea4abb5
Remove duplicate notification
Josh-Matsuoka Oct 20, 2022
ea970a6
Add error hint for Probetemplate component
Josh-Matsuoka Oct 21, 2022
6e3c9fd
clean up, formatting
andrewazores Oct 21, 2022
ad90878
add parameter to suppress graphical notification on error
andrewazores Oct 21, 2022
52054af
test for and only display JMC Agent card if support is detected
andrewazores Oct 21, 2022
8f083ff
fixup! test for and only display JMC Agent card if support is detected
andrewazores Oct 21, 2022
a74db43
apply prettier
andrewazores Oct 21, 2022
d991616
chore(events): clean up api calls and use separate state for 2 tables
tthvo Oct 21, 2022
8f8e1e9
chore(live-probes): clean up live probe views
tthvo Oct 22, 2022
582eb6b
feat(agent): add delete modal for active probe view
tthvo Oct 22, 2022
2048604
chore(agent): clean up probe template view
tthvo Oct 22, 2022
7e4d03f
chore(agent): continue to clean up
tthvo Oct 24, 2022
13baf3b
tests(agent): fix tests
tthvo Oct 24, 2022
1d86c1a
Suppress notifications on refreshing probes
Josh-Matsuoka Oct 24, 2022
cf62bf3
Fix notification handling for removing probes
Josh-Matsuoka Oct 24, 2022
96ca5f6
leave card visible but disable tab if Agent not detected
andrewazores Oct 24, 2022
b234ed7
Refresh probes table when receiving a removal notification, adjust te…
Josh-Matsuoka Oct 24, 2022
b41291c
correct hook dep
andrewazores Oct 24, 2022
a2cc958
don't re-query after successful deletion request, allow notification …
andrewazores Oct 24, 2022
6045b0b
skip query on notification, just clear client-side model
andrewazores Oct 24, 2022
65fb096
test correction
andrewazores Oct 24, 2022
1c58913
Actually commit tests this time
Josh-Matsuoka Oct 24, 2022
1d1c034
Fix test mocks, adjust test to check for both probes, fix dependency …
Josh-Matsuoka Oct 24, 2022
23aba8f
fix(agent): disable remove button if no probes
tthvo Oct 24, 2022
d2d00eb
fix(agent): add more tests
tthvo Oct 24, 2022
4a48bd5
chore(agent): apply prettier
tthvo Oct 24, 2022
0246a64
disable probe insertion button if agent not detected
andrewazores Oct 24, 2022
fcfcd73
tests(agent): add tests for inserting probes
tthvo Oct 25, 2022
5624cfb
chore(agent): remove unused imports
tthvo Oct 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions src/app/Agent/AgentLiveProbes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
/*
* 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 { useSubscriptions } from '@app/utils/useSubscriptions';
import {
Button,
Card,
CardBody,
CardHeaderMain,
CardHeader,
Toolbar,
ToolbarContent,
ToolbarGroup,
ToolbarItem,
TextInput,
Text,
TextVariants,
Stack,
StackItem,
} from '@patternfly/react-core';
import {
Table,
TableBody,
TableHeader,
TableVariant,
ISortBy,
SortByDirection,
sortable,
} from '@patternfly/react-table';
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 interface AgentLiveProbesProps {}

export const AgentLiveProbes: React.FunctionComponent<AgentLiveProbesProps> = (props) => {
const context = React.useContext(ServiceContext);
const addSubscription = useSubscriptions();

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);
const [errorMessage, setErrorMessage] = React.useState('');
const [warningModalOpen, setWarningModalOpen] = React.useState(false);

const tableColumns = [
{ title: 'ID', transforms: [sortable] },
{ title: 'Name', transforms: [sortable] },
{ title: 'Class', transforms: [sortable] },
{ title: 'Description' },
{ title: 'Method', transforms: [sortable] },
];

const handleProbes = React.useCallback(
(probes) => {
setProbes(probes);
setErrorMessage('');
setIsLoading(false);
},
[setProbes, setIsLoading, setErrorMessage]
);

const handleError = React.useCallback(
(error) => {
setErrorMessage(error.message);
setIsLoading(false);
},
[setIsLoading, setErrorMessage]
);

const refreshProbes = React.useCallback(() => {
setIsLoading(true);
addSubscription(
context.api.getActiveProbes(true).subscribe({
next: (value) => handleProbes(value),
error: (err) => handleError(err),
})
);
}, [addSubscription, context.api, setIsLoading, handleProbes, 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
.removeProbes()
.pipe(first())
.subscribe(() => {
// do nothing - notification updates state
})
);
}, [addSubscription, context.api]);

const handleWarningModalAccept = React.useCallback(() => handleDeleteAllProbes(), [handleDeleteAllProbes]);

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(() => {
addSubscription(
context.target.target().subscribe(() => {
setFilterText('');
refreshProbes();
})
);
}, [context, context.target, addSubscription, setFilterText, refreshProbes]);

React.useEffect(() => {
if (!context.settings.autoRefreshEnabled()) {
return;
}
const id = window.setInterval(
() => refreshProbes(),
context.settings.autoRefreshPeriod() * context.settings.autoRefreshUnits()
);
return () => window.clearInterval(id);
}, [context.settings, refreshProbes]);

React.useEffect(() => {
addSubscription(
context.target.authFailure().subscribe(() => {
setErrorMessage(authFailMessage);
})
);
}, [addSubscription, context.target, setErrorMessage]);

React.useEffect(() => {
addSubscription(
context.notificationChannel.messages(NotificationCategory.ProbeTemplateApplied).subscribe((_) => refreshProbes())
);
}, [addSubscription, context, context.notificationChannel, refreshProbes]);

React.useEffect(() => {
addSubscription(
context.notificationChannel.messages(NotificationCategory.ProbesRemoved).subscribe((_) => setProbes([]))
);
}, [addSubscription, context, context.notificationChannel, setProbes]);

React.useEffect(() => {
let filtered: EventProbe[];
if (!filterText) {
filtered = probes;
} else {
const ft = filterText.trim().toLowerCase();
filtered = probes.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();
}
setFilteredProbes([...filtered]);
}, [filterText, probes, sortBy, setFilteredProbes]);

const displayTemplates = React.useMemo(
() => filteredProbes.map((t: EventProbe) => [t.id, t.name, t.clazz, t.description, t.methodName]),
[filteredProbes]
);

if (errorMessage != '') {
return (
<ErrorView
title={'Error retrieving active probes'}
message={`${errorMessage}. Note: This is commonly caused by the agent not being loaded/active, check that the target was started with the agent (-javaagent:/path/to/agent.jar).`}
retry={isAuthFail(errorMessage) ? authRetry : undefined}
/>
);
} else if (isLoading) {
return <LoadingView />;
} else {
return (
<>
<Stack hasGutter style={{ marginTop: '1em' }}>
<StackItem>
<Card>
<CardHeader>
<CardHeaderMain>
<Text component={TextVariants.h4}>About the JMC Agent</Text>
</CardHeaderMain>
</CardHeader>
<CardBody>
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.
</CardBody>
</Card>
</StackItem>
<StackItem>
<Toolbar id="active-probes-toolbar">
<ToolbarContent>
<ToolbarGroup variant="filter-group">
<ToolbarItem>
<TextInput
name="templateFilter"
id="templateFilter"
type="search"
placeholder="Filter..."
aria-label="Probe template filter"
onChange={setFilterText}
/>
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup variant="icon-button-group">
<ToolbarItem>
<Button
key="delete"
variant="danger"
onClick={handleDeleteButton}
isDisabled={!filteredProbes.length}
>
Remove All Probes
</Button>
</ToolbarItem>
</ToolbarGroup>
</ToolbarContent>
<DeleteWarningModal
warningType={DeleteWarningType.DeleteActiveProbes}
visible={warningModalOpen}
onAccept={handleWarningModalAccept}
onClose={handleWarningModalClose}
/>
</Toolbar>
<Table
aria-label="Active Probes"
variant={TableVariant.compact}
cells={tableColumns}
rows={displayTemplates}
onSort={handleSort}
>
<TableHeader />
<TableBody />
</Table>
</StackItem>
</Stack>
</>
);
}
};
Loading