mirror of
https://github.com/grafana/grafana.git
synced 2025-01-05 13:45:16 -06:00
Alerting: visual alert type picker (#46111)
This commit is contained in:
parent
644e7e7c19
commit
64d4e6fcaa
@ -115,7 +115,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps &
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
{isSelected !== undefined && <input aria-label="option" type="radio" checked={isSelected} />}
|
||||
{isSelected !== undefined && <input aria-label="option" type="radio" readOnly checked={isSelected} />}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
@ -17,8 +17,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { DashboardSearchHit } from 'app/features/search/types';
|
||||
import { getDefaultQueries } from './utils/rule-form';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import * as api from 'app/features/manage-dashboards/state/actions';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
@ -29,6 +29,7 @@ jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
|
||||
jest.mock('./api/ruler');
|
||||
jest.mock('./utils/config');
|
||||
jest.mock('../../../../app/features/manage-dashboards/state/actions');
|
||||
|
||||
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||
// lets just skip it
|
||||
@ -39,7 +40,7 @@ jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: jest.mocked(getAllDataSources),
|
||||
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
api: {
|
||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
||||
@ -80,6 +81,10 @@ const ui = {
|
||||
save: byRole('button', { name: 'Save' }),
|
||||
addAnnotation: byRole('button', { name: /Add info/ }),
|
||||
addLabel: byRole('button', { name: /Add label/ }),
|
||||
// alert type buttons
|
||||
grafanaManagedAlert: byRole('button', { name: /Grafana managed/ }),
|
||||
lotexAlert: byRole('button', { name: /Cortex or Loki alert/ }),
|
||||
lotexRecordingRule: byRole('button', { name: /Cortex or Loki recording rule/ }),
|
||||
},
|
||||
};
|
||||
|
||||
@ -123,10 +128,13 @@ describe('RuleEditor', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([]);
|
||||
|
||||
await renderRuleEditor();
|
||||
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
|
||||
|
||||
userEvent.type(await ui.inputs.name.find(), 'my great new rule');
|
||||
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
|
||||
userEvent.click(await ui.buttons.lotexAlert.get());
|
||||
const dataSourceSelect = ui.inputs.dataSource.get();
|
||||
userEvent.click(byRole('combobox').get(dataSourceSelect));
|
||||
await clickSelectOption(dataSourceSelect, 'Prom (default)');
|
||||
@ -165,7 +173,40 @@ describe('RuleEditor', () => {
|
||||
});
|
||||
|
||||
it('can create new grafana managed alert', async () => {
|
||||
const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([
|
||||
const dataSources = {
|
||||
default: mockDataSource(
|
||||
{
|
||||
type: 'prometheus',
|
||||
name: 'Prom',
|
||||
isDefault: true,
|
||||
},
|
||||
{ alerting: true }
|
||||
),
|
||||
};
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRulesGroup.mockResolvedValue({
|
||||
name: 'group2',
|
||||
rules: [],
|
||||
});
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
namespace1: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
},
|
||||
],
|
||||
namespace2: [
|
||||
{
|
||||
name: 'group2',
|
||||
rules: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([
|
||||
{
|
||||
title: 'Folder A',
|
||||
id: 1,
|
||||
@ -176,24 +217,13 @@ describe('RuleEditor', () => {
|
||||
},
|
||||
] as DashboardSearchHit[]);
|
||||
|
||||
const dataSources = {
|
||||
default: mockDataSource({
|
||||
type: 'prometheus',
|
||||
name: 'Prom',
|
||||
isDefault: true,
|
||||
}),
|
||||
};
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
|
||||
// fill out the form
|
||||
await renderRuleEditor();
|
||||
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
|
||||
|
||||
userEvent.type(await ui.inputs.name.find(), 'my great new rule');
|
||||
await clickSelectOption(ui.inputs.alertType.get(), /Classic Grafana alerts based on thresholds/);
|
||||
|
||||
const folderInput = await ui.inputs.folder.find();
|
||||
await waitFor(() => expect(searchFolderMock).toHaveBeenCalled());
|
||||
await clickSelectOption(folderInput, 'Folder A');
|
||||
|
||||
userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary');
|
||||
@ -264,12 +294,17 @@ describe('RuleEditor', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([]);
|
||||
|
||||
await renderRuleEditor();
|
||||
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
|
||||
|
||||
userEvent.type(await ui.inputs.name.find(), 'my great new recording rule');
|
||||
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed recording rule/);
|
||||
userEvent.click(await ui.buttons.lotexRecordingRule.get());
|
||||
|
||||
const dataSourceSelect = ui.inputs.dataSource.get();
|
||||
userEvent.click(byRole('combobox').get(dataSourceSelect));
|
||||
|
||||
await clickSelectOption(dataSourceSelect, 'Prom (default)');
|
||||
await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
|
||||
await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
|
||||
@ -320,7 +355,6 @@ describe('RuleEditor', () => {
|
||||
uid: 'abcd',
|
||||
id: 1,
|
||||
};
|
||||
const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([folder] as DashboardSearchHit[]);
|
||||
const getFolderByUid = jest.fn().mockResolvedValue({
|
||||
...folder,
|
||||
canSave: true,
|
||||
@ -338,6 +372,7 @@ describe('RuleEditor', () => {
|
||||
} as any as BackendSrv;
|
||||
setBackendSrv(backendSrv);
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
@ -365,9 +400,10 @@ describe('RuleEditor', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([folder] as DashboardSearchHit[]);
|
||||
|
||||
await renderRuleEditor(uid);
|
||||
await waitFor(() => expect(searchFolderMock).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
|
||||
|
||||
// check that it's filled in
|
||||
const nameInput = await ui.inputs.name.find();
|
||||
@ -484,11 +520,14 @@ describe('RuleEditor', () => {
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.searchFolders.mockResolvedValue([]);
|
||||
|
||||
// render rule editor, select cortex/loki managed alerts
|
||||
await renderRuleEditor();
|
||||
await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled());
|
||||
|
||||
await ui.inputs.name.find();
|
||||
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
|
||||
userEvent.click(await ui.buttons.lotexAlert.get());
|
||||
|
||||
// wait for ui theck each datasource if it supports rule editing
|
||||
await waitFor(() => expect(mocks.api.fetchRulerRulesGroup).toHaveBeenCalledTimes(4));
|
||||
|
@ -45,6 +45,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
...getDefaultFormValues(),
|
||||
queries: getDefaultQueries(),
|
||||
...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}),
|
||||
type: RuleFormType.grafana,
|
||||
};
|
||||
}, [existing, queryParams]);
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Field, Input, InputControl, Select, useStyles2 } from '@grafana/ui';
|
||||
import React, { FC } from 'react';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, Input, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { Folder, RuleFolderPicker } from './RuleFolderPicker';
|
||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { CloudRulesSourcePicker } from './CloudRulesSourcePicker';
|
||||
import { checkForPathSeparator } from './util';
|
||||
import { RuleTypePicker } from './rule-types/RuleTypePicker';
|
||||
|
||||
interface Props {
|
||||
editingExistingRule: boolean;
|
||||
@ -30,38 +30,36 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
watch,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
const alertTypeOptions = useMemo((): SelectableValue[] => {
|
||||
const result = [
|
||||
{
|
||||
label: 'Grafana managed alert',
|
||||
value: RuleFormType.grafana,
|
||||
description: 'Classic Grafana alerts based on thresholds.',
|
||||
},
|
||||
];
|
||||
|
||||
if (contextSrv.isEditor) {
|
||||
result.push({
|
||||
label: 'Cortex/Loki managed alert',
|
||||
value: RuleFormType.cloudAlerting,
|
||||
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
||||
});
|
||||
result.push({
|
||||
label: 'Cortex/Loki managed recording rule',
|
||||
value: RuleFormType.cloudRecording,
|
||||
description: 'Recording rule to pre-compute frequently needed or expensive calculations. Based on Prometheus.',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={1} title="Rule type">
|
||||
<Field
|
||||
disabled={editingExistingRule}
|
||||
error={errors.type?.message}
|
||||
invalid={!!errors.type?.message}
|
||||
data-testid="alert-type-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange } }) => (
|
||||
<RuleTypePicker
|
||||
aria-label="Rule type"
|
||||
selected={getValues('type') ?? RuleFormType.grafana}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select alert type' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Rule name"
|
||||
@ -88,31 +86,6 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
/>
|
||||
</Field>
|
||||
<div className={styles.flexRow}>
|
||||
<Field
|
||||
disabled={editingExistingRule}
|
||||
label="Rule type"
|
||||
className={styles.formInput}
|
||||
error={errors.type?.message}
|
||||
invalid={!!errors.type?.message}
|
||||
data-testid="alert-type-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
aria-label="Rule type"
|
||||
{...field}
|
||||
options={alertTypeOptions}
|
||||
onChange={(v: SelectableValue) => onChange(v?.value)}
|
||||
/>
|
||||
)}
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select alert type' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && (
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React, { FC } from 'react';
|
||||
import { RuleType, SharedProps } from './RuleType';
|
||||
import { DisabledTooltip } from './DisabledTooltip';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
|
||||
interface Props extends SharedProps {
|
||||
onClick: (value: RuleFormType) => void;
|
||||
}
|
||||
|
||||
const CortexFlavoredType: FC<Props> = ({ selected = false, disabled = false, onClick }) => {
|
||||
return (
|
||||
<DisabledTooltip visible={disabled}>
|
||||
<RuleType
|
||||
name="Cortex or Loki alert"
|
||||
description={
|
||||
<span>
|
||||
Use a Cortex or Loki datasource.
|
||||
<br />
|
||||
Expressions are not supported.
|
||||
</span>
|
||||
}
|
||||
image="/public/img/alerting/cortex_logo.svg"
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
value={RuleFormType.cloudAlerting}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</DisabledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export { CortexFlavoredType };
|
@ -0,0 +1,28 @@
|
||||
import React, { FC } from 'react';
|
||||
import { RuleType, SharedProps } from './RuleType';
|
||||
import { DisabledTooltip } from './DisabledTooltip';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
|
||||
const RecordingRuleType: FC<SharedProps> = ({ selected = false, disabled = false, onClick }) => {
|
||||
return (
|
||||
<DisabledTooltip visible={disabled}>
|
||||
<RuleType
|
||||
name="Cortex or Loki recording rule"
|
||||
description={
|
||||
<span>
|
||||
Precompute expressions.
|
||||
<br />
|
||||
Should be combined with an alert rule.
|
||||
</span>
|
||||
}
|
||||
image="/public/img/alerting/cortex_logo_recording.svg"
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
value={RuleFormType.cloudRecording}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</DisabledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export { RecordingRuleType };
|
@ -0,0 +1,20 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
const DisabledTooltip: FC<Props> = ({ children, visible = false }) => {
|
||||
if (!visible) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="You do not appear to have any compatible datasources." placement="top">
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export { DisabledTooltip };
|
@ -0,0 +1,25 @@
|
||||
import React, { FC } from 'react';
|
||||
import { RuleType, SharedProps } from './RuleType';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
|
||||
const GrafanaManagedRuleType: FC<SharedProps> = ({ selected = false, disabled, onClick }) => {
|
||||
return (
|
||||
<RuleType
|
||||
name="Grafana managed alert"
|
||||
description={
|
||||
<span>
|
||||
Supports multiple data sources of any kind.
|
||||
<br />
|
||||
Transform data with expressions.
|
||||
</span>
|
||||
}
|
||||
image="/public/img/grafana_icon.svg"
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
value={RuleFormType.grafana}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { GrafanaManagedRuleType };
|
@ -0,0 +1,52 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, useStyles2 } from '@grafana/ui';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
|
||||
interface Props extends SharedProps {
|
||||
image: string;
|
||||
name: string;
|
||||
description: ReactNode;
|
||||
value: RuleFormType;
|
||||
}
|
||||
|
||||
// these properties are shared between all Rule Types
|
||||
export interface SharedProps {
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: RuleFormType) => void;
|
||||
}
|
||||
|
||||
const RuleType: FC<Props> = (props) => {
|
||||
const { name, description, image, selected = false, value, onClick, disabled = false } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const cardStyles = cx({
|
||||
[styles.wrapper]: true,
|
||||
[styles.disabled]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={cardStyles} isSelected={selected} onClick={() => onClick(value)} disabled={disabled}>
|
||||
<Card.Figure>
|
||||
<img src={image} />
|
||||
</Card.Figure>
|
||||
<Card.Heading>{name}</Card.Heading>
|
||||
<Card.Description>{description}</Card.Description>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
width: 380px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
`,
|
||||
disabled: css`
|
||||
opacity: 0.5;
|
||||
`,
|
||||
});
|
||||
|
||||
export { RuleType };
|
@ -0,0 +1,52 @@
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
|
||||
import { RuleFormType } from '../../../types/rule-form';
|
||||
import { GrafanaManagedRuleType } from './GrafanaManagedAlert';
|
||||
import { CortexFlavoredType } from './CortexOrLokiAlert';
|
||||
import { RecordingRuleType } from './CortexOrLokiRecordingRule';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
|
||||
interface RuleTypePickerProps {
|
||||
onChange: (value: RuleFormType) => void;
|
||||
selected: RuleFormType;
|
||||
}
|
||||
|
||||
const RuleTypePicker: FC<RuleTypePickerProps> = ({ selected, onChange }) => {
|
||||
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
|
||||
const hasLotexDatasources = !isEmpty(rulesSourcesWithRuler);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" gap={2}>
|
||||
<GrafanaManagedRuleType selected={selected === RuleFormType.grafana} onClick={onChange} />
|
||||
<CortexFlavoredType
|
||||
selected={selected === RuleFormType.cloudAlerting}
|
||||
onClick={onChange}
|
||||
disabled={!hasLotexDatasources}
|
||||
/>
|
||||
<RecordingRuleType
|
||||
selected={selected === RuleFormType.cloudRecording}
|
||||
onClick={onChange}
|
||||
disabled={!hasLotexDatasources}
|
||||
/>
|
||||
</Stack>
|
||||
<small className={styles.meta}>
|
||||
Select “Grafana managed” unless you have a Cortex or Loki data source with the Ruler API enabled.
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
meta: css`
|
||||
color: ${theme.colors.text.disabled};
|
||||
`,
|
||||
});
|
||||
|
||||
export { RuleTypePicker };
|
1
public/img/alerting/cortex_logo.svg
Normal file
1
public/img/alerting/cortex_logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="-4.2 -3.7 439.14 439.14" xmlns="http://www.w3.org/2000/svg"><path d="m329.643 275.202a54.609 54.609 0 1 0 -54.608-54.61 54.672 54.672 0 0 0 54.608 54.61zm0-89.35a34.947 34.947 0 1 1 -34.947 34.948 34.947 34.947 0 0 1 34.947-34.948z" fill="none"/><path d="m417.343 216.014c0-111.3-90.55-201.85-201.85-201.85s-201.85 90.55-201.85 201.85 90.55 201.85 201.85 201.85 201.85-90.55 201.85-201.85zm-292.54 9.436h-18.2a35.326 35.326 0 1 1 .278-6.204h22.287v.592l.064-.025 22.046 55.117 24.993-57.842h29.413l26.339-65.423 23.712 67.75h13.11a60.813 60.813 0 0 1 27.464-49.658l-58.457-55.534a34.96 34.96 0 1 1 4.39-4.389l59.643 56.66a60.816 60.816 0 1 1 -1.999 107.117l-58.118 58.118-4.386-4.386 57.134-57.135a60.828 60.828 0 0 1 -25.476-44.59h-17.707l-19.716-56.331-21.743 54.005h-29.525l-29.292 67.79zm54.916 128.737a34.947 34.947 0 1 1 34.947 34.947 34.947 34.947 0 0 1 -34.947-34.947z" fill="none"/><g fill="#3b697e"><path d="m426.65 216.014c0-116.432-94.725-211.156-211.157-211.156s-211.155 94.724-211.155 211.156 94.722 211.156 211.155 211.156 211.157-94.724 211.157-211.156zm-413.006 0c0-111.3 90.55-201.85 201.85-201.85s201.849 90.55 201.849 201.85-90.55 201.85-201.85 201.85-201.85-90.55-201.85-201.85z"/><circle cx="329.643" cy="220.8" r="34.947"/><circle cx="214.666" cy="354.187" r="34.947"/><path d="m209.874 223.292 21.743-54.005 19.716 56.331h17.707a60.828 60.828 0 0 0 25.476 44.59l-57.134 57.135 4.386 4.386 58.118-58.118a60.81 60.81 0 1 0 1.999-107.116l-59.643-56.66a34.954 34.954 0 1 0 -4.39 4.388l58.457 55.534a60.813 60.813 0 0 0 -27.463 49.657h-13.111l-23.712-67.749-26.339 65.423h-29.413l-24.993 57.841-22.046-55.117-.064.026v-.592h-22.287a34.958 34.958 0 1 0 -.278 6.204h18.2l26.254 65.632 29.292-67.79zm119.769-57.307a54.609 54.609 0 1 1 -54.608 54.608 54.67 54.67 0 0 1 54.608-54.608z"/></g></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
public/img/alerting/cortex_logo_recording.svg
Normal file
1
public/img/alerting/cortex_logo_recording.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 440 440" xmlns="http://www.w3.org/2000/svg"><path d="m333.843 278.902h.001c29.958 0 54.609-24.651 54.609-54.609s-24.651-54.609-54.609-54.609c-29.957 0-54.608 24.651-54.609 54.608.034 29.945 24.663 54.575 54.608 54.61zm0-89.35c19.171 0 34.947 15.776 34.947 34.947s-15.776 34.947-34.947 34.947-34.946-15.775-34.947-34.946v-.001c0-19.171 15.776-34.947 34.947-34.947z" fill="none"/><path d="m421.543 219.714c0-111.3-90.55-201.85-201.85-201.85s-201.85 90.55-201.85 201.85 90.55 201.85 201.85 201.85 201.85-90.55 201.85-201.85zm-292.54 9.436h-18.2c-2.333 17.465-17.395 30.649-35.015 30.649-19.379 0-35.326-15.947-35.326-35.326 0-19.38 15.947-35.326 35.326-35.326 18.804 0 34.48 15.013 35.293 33.799h22.287v.592l.064-.025 22.046 55.117 24.993-57.842h29.413l26.339-65.423 23.712 67.75h13.11c.397-20.053 10.689-38.663 27.464-49.658l-58.457-55.534c-6.288 5.257-14.229 8.14-22.425 8.14-19.179 0-34.96-15.782-34.96-34.96 0-19.179 15.781-34.96 34.96-34.96 19.178 0 34.96 15.781 34.96 34.96 0 8.199-2.884 16.142-8.145 22.431l59.643 56.66c8.597-4.416 18.124-6.72 27.789-6.72 33.363 0 60.816 27.453 60.816 60.816 0 33.362-27.453 60.816-60.816 60.816-10.433 0-20.692-2.685-29.788-7.795l-58.118 58.118-4.386-4.386 57.134-57.135c-14.649-10.368-23.983-26.705-25.476-44.59h-17.707l-19.716-56.331-21.743 54.005h-29.525l-29.292 67.79zm54.916 128.737c0-19.171 15.776-34.947 34.947-34.947s34.947 15.776 34.947 34.947-15.776 34.947-34.947 34.947-34.947-15.776-34.947-34.947z" fill="none"/><g fill="#3b697e"><path d="m430.85 219.714c0-116.432-94.725-211.156-211.157-211.156s-211.155 94.724-211.155 211.156 94.722 211.156 211.155 211.156 211.157-94.724 211.157-211.156zm-413.006 0c0-111.3 90.55-201.85 201.85-201.85s201.849 90.55 201.849 201.85-90.55 201.85-201.85 201.85-201.85-90.55-201.85-201.85h.001z" fill-rule="nonzero"/><circle cx="333.843" cy="224.5" r="34.947"/><circle cx="218.866" cy="357.887" r="34.947"/><path d="m214.074 226.992 21.743-54.005 19.716 56.331h17.707c1.493 17.885 10.827 34.222 25.476 44.59l-57.134 57.135 4.386 4.386 58.118-58.118c9.093 5.106 19.348 7.789 29.776 7.789 33.36 0 60.81-27.451 60.81-60.81s-27.45-60.81-60.81-60.81c-9.66 0-19.183 2.302-27.777 6.715l-59.643-56.66c5.261-6.288 8.146-14.231 8.146-22.43 0-19.175-15.779-34.954-34.954-34.954s-34.954 15.779-34.954 34.954 15.779 34.954 34.954 34.954c8.193 0 16.132-2.881 22.418-8.136l58.457 55.534c-16.774 10.995-27.066 29.605-27.463 49.657h-13.111l-23.712-67.749-26.339 65.423h-29.413l-24.993 57.841-22.046-55.117-.064.026v-.592h-22.287c-.821-18.577-16.328-33.415-34.924-33.415-19.177 0-34.958 15.781-34.958 34.958 0 19.178 15.781 34.958 34.958 34.958 17.425 0 32.323-13.028 34.646-30.297h18.2l26.254 65.632 29.292-67.79zm119.769-57.307h.001c29.958 0 54.609 24.651 54.609 54.609s-24.651 54.609-54.609 54.609-54.609-24.651-54.609-54.609v-.001c.034-29.944 24.664-54.574 54.608-54.608z" fill-rule="nonzero"/></g><path d="m337.399 336.8c0-17.198-13.961-31.16-31.159-31.16h-264.898c-17.198 0-31.161 13.962-31.161 31.16v62.32c0 17.198 13.963 31.16 31.161 31.16h264.898c17.198 0 31.159-13.962 31.159-31.16z"/><circle cx="84.842" cy="367.962" fill="#f00" r="36.225"/><path d="m142.162 395.885v-55.846h23.732c5.968 0 10.305.501 13.009 1.504 2.706 1.003 4.87 2.787 6.496 5.353 1.625 2.565 2.438 5.498 2.438 8.799 0 4.19-1.233 7.651-3.696 10.381s-6.145 4.45-11.047 5.161c2.438 1.423 4.451 2.984 6.038 4.686 1.587 1.701 3.727 4.724 6.418 9.066l6.82 10.896h-13.485l-8.153-12.153c-2.895-4.342-4.875-7.079-5.942-8.209s-2.197-1.904-3.39-2.323c-1.194-.42-3.086-.63-5.677-.63h-2.285v23.315zm11.276-32.229h8.342c5.41 0 8.788-.228 10.133-.685 1.346-.456 2.4-1.245 3.162-2.362.761-1.117 1.143-2.514 1.143-4.19 0-1.879-.502-3.397-1.505-4.552-1.004-1.156-2.419-1.886-4.248-2.19-.914-.127-3.657-.191-8.228-.191h-8.799z" fill="#fff" fill-rule="nonzero"/><path d="m198.465 395.885v-55.846h41.407v9.447h-30.132v12.38h28.037v9.41h-28.037v15.199h31.199v9.41z" fill="#fff" fill-rule="nonzero"/><path d="m286.233 375.351 10.933 3.467c-1.677 6.094-4.464 10.621-8.362 13.581-3.898 2.958-8.844 4.437-14.838 4.437-7.415 0-13.509-2.533-18.285-7.6-4.774-5.065-7.161-11.992-7.161-20.78 0-9.294 2.4-16.513 7.2-21.656s11.111-7.714 18.932-7.714c6.832 0 12.381 2.02 16.648 6.057 2.539 2.387 4.444 5.815 5.713 10.285l-11.16 2.667c-.662-2.895-2.039-5.181-4.134-6.857-2.096-1.676-4.641-2.514-7.637-2.514-4.14 0-7.5 1.486-10.077 4.457s-3.867 7.784-3.867 14.438c0 7.059 1.27 12.088 3.81 15.085s5.841 4.494 9.904 4.494c2.997 0 5.575-.952 7.734-2.857 2.158-1.904 3.707-4.901 4.647-8.99z" fill="#fff" fill-rule="nonzero"/></svg>
|
After Width: | Height: | Size: 4.5 KiB |
Loading…
Reference in New Issue
Block a user