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:
Peter Holmberg 2022-04-01 16:34:08 +02:00 committed by GitHub
parent 7b30fae36f
commit cb03b05ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 2 deletions

0
.yarn/releases/yarn-3.2.0.cjs vendored Executable file → Normal file
View File

View File

@ -128,6 +128,7 @@
"@types/hoist-non-react-statics": "3.3.1",
"@types/jest": "27.4.1",
"@types/jquery": "3.5.14",
"@types/js-yaml": "^4.0.5",
"@types/jsurl": "^1.2.28",
"@types/lingui__macro": "^3",
"@types/lodash": "4.14.181",
@ -311,6 +312,7 @@
"immer": "9.0.12",
"immutable": "4.0.0",
"jquery": "3.6.0",
"js-yaml": "^4.1.0",
"json-source-map": "0.6.1",
"jsurl": "^0.1.5",
"lezer-promql": "0.22.0",

View File

@ -6,7 +6,7 @@ import { css } from '@emotion/css';
import { AlertTypeStep } from './AlertTypeStep';
import { DetailsStep } from './DetailsStep';
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 { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
@ -23,6 +23,7 @@ import { useAppNotification } from 'app/core/copy/appNotification';
import { CloudConditionsStep } from './CloudConditionsStep';
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
import * as ruleId from '../../utils/rule-id';
import { RuleInspector } from './RuleInspector';
type Props = {
existing?: RuleWithLocation;
@ -33,6 +34,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
const dispatch = useDispatch();
const notifyApp = useAppNotification();
const [queryParams] = useQueryParams();
const [showEditYaml, setShowEditYaml] = useState(false);
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
@ -106,7 +108,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
return (
<FormProvider {...formAPI}>
<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}>
<Button variant="secondary" disabled={submitState.loading} type="button" fill="outline">
Cancel
@ -117,6 +119,16 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
Delete
</Button>
) : null}
{isCortexLokiOrRecordingRule(watch) && (
<Button
variant="secondary"
type="button"
onClick={() => setShowEditYaml(true)}
disabled={submitState.loading}
>
Edit yaml
</Button>
)}
<Button
variant="primary"
type="button"
@ -162,10 +174,17 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
onDismiss={() => setShowDeleteModal(false)}
/>
) : null}
{showEditYaml ? <RuleInspector onClose={() => setShowEditYaml(false)} /> : null}
</FormProvider>
);
};
const isCortexLokiOrRecordingRule = (watch: UseFormWatch<RuleFormValues>) => {
const [ruleType, dataSourceName] = watch(['type', 'dataSourceName']);
return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== '';
};
const getStyles = (theme: GrafanaTheme2) => {
return {
buttonSpinner: css`

View File

@ -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;
`,
});

View File

@ -10112,6 +10112,13 @@ __metadata:
languageName: node
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":
version: 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/jest": 27.4.1
"@types/jquery": 3.5.14
"@types/js-yaml": ^4.0.5
"@types/jsurl": ^1.2.28
"@types/lingui__macro": ^3
"@types/lodash": 4.14.181
@ -20604,6 +20612,7 @@ __metadata:
jest-matcher-utils: 27.5.1
jest-mock-console: 1.2.3
jquery: 3.6.0
js-yaml: ^4.1.0
json-source-map: 0.6.1
jsurl: ^0.1.5
lerna: ^4.0.0