mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Enable editing evaluation interval in alert form when creating a new group (#60083)
* Enable editing evaluation interval in alert form when creating a new group * Disable group when no folder selected and focus on group when clicking add new * Improve group selector showing interval as a description for each option * Fix evaluate every input and label aligment * Fix columns width in EditRuleGroupModal rules table * Reduce amount of space in the evaluation behaviour section
This commit is contained in:
parent
79ffb699ee
commit
171cd60480
@ -11297,6 +11297,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BacktestConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AlertQuery"
|
||||
}
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"from": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"no_data_state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Alerting",
|
||||
"NoData",
|
||||
"OK"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BacktestResult": {
|
||||
"$ref": "#/definitions/Frame"
|
||||
},
|
||||
"BasicAuth": {
|
||||
"type": "object",
|
||||
"title": "BasicAuth contains basic HTTP authentication credentials.",
|
||||
|
@ -128,7 +128,7 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
await clickSelectOption(folderInput, 'Folder A');
|
||||
const groupInput = await ui.inputs.group.find();
|
||||
await userEvent.click(byRole('combobox').get(groupInput));
|
||||
await clickSelectOption(groupInput, 'group1 (1m)');
|
||||
await clickSelectOption(groupInput, 'group1');
|
||||
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
|
||||
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@ -180,6 +180,8 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
const evaluateEveryInForm = watch('evaluateEvery');
|
||||
useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
|
||||
|
||||
return (
|
||||
<FormProvider {...formAPI}>
|
||||
|
@ -40,8 +40,16 @@ const useGetGroups = (groupfoldersForGrafana: RulerRulesConfigDTO | null | undef
|
||||
return groupOptions;
|
||||
};
|
||||
|
||||
function mapGroupsToOptions(groups: string[]): Array<SelectableValue<string>> {
|
||||
return groups.map((group) => ({ label: group, value: group }));
|
||||
function mapGroupsToOptions(
|
||||
groupsForFolder: RulerRulesConfigDTO | null | undefined,
|
||||
groups: string[],
|
||||
folderTitle: string
|
||||
): Array<SelectableValue<string>> {
|
||||
return groups.map((group) => ({
|
||||
label: group,
|
||||
value: group,
|
||||
description: `${getIntervalForGroup(groupsForFolder, group, folderTitle)}`,
|
||||
}));
|
||||
}
|
||||
interface FolderAndGroupProps {
|
||||
initialFolder: RuleForm | null;
|
||||
@ -52,11 +60,14 @@ export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
|
||||
|
||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||
|
||||
const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
|
||||
useGetGroups(groupfoldersForGrafana?.result, folderTitle)
|
||||
);
|
||||
const groupsForFolder = groupfoldersForGrafana?.result;
|
||||
return { groupOptions, groupsForFolder, loading: groupfoldersForGrafana?.loading };
|
||||
|
||||
const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
|
||||
groupsForFolder,
|
||||
useGetGroups(groupfoldersForGrafana?.result, folderTitle),
|
||||
folderTitle
|
||||
);
|
||||
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
||||
};
|
||||
|
||||
const useRuleFolderFilter = (existingRuleForm: RuleForm | null) => {
|
||||
@ -93,6 +104,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
||||
formState: { errors },
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -105,7 +117,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
||||
const [selectedGroup, setSelectedGroup] = useState(group);
|
||||
const initialRender = useRef(true);
|
||||
|
||||
const { groupOptions, groupsForFolder, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
||||
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
||||
|
||||
useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]);
|
||||
|
||||
@ -120,6 +132,10 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
||||
initialRender.current = false;
|
||||
}, [group, folder?.title]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue('group', selectedGroup);
|
||||
}, [selectedGroup, setValue]);
|
||||
|
||||
const groupIsInGroupOptions = useCallback(
|
||||
(group_: string) => {
|
||||
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
|
||||
@ -189,16 +205,15 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
) : (
|
||||
<SelectWithAdd
|
||||
disabled={!folder}
|
||||
key={`my_unique_select_key__${folder?.title ?? ''}`}
|
||||
{...field}
|
||||
options={groupOptions}
|
||||
getOptionLabel={(option: SelectableValue<string>) =>
|
||||
`${option.label} (${getIntervalForGroup(groupsForFolder, option.label ?? '', folder?.title ?? '')})`
|
||||
}
|
||||
getOptionLabel={(option: SelectableValue<string>) => `${option.label}`}
|
||||
value={selectedGroup}
|
||||
custom={isAddingGroup}
|
||||
onCustomChange={(custom: boolean) => setIsAddingGroup(custom)}
|
||||
placeholder="Evaluation group name"
|
||||
placeholder={isAddingGroup ? 'New evaluation group name' : 'Evaluation group name'}
|
||||
onChange={(value: string) => {
|
||||
field.onChange(value);
|
||||
setSelectedGroup(value);
|
||||
|
@ -4,7 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Card, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logInfo, LogMessages } from '../../Analytics';
|
||||
@ -13,7 +13,7 @@ import { RuleForm, RuleFormValues } from '../../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { parsePrometheusDuration } from '../../utils/time';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
||||
import { EditCloudGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
||||
|
||||
import { MINUTE } from './AlertRuleForm';
|
||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
||||
@ -79,6 +79,49 @@ const useIsNewGroup = (folder: string, group: string) => {
|
||||
return !groupIsInGroupOptions(group);
|
||||
};
|
||||
|
||||
export const EvaluateEveryNewGroup = ({ rules }: { rules: RulerRulesConfigDTO | null | undefined }) => {
|
||||
const {
|
||||
watch,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
const styles = useStyles2(getStyles);
|
||||
const evaluateEveryId = 'eval-every-input';
|
||||
return (
|
||||
<Field
|
||||
label="Evaluation interval"
|
||||
description="Applies to every rule within a group. It can overwrite the interval of an existing alert rule."
|
||||
>
|
||||
<div className={styles.alignInterval}>
|
||||
<Stack direction="row" justify-content="left" align-items="baseline" gap={0}>
|
||||
<InlineLabel
|
||||
htmlFor={evaluateEveryId}
|
||||
width={16}
|
||||
tooltip="How often the alert will be evaluated to see if it fires"
|
||||
>
|
||||
Evaluate every
|
||||
</InlineLabel>
|
||||
<Field
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateEvery?.message}
|
||||
invalid={!!errors.evaluateEvery}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input
|
||||
id={evaluateEveryId}
|
||||
width={8}
|
||||
{...register(
|
||||
'evaluateEvery',
|
||||
evaluateEveryValidationOptions(rules, watch('group'), watch('folder.title'))
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
function FolderGroupAndEvaluationInterval({
|
||||
initialFolder,
|
||||
evaluateEvery,
|
||||
@ -89,7 +132,7 @@ function FolderGroupAndEvaluationInterval({
|
||||
setEvaluateEvery: (value: string) => void;
|
||||
}) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||
|
||||
const group = watch('group');
|
||||
@ -101,10 +144,15 @@ function FolderGroupAndEvaluationInterval({
|
||||
const isNewGroup = useIsNewGroup(folder?.title ?? '', group);
|
||||
|
||||
useEffect(() => {
|
||||
group &&
|
||||
folder &&
|
||||
setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
|
||||
}, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery]);
|
||||
if (!isNewGroup) {
|
||||
group &&
|
||||
folder &&
|
||||
setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
|
||||
} else {
|
||||
setEvaluateEvery(MINUTE);
|
||||
setValue('evaluateEvery', MINUTE);
|
||||
}
|
||||
}, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery, isNewGroup, setValue]);
|
||||
|
||||
const closeEditGroupModal = (saved = false) => {
|
||||
if (!saved) {
|
||||
@ -130,43 +178,42 @@ function FolderGroupAndEvaluationInterval({
|
||||
/>
|
||||
)}
|
||||
{folder && group && (
|
||||
<Card className={styles.cardContainer}>
|
||||
<Card.Heading>Evaluation behavior</Card.Heading>
|
||||
<Card.Meta>
|
||||
<Stack direction="column">
|
||||
<div className={styles.evaluateLabel}>
|
||||
{`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
|
||||
<span className={styles.bold}>{evaluateEvery}</span>.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
{!isNewGroup && (
|
||||
<div>
|
||||
{`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
|
||||
</div>
|
||||
<div className={styles.evaluationContainer}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<div className={styles.marginTop}>
|
||||
{isNewGroup && group ? (
|
||||
<EvaluateEveryNewGroup rules={groupfoldersForGrafana?.result} />
|
||||
) : (
|
||||
<Stack direction="column" gap={1}>
|
||||
<div className={styles.evaluateLabel}>
|
||||
{`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
|
||||
<span className={styles.bold}>{evaluateEvery}</span>.
|
||||
</div>
|
||||
{!isNewGroup && (
|
||||
<div>
|
||||
{`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<br />
|
||||
</Stack>
|
||||
</Card.Meta>
|
||||
<Card.Actions>
|
||||
</div>
|
||||
<Stack direction="row" justify-content="right" align-items="center">
|
||||
{isNewGroup && (
|
||||
<div className={styles.warningMessage}>
|
||||
{`To edit the evaluation group interval, save the alert rule.`}
|
||||
{!isNewGroup && (
|
||||
<div className={styles.marginTop}>
|
||||
<Button
|
||||
icon={'edit'}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={editGroupDisabled}
|
||||
onClick={onOpenEditGroupModal}
|
||||
>
|
||||
<span>{'Edit evaluation group'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
icon={'edit'}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={editGroupDisabled}
|
||||
onClick={onOpenEditGroupModal}
|
||||
>
|
||||
<span>{'Edit evaluation group'}</span>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -280,8 +327,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
align-self: left;
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
cardContainer: css`
|
||||
evaluationContainer: css`
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
padding: ${theme.spacing(2)};
|
||||
max-width: ${theme.breakpoints.values.sm}px;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
intervalChangedLabel: css`
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
@ -297,4 +347,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
bold: css`
|
||||
font-weight: bold;
|
||||
`,
|
||||
alignInterval: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
margin-left: -${theme.spacing(1)};
|
||||
`,
|
||||
marginTop: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Input, Select } from '@grafana/ui';
|
||||
@ -43,6 +43,14 @@ export const SelectWithAdd: FC<Props> = ({
|
||||
[options, addLabel]
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current && isCustom) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isCustom]);
|
||||
|
||||
if (isCustom) {
|
||||
return (
|
||||
<Input
|
||||
@ -53,6 +61,7 @@ export const SelectWithAdd: FC<Props> = ({
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
|
@ -151,7 +151,7 @@ export const RulesForGroupTable = ({
|
||||
renderCell: ({ data: { alertName } }) => {
|
||||
return <>{alertName}</>;
|
||||
},
|
||||
size: 0.6,
|
||||
size: '330px',
|
||||
},
|
||||
{
|
||||
id: 'for',
|
||||
@ -174,7 +174,7 @@ export const RulesForGroupTable = ({
|
||||
return <>{numberEvaluations}</>;
|
||||
}
|
||||
},
|
||||
size: 0.2,
|
||||
size: 0.6,
|
||||
},
|
||||
];
|
||||
}, [currentInterval]);
|
||||
@ -208,6 +208,37 @@ interface FormValues {
|
||||
groupInterval: string;
|
||||
}
|
||||
|
||||
export const evaluateEveryValidationOptions = (
|
||||
rules: RulerRulesConfigDTO | null | undefined,
|
||||
groupName: string,
|
||||
nameSpaceName: string
|
||||
): RegisterOptions => ({
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
validate: (value: string) => {
|
||||
try {
|
||||
const duration = parsePrometheusDuration(value);
|
||||
|
||||
if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
|
||||
if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
if (rulesInSameGroupHaveInvalidFor(rules, groupName, nameSpaceName, value).length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
|
||||
}
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Failed to parse duration';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||
const {
|
||||
nameSpaceAndGroup: { namespace, group },
|
||||
@ -285,35 +316,6 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const groupfoldersForSource = rulerRuleRequests[sourceName];
|
||||
|
||||
const evaluateEveryValidationOptions: RegisterOptions = {
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
validate: (value: string) => {
|
||||
try {
|
||||
const duration = parsePrometheusDuration(value);
|
||||
|
||||
if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
|
||||
if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
if (
|
||||
rulesInSameGroupHaveInvalidFor(groupfoldersForSource.result, groupName, nameSpaceName, value).length === 0
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
|
||||
}
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Failed to parse duration';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.modal}
|
||||
@ -387,7 +389,10 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||
<Input
|
||||
id="groupInterval"
|
||||
placeholder="1m"
|
||||
{...register('groupInterval', evaluateEveryValidationOptions)}
|
||||
{...register(
|
||||
'groupInterval',
|
||||
evaluateEveryValidationOptions(groupfoldersForSource?.result, groupName, nameSpaceName)
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
@ -27,6 +27,7 @@ export interface RuleFormValues {
|
||||
noDataState: GrafanaAlertStateDecision;
|
||||
execErrState: GrafanaAlertStateDecision;
|
||||
folder: RuleForm | null;
|
||||
evaluateEvery: string;
|
||||
evaluateFor: string;
|
||||
|
||||
// cortex / loki rules
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { EvalFunction } from '../../state/alertDef';
|
||||
import { MINUTE } from '../components/rule-editor/AlertRuleForm';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
|
||||
import { getRulesAccess } from './access-control';
|
||||
@ -60,6 +61,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
|
||||
noDataState: GrafanaAlertStateDecision.NoData,
|
||||
execErrState: GrafanaAlertStateDecision.Error,
|
||||
evaluateFor: '5m',
|
||||
evaluateEvery: MINUTE,
|
||||
|
||||
// cortex / loki
|
||||
namespace: '',
|
||||
@ -124,6 +126,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
name: ga.title,
|
||||
type: RuleFormType.grafana,
|
||||
group: group.name,
|
||||
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||
evaluateFor: rule.for || '0',
|
||||
noDataState: ga.no_data_state,
|
||||
execErrState: ga.exec_err_state,
|
||||
|
Loading…
Reference in New Issue
Block a user