mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add yaml editor to cloud rules (#46533)
* ruleinspector component * Adding yaml component * setvalues * update yarn.lock * bump types * chore: update lockfile * move apply button position * move button back to tab Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>
This commit is contained in:
0
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file → Normal file
0
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file → Normal file
@@ -128,6 +128,7 @@
|
|||||||
"@types/hoist-non-react-statics": "3.3.1",
|
"@types/hoist-non-react-statics": "3.3.1",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "27.4.1",
|
||||||
"@types/jquery": "3.5.14",
|
"@types/jquery": "3.5.14",
|
||||||
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/jsurl": "^1.2.28",
|
"@types/jsurl": "^1.2.28",
|
||||||
"@types/lingui__macro": "^3",
|
"@types/lingui__macro": "^3",
|
||||||
"@types/lodash": "4.14.181",
|
"@types/lodash": "4.14.181",
|
||||||
@@ -311,6 +312,7 @@
|
|||||||
"immer": "9.0.12",
|
"immer": "9.0.12",
|
||||||
"immutable": "4.0.0",
|
"immutable": "4.0.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"json-source-map": "0.6.1",
|
"json-source-map": "0.6.1",
|
||||||
"jsurl": "^0.1.5",
|
"jsurl": "^0.1.5",
|
||||||
"lezer-promql": "0.22.0",
|
"lezer-promql": "0.22.0",
|
||||||
|
@@ -6,7 +6,7 @@ import { css } from '@emotion/css';
|
|||||||
import { AlertTypeStep } from './AlertTypeStep';
|
import { AlertTypeStep } from './AlertTypeStep';
|
||||||
import { DetailsStep } from './DetailsStep';
|
import { DetailsStep } from './DetailsStep';
|
||||||
import { QueryStep } from './QueryStep';
|
import { QueryStep } from './QueryStep';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider, UseFormWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
@@ -23,6 +23,7 @@ import { useAppNotification } from 'app/core/copy/appNotification';
|
|||||||
import { CloudConditionsStep } from './CloudConditionsStep';
|
import { CloudConditionsStep } from './CloudConditionsStep';
|
||||||
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
|
import { RuleInspector } from './RuleInspector';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
existing?: RuleWithLocation;
|
existing?: RuleWithLocation;
|
||||||
@@ -33,6 +34,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const notifyApp = useAppNotification();
|
const notifyApp = useAppNotification();
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
|
const [showEditYaml, setShowEditYaml] = useState(false);
|
||||||
|
|
||||||
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
|
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
|
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
|
||||||
@@ -106,7 +108,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
|
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
|
||||||
<PageToolbar title="Create alert rule" pageIcon="bell">
|
<PageToolbar title={`${existing ? 'Edit' : 'Create'} alert rule`} pageIcon="bell">
|
||||||
<Link to={returnTo}>
|
<Link to={returnTo}>
|
||||||
<Button variant="secondary" disabled={submitState.loading} type="button" fill="outline">
|
<Button variant="secondary" disabled={submitState.loading} type="button" fill="outline">
|
||||||
Cancel
|
Cancel
|
||||||
@@ -117,6 +119,16 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isCortexLokiOrRecordingRule(watch) && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowEditYaml(true)}
|
||||||
|
disabled={submitState.loading}
|
||||||
|
>
|
||||||
|
Edit yaml
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -162,10 +174,17 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
onDismiss={() => setShowDeleteModal(false)}
|
onDismiss={() => setShowDeleteModal(false)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showEditYaml ? <RuleInspector onClose={() => setShowEditYaml(false)} /> : null}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCortexLokiOrRecordingRule = (watch: UseFormWatch<RuleFormValues>) => {
|
||||||
|
const [ruleType, dataSourceName] = watch(['type', 'dataSourceName']);
|
||||||
|
|
||||||
|
return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== '';
|
||||||
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
buttonSpinner: css`
|
buttonSpinner: css`
|
||||||
|
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { dump, load } from 'js-yaml';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Button, CodeEditor, Drawer, Tab, TabsBar, useStyles2 } from '@grafana/ui';
|
||||||
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [{ label: 'Yaml', value: 'yaml' }];
|
||||||
|
|
||||||
|
export const RuleInspector: FC<Props> = ({ onClose }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState('yaml');
|
||||||
|
const { setValue } = useFormContext<RuleFormValues>();
|
||||||
|
const styles = useStyles2(drawerStyles);
|
||||||
|
|
||||||
|
const onApply = (formValues: RuleFormValues) => {
|
||||||
|
// Need to loop through all values and set them individually
|
||||||
|
// TODO this is not type-safe :(
|
||||||
|
for (const key in formValues) {
|
||||||
|
// @ts-ignore
|
||||||
|
setValue(key, formValues[key]);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="Inspect Alert rule"
|
||||||
|
subtitle={
|
||||||
|
<div className={styles.subtitle}>
|
||||||
|
<RuleInspectorSubtitle setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{activeTab === 'yaml' && <InspectorYamlTab onSubmit={onApply} />}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SubtitleProps {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuleInspectorSubtitle: FC<SubtitleProps> = ({ activeTab, setActiveTab }) => {
|
||||||
|
return (
|
||||||
|
<TabsBar>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
key={`${tab.value}-${index}`}
|
||||||
|
label={tab.label}
|
||||||
|
value={tab.value}
|
||||||
|
onChangeTab={() => setActiveTab(tab.value)}
|
||||||
|
active={activeTab === tab.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface YamlTabProps {
|
||||||
|
onSubmit: (newModel: RuleFormValues) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InspectorYamlTab: FC<YamlTabProps> = ({ onSubmit }) => {
|
||||||
|
const styles = useStyles2(yamlTabStyle);
|
||||||
|
const { getValues } = useFormContext<RuleFormValues>();
|
||||||
|
const [alertRuleAsYaml, setAlertRuleAsYaml] = useState(dump(getValues()));
|
||||||
|
|
||||||
|
const onApply = () => {
|
||||||
|
onSubmit(load(alertRuleAsYaml) as RuleFormValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.applyButton}>
|
||||||
|
<Button type="button" onClick={onApply}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<AutoSizer disableWidth>
|
||||||
|
{({ height }) => (
|
||||||
|
<CodeEditor
|
||||||
|
width="100%"
|
||||||
|
height={height}
|
||||||
|
language="yaml"
|
||||||
|
value={alertRuleAsYaml}
|
||||||
|
onBlur={setAlertRuleAsYaml}
|
||||||
|
monacoOptions={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const yamlTabStyle = (theme: GrafanaTheme2) => ({
|
||||||
|
content: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
|
applyButton: css`
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawerStyles = () => ({
|
||||||
|
subtitle: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`,
|
||||||
|
});
|
@@ -10112,6 +10112,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/js-yaml@npm:^4.0.5":
|
||||||
|
version: 4.0.5
|
||||||
|
resolution: "@types/js-yaml@npm:4.0.5"
|
||||||
|
checksum: 7dcac8c50fec31643cc9d6444b5503239a861414cdfaa7ae9a38bc22597c4d850c4b8cec3d82d73b3fbca408348ce223b0408d598b32e094470dfffc6d486b4d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
|
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
|
||||||
version: 7.0.9
|
version: 7.0.9
|
||||||
resolution: "@types/json-schema@npm:7.0.9"
|
resolution: "@types/json-schema@npm:7.0.9"
|
||||||
@@ -20496,6 +20503,7 @@ __metadata:
|
|||||||
"@types/hoist-non-react-statics": 3.3.1
|
"@types/hoist-non-react-statics": 3.3.1
|
||||||
"@types/jest": 27.4.1
|
"@types/jest": 27.4.1
|
||||||
"@types/jquery": 3.5.14
|
"@types/jquery": 3.5.14
|
||||||
|
"@types/js-yaml": ^4.0.5
|
||||||
"@types/jsurl": ^1.2.28
|
"@types/jsurl": ^1.2.28
|
||||||
"@types/lingui__macro": ^3
|
"@types/lingui__macro": ^3
|
||||||
"@types/lodash": 4.14.181
|
"@types/lodash": 4.14.181
|
||||||
@@ -20604,6 +20612,7 @@ __metadata:
|
|||||||
jest-matcher-utils: 27.5.1
|
jest-matcher-utils: 27.5.1
|
||||||
jest-mock-console: 1.2.3
|
jest-mock-console: 1.2.3
|
||||||
jquery: 3.6.0
|
jquery: 3.6.0
|
||||||
|
js-yaml: ^4.1.0
|
||||||
json-source-map: 0.6.1
|
json-source-map: 0.6.1
|
||||||
jsurl: ^0.1.5
|
jsurl: ^0.1.5
|
||||||
lerna: ^4.0.0
|
lerna: ^4.0.0
|
||||||
|
Reference in New Issue
Block a user