diff --git a/locales/en/public.json b/locales/en/public.json
index dd9e8b88d..873a51481 100644
--- a/locales/en/public.json
+++ b/locales/en/public.json
@@ -865,7 +865,9 @@
},
"DEFINITION_HELPER_TEXT": "Enter a Smart Trigger definition. A custom trigger definition must consist of both an expression that defines the overall trigger condition and the name of an event template that is used for the JFR Recording. The expression is a Common Expression Language (CEL) code snippet that defines one or more constraints and a target duration.",
"DEFINITION_HINT": "The set of constraints and target duration must be separated by a semicolon (;) character. Each constraint must include: the name of an MBean counter; a relational operator such as > (greater than), = (equal to), <(less than), and so on; and a specified value. The type of relational operator and value that you can specify depends on the associated MBean counter type. The constraint and duration must be enclosed in square brackets, followed by a tilde then the Recording Template name.",
- "DEFINITION_HINT_BODY": "For an example definition: [ProcessCpuLoad>0.2;TargetDuration>duration(\"30s\")]~Profiling"
+ "DEFINITION_HINT_BODY": "For an example definition: [ProcessCpuLoad>0.2;TargetDuration>duration(\"30s\")]",
+ "TEMPLATE_SELECT": "Select an Event Template for starting a recording when the Trigger Condition is met.",
+ "AVAILABLE_MBEANS": "Select an MBean to check the value of."
},
"AuditLog": {
"TITLE": "Audit Log",
diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx
index 0e11c6be8..494f58419 100644
--- a/src/app/Shared/Services/Api.service.tsx
+++ b/src/app/Shared/Services/Api.service.tsx
@@ -76,6 +76,7 @@ import {
AuditQueryParams,
AuditRevisionsResponse,
AuditRevisionDetail,
+ MbeanAttributeMap,
} from './api.types';
import {
isHttpError,
@@ -2082,6 +2083,20 @@ export class ApiService {
);
}
+ getTargetMbeans(
+ target: TargetStub,
+ suppressNotifications = false,
+ skipStatusCheck = false,
+ ): Observable {
+ return this.doGet(
+ `targets/${target.id}/mbean-query`,
+ 'beta',
+ undefined,
+ suppressNotifications,
+ skipStatusCheck,
+ );
+ }
+
getTargetEventTypes(
target: TargetStub,
suppressNotifications = false,
diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts
index 58f3a6282..ce5845654 100644
--- a/src/app/Shared/Services/api.types.ts
+++ b/src/app/Shared/Services/api.types.ts
@@ -445,6 +445,21 @@ export interface SmartTrigger {
timeConditionFirstMet: string;
}
+export interface MBeanAttributeInfo {
+ name: string;
+ type: string;
+ description: string;
+ parentBean: string;
+ isReadable: boolean;
+ isWritable: boolean;
+ isIs: boolean;
+}
+
+export interface MbeanAttributeMap {
+ mBeanName: string;
+ attributes: MBeanAttributeInfo[];
+}
+
// ======================================
// Template resources
// ======================================
diff --git a/src/app/Triggers/SmartTriggers.tsx b/src/app/Triggers/SmartTriggers.tsx
index 7f4cd9cea..15ca51d31 100644
--- a/src/app/Triggers/SmartTriggers.tsx
+++ b/src/app/Triggers/SmartTriggers.tsx
@@ -13,13 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { EventTemplateIdentifier } from '@app/CreateRecording/types';
import { ColumnConfig, DiagnosticsTable } from '@app/Diagnostics/DiagnosticsTable';
import { DeleteWarningModal } from '@app/Modal/DeleteWarningModal';
import { DeleteOrDisableWarningType } from '@app/Modal/types';
import { CEL_SPEC_HREF } from '@app/Rules/utils';
+import { SelectTemplateSelectorForm } from '@app/Shared/Components/SelectTemplateSelectorForm';
import { LoadingProps } from '@app/Shared/Components/types';
-import { NotificationCategory, NullableTarget, SmartTrigger } from '@app/Shared/Services/api.types';
+import {
+ EventTemplate,
+ MBeanAttributeInfo,
+ MbeanAttributeMap,
+ NotificationCategory,
+ NullableTarget,
+ SmartTrigger,
+ Target,
+} from '@app/Shared/Services/api.types';
import { ServiceContext } from '@app/Shared/Services/Services';
import { useSubscriptions } from '@app/utils/hooks/useSubscriptions';
import { TableColumn, hashCode, portalRoot, sortResources } from '@app/utils/utils';
@@ -52,6 +62,8 @@ import {
FormHelperText,
HelperText,
HelperTextItem,
+ FormSelect,
+ FormSelectOption,
} from '@patternfly/react-core';
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
import { EllipsisVIcon, SearchIcon } from '@patternfly/react-icons';
@@ -61,6 +73,7 @@ import _ from 'lodash';
import * as React from 'react';
import { Trans } from 'react-i18next';
import { first, forkJoin, Observable } from 'rxjs';
+import { SmartTriggersFormData } from './types';
export const tableColumns: TableColumn[] = [
{
@@ -633,16 +646,70 @@ export interface CreateSmartTriggersModalProps {
onAccept: (s: string) => void;
}
+export interface MbeanSelectorOption {
+ value: string;
+ label: string;
+ disabled: boolean;
+}
+
export const CreateSmartTriggersModal: React.FC = ({ onClose, ...props }) => {
const submitRef = React.useRef(null); // Use ref to refer to submit trigger div
const abortRef = React.useRef(null); // Use ref to refer to abort trigger div
+ const [templates, setTemplates] = React.useState([]);
+ const [mbeans, setMbeans] = React.useState([]);
+ const [mbeanSelectValue, setMbeanSelectValue] = React.useState();
+ const addSubscription = useSubscriptions();
+ const context = React.useContext(ServiceContext);
+ const [formData, setFormData] = React.useState({
+ name: '',
+ nameValid: ValidatedOptions.default,
+ enabled: true,
+ expression: '', // Use this for displaying Match Expression input
+ expressionValid: ValidatedOptions.default,
+ });
+
+ // FIXME Triggers currently rely on MbeanMetrics. We can query all
+ // registered mbeans/attributes but we need to filter the supported ones.
+ const supportedTriggerAttributes: string[] = React.useMemo(
+ () => [
+ 'ThreadCount',
+ 'DaemonThreadCount',
+ 'Arch',
+ 'AvailableProcessors',
+ 'Version',
+ 'SystemCpuLoad',
+ 'SystemLoadAverage',
+ 'ProcessCpuLoad',
+ 'TotalPhysicalMemorySize',
+ 'FreePhyiscalMemorySize',
+ 'TotalSwapSpaceSize',
+ 'HeapMemoryUsage',
+ 'HeapMemoryUsagePercent',
+ 'BootClassPath',
+ 'ClassPath',
+ 'InputArguments',
+ 'LibraryPath',
+ 'ManagementSpecVersion',
+ 'SpecName',
+ 'SpecVendor',
+ 'StartTime',
+ 'SystemProperties',
+ 'Uptime',
+ 'VmName',
+ 'VmVendor',
+ 'VmVersion',
+ 'BootClassPathSupported',
+ ],
+ [],
+ );
const [uploading, setUploading] = React.useState(false);
- const expressionRegex = RegExp('\\[(.*(&&)*|(\\|\\|)*)\\]~([\\w\\-]+)(?:\\.jfc)?');
+ const expressionRegex = RegExp('\\[(.*(&&)*|(\\|\\|)*)\\]');
const [expressionInput, setExpressionInput] = React.useState('');
const [expressionValid, setExpressionValid] = React.useState(ValidatedOptions.default);
+ const [templateValid, setTemplateValid] = React.useState(ValidatedOptions.default);
const reset = React.useCallback(() => {
setUploading(false);
@@ -660,11 +727,11 @@ export const CreateSmartTriggersModal: React.FC =
const handleSubmit = React.useCallback(() => {
submitRef.current && submitRef.current.click();
- props.onAccept(expressionInput);
+ props.onAccept(expressionInput + '~' + formData.template?.name);
setUploading(false);
onClose();
setExpressionInput('');
- }, [props, onClose, expressionInput, submitRef]);
+ }, [props, onClose, expressionInput, submitRef, formData.template?.name]);
const submitButtonLoadingProps = React.useMemo(
() =>
@@ -676,6 +743,66 @@ export const CreateSmartTriggersModal: React.FC =
[uploading],
);
+ const refreshFormOptions = React.useCallback(
+ (target: Target) => {
+ if (!target) {
+ return;
+ }
+ addSubscription(
+ forkJoin({
+ templates: context.api.getTargetEventTemplates(target),
+ mbeans: context.api.getTargetMbeans(target),
+ }).subscribe({
+ next: ({ templates, mbeans }) => {
+ setTemplates(templates);
+ setMbeans(mbeans);
+ },
+ }),
+ );
+ },
+ [addSubscription, context.api, setTemplates],
+ );
+
+ React.useEffect(() => {
+ addSubscription(context.target.target().subscribe(refreshFormOptions));
+ }, [addSubscription, context.target, refreshFormOptions]);
+
+ const selectedSpecifier = React.useMemo(() => {
+ const { template } = formData;
+ if (template && template.name && template.type) {
+ return `${template.name},${template.type}`;
+ }
+ return '';
+ }, [formData]);
+
+ const MbeanOptions = React.useMemo(() => {
+ var attributes: MbeanSelectorOption[] = [];
+ mbeans.forEach((m: MbeanAttributeMap) => {
+ m.attributes.forEach((i: MBeanAttributeInfo) => {
+ if (supportedTriggerAttributes.includes(i.name)) {
+ attributes.push({
+ value: i.name,
+ label: `${i.parentBean} - ${i.name} (${i.type})`,
+ disabled: false,
+ });
+ }
+ });
+ });
+ return attributes;
+ }, [mbeans, supportedTriggerAttributes]);
+
+ const handleTemplateChange = React.useCallback(
+ (template: EventTemplateIdentifier) => {
+ setFormData((old) => ({ ...old, template }));
+ setTemplateValid(ValidatedOptions.success);
+ },
+ [setFormData],
+ );
+
+ const onMbeanChange = (_event: React.FormEvent, value: string) => {
+ setMbeanSelectValue(value);
+ };
+
return (
=
description="Create a customized Smart Trigger. This is a specialized tool available in the Cryostat Agent that listens for a condition to be met for a specified Mbean, after which a recording will be started with the specified template. This is only available for targets using the Cryostat Agent."
>