From 0018bc54f5c99b4a67d7c0670ada637f0a0356eb Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 25 Feb 2022 20:14:49 +0000 Subject: [PATCH] feat(rules): add match expression evaluation component to rule creation form (#381) * feat(rules): add match expression evaluation component to rule creation form * rename component * remove unused prop * reword label * remove surrounding quotes from example expression * add header/title to evaluator card * add validation result to matchExpression form field * update label icons to match field validation icons --- src/app/Dashboard/Dashboard.tsx | 14 +- src/app/Rules/CreateRule.tsx | 374 +++++++++++---------- src/app/Rules/MatchExpressionEvaluator.tsx | 150 +++++++++ src/app/Shared/Services/Target.service.tsx | 5 + src/app/TargetSelect/TargetSelect.tsx | 131 ++++---- src/app/TargetView/TargetView.tsx | 13 +- 6 files changed, 435 insertions(+), 252 deletions(-) create mode 100644 src/app/Rules/MatchExpressionEvaluator.tsx diff --git a/src/app/Dashboard/Dashboard.tsx b/src/app/Dashboard/Dashboard.tsx index d111ed502..66ca64b6d 100644 --- a/src/app/Dashboard/Dashboard.tsx +++ b/src/app/Dashboard/Dashboard.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 @@ -41,7 +41,7 @@ import { TargetView } from '@app/TargetView/TargetView'; export const Dashboard = () => { return ( - + ); } diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index 03c6a1351..f4beb36dd 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -36,7 +36,7 @@ * SOFTWARE. */ import * as React from 'react'; -import { ActionGroup, Button, Card, CardBody, Form, FormGroup, FormSelect, FormSelectOption, FormSelectOptionGroup, Split, SplitItem, Text, TextInput, TextVariants, ValidatedOptions } from '@patternfly/react-core'; +import { ActionGroup, Button, Card, CardBody, CardHeader, CardHeaderMain, Form, FormGroup, FormSelect, FormSelectOption, Grid, GridItem, Split, SplitItem, Text, TextInput, TextVariants, ValidatedOptions } from '@patternfly/react-core'; import { useHistory, withRouter } from 'react-router-dom'; import { first } from 'rxjs/operators'; import { ServiceContext } from '@app/Shared/Services/Services'; @@ -45,6 +45,7 @@ import { BreadcrumbPage, BreadcrumbTrail } from '@app/BreadcrumbPage/BreadcrumbP import { useSubscriptions } from '@app/utils/useSubscriptions'; import { EventTemplate } from '../CreateRecording/CreateRecording'; import { Rule } from './Rules'; +import { MatchExpressionEvaluator } from './MatchExpressionEvaluator'; import { FormSelectTemplateSelector } from '../TemplateSelector/FormSelectTemplateSelector'; // FIXME check if this is correct/matches backend name validation @@ -59,8 +60,8 @@ const Comp = () => { const [name, setName] = React.useState(''); const [nameValid, setNameValid] = React.useState(ValidatedOptions.default); const [description, setDescription] = React.useState(''); - // TODO validate and evaluate match expressions const [matchExpression, setMatchExpression] = React.useState(''); + const [matchExpressionValid, setMatchExpressionValid] = React.useState(ValidatedOptions.default); const [templates, setTemplates] = React.useState([] as EventTemplate[]); const [template, setTemplate] = React.useState(null as string | null); const [templateType, setTemplateType] = React.useState(null as string | null); @@ -170,186 +171,211 @@ const Comp = () => { return ( - - -
- - Automated Rules are configurations that instruct Cryostat to create JDK Flight Recordings on matching - target JVM applications. Each Automated Rule specifies parameters for which Event Template to use, how - much data should be kept in the application recording buffer, and how frequently Cryostat should copy the - application recording buffer into Cryostat's own archived storage. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + Automated Rules are configurations that instruct Cryostat to create JDK Flight Recordings on matching + target JVM applications. Each Automated Rule specifies parameters for which Event Template to use, how + much data should be kept in the application recording buffer, and how frequently Cryostat should copy the + application recording buffer into Cryostat's own archived storage. + + + + + + + + Enter a match expression. This is a Java-like code snippet that is evaluated against each target application to determine whether the rule should be applied. + Select a target from the dropdown on the right to view the context object available within the match expression context and test if the expression matches. + + } + validated={matchExpressionValid} + > + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - -
-
-
+ + + + + + + + + + + + + + + Match Expression Evaluator + + + + + + + + +
); }; diff --git a/src/app/Rules/MatchExpressionEvaluator.tsx b/src/app/Rules/MatchExpressionEvaluator.tsx new file mode 100644 index 000000000..c0e8fdc4a --- /dev/null +++ b/src/app/Rules/MatchExpressionEvaluator.tsx @@ -0,0 +1,150 @@ +/* + * 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 { CodeBlock, CodeBlockCode, Label, Stack, StackItem, Text, ValidatedOptions } from '@patternfly/react-core'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/useSubscriptions'; +import { Target } from '@app/Shared/Services/Target.service'; +import { TargetSelect } from '@app/TargetSelect/TargetSelect'; +import { NoTargetSelected } from '@app/TargetView/NoTargetSelected'; +import {CheckCircleIcon, ExclamationCircleIcon, InfoCircleIcon, WarningTriangleIcon} from '@patternfly/react-icons'; + +export interface MatchExpressionEvaluatorProps { + matchExpression?: string; + onChange?: (validated: ValidatedOptions) => void; +} + +export const MatchExpressionEvaluator: React.FunctionComponent = (props) => { + const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); + const [target, setTarget] = React.useState(undefined as Target | undefined); + const [valid, setValid] = React.useState(ValidatedOptions.default); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe(setTarget) + ); + }, [context, context.target]); + + React.useEffect(() => { + if (!props.matchExpression || !target?.connectUrl) { + setValid(ValidatedOptions.default); + return; + } + try { + const f = new Function('target', `return ${props.matchExpression}`); + const res = f(target); + if (typeof res === 'boolean') { + setValid(res ? ValidatedOptions.success : ValidatedOptions.warning); + return; + } + setValid(ValidatedOptions.error); + return; + } catch (err) { + setValid(ValidatedOptions.error); + return; + } + }, [target, props.matchExpression]); + + React.useEffect(() => { + if (!!props.onChange) { + props.onChange(valid); + } + }, [props.onChange, valid]); + + const statusLabel = React.useMemo(() => { + switch (valid) { + case ValidatedOptions.success: + return (); + case ValidatedOptions.warning: + return (); + case ValidatedOptions.error: + return (); + default: + return (); + } + }, [valid]); + + const exampleExpression = React.useMemo(() => { + let body: string; + if (!target || !target?.alias || !target?.connectUrl) { + body = 'true'; + } else { + body = `target.alias == '${target?.alias}' || target.annotations.cryostat['PORT'] == ${target?.annotations?.cryostat['PORT']}`; + } + body = JSON.stringify(body, null, 2); + body = body.substring(1, body.length - 1); + return (<> + + + { body } + + + ); + },[target]); + + return (<> + + + + + + { statusLabel } + + + + Hint: try an expression like + + { exampleExpression } + + + { + !!target?.alias && !!target?.connectUrl ? + + + { JSON.stringify(target, null, 2) } + + + : + + } + + + ); + +}; diff --git a/src/app/Shared/Services/Target.service.tsx b/src/app/Shared/Services/Target.service.tsx index eb496a905..93eda5d71 100644 --- a/src/app/Shared/Services/Target.service.tsx +++ b/src/app/Shared/Services/Target.service.tsx @@ -42,6 +42,11 @@ export const NO_TARGET = {} as Target; export interface Target { connectUrl: string; alias: string; + labels?: Map; + annotations?: { + cryostat: Map; + platform: Map; + } } class TargetService { diff --git a/src/app/TargetSelect/TargetSelect.tsx b/src/app/TargetSelect/TargetSelect.tsx index 4865d556a..bdd65a8a7 100644 --- a/src/app/TargetSelect/TargetSelect.tsx +++ b/src/app/TargetSelect/TargetSelect.tsx @@ -40,9 +40,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { NotificationsContext } from '@app/Notifications/Notifications'; import { NO_TARGET, Target } from '@app/Shared/Services/Target.service'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { Button, Card, CardActions, CardBody, CardHeader, CardHeaderMain, Grid, - GridItem, Select, SelectOption, SelectVariant, Text, TextVariants -} from '@patternfly/react-core'; +import { Button, Card, CardActions, CardBody, CardHeader, CardHeaderMain, Select, SelectOption, SelectVariant, Text, TextVariants } from '@patternfly/react-core'; import { ContainerNodeIcon, PlusCircleIcon, Spinner2Icon, TrashIcon } from '@patternfly/react-icons'; import { of } from 'rxjs'; import { catchError, first } from 'rxjs/operators'; @@ -51,7 +49,6 @@ import { CreateTargetModal } from './CreateTargetModal'; import _ from 'lodash'; export interface TargetSelectProps { - isCompact?: boolean; } export const TargetSelect: React.FunctionComponent = (props) => { @@ -190,71 +187,67 @@ export const TargetSelect: React.FunctionComponent = (props) }, [context.api, selected, setLoading, addSubscription, notifications, selectNone]); return (<> - - - - - - - Target JVM - - - -