mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: bootstrap silences page (#32810)
This commit is contained in:
parent
c9e5088e8b
commit
e6a98ce1e4
@ -196,6 +196,9 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
alertChildNavs := []*dtos.NavLink{
|
||||
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
|
||||
}
|
||||
if hs.Cfg.IsNgAlertEnabled() {
|
||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
||||
}
|
||||
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsNgAlertEnabled() {
|
||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
||||
}
|
||||
|
@ -31,10 +31,13 @@ export const Page: PageType = ({ navModel, children, className, contentWidth, ..
|
||||
}, [navModel]);
|
||||
|
||||
return (
|
||||
<div {...otherProps} className={cx(styles.wrapper, className)}>
|
||||
<div
|
||||
{...otherProps}
|
||||
className={cx(styles.wrapper, className, contentWidth ? styles.contentWidth(contentWidth) : undefined)}
|
||||
>
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} contentWidth={contentWidth} />
|
||||
<PageHeader model={navModel} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
@ -56,4 +59,9 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
width: 100%;
|
||||
background: ${theme.colors.bg1};
|
||||
`,
|
||||
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css`
|
||||
.page-container {
|
||||
max-width: ${theme.breakpoints[size]};
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
@ -1,12 +1,11 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { Tab, TabsBar, Icon, IconName, useStyles } from '@grafana/ui';
|
||||
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme } from '@grafana/data';
|
||||
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
||||
|
||||
export interface Props {
|
||||
model: NavModel;
|
||||
contentWidth?: keyof GrafanaTheme['breakpoints'];
|
||||
}
|
||||
|
||||
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
|
||||
@ -72,7 +71,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const PageHeader: FC<Props> = ({ model, contentWidth }) => {
|
||||
export const PageHeader: FC<Props> = ({ model }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
if (!model) {
|
||||
@ -84,7 +83,7 @@ export const PageHeader: FC<Props> = ({ model, contentWidth }) => {
|
||||
|
||||
return (
|
||||
<div className={styles.headerCanvas}>
|
||||
<div className={cx('page-container', contentWidth ? styles.contentWidth(contentWidth) : undefined)}>
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
{renderHeaderTitle(main)}
|
||||
{children && children.length && <Navigation>{children}</Navigation>}
|
||||
@ -143,9 +142,6 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
background: ${theme.colors.bg2};
|
||||
border-bottom: 1px solid ${theme.colors.border1};
|
||||
`,
|
||||
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css`
|
||||
max-width: ${theme.breakpoints[size]};
|
||||
`,
|
||||
});
|
||||
|
||||
export default PageHeader;
|
||||
|
@ -69,6 +69,8 @@ export const RuleList: FC = () => {
|
||||
const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
||||
const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
||||
|
||||
const showNewAlertSplash = dispatched && !loading && !haveResults;
|
||||
|
||||
const combinedNamespaces = useCombinedRuleNamespaces();
|
||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||
const sorted = combinedNamespaces
|
||||
@ -116,13 +118,15 @@ export const RuleList: FC = () => {
|
||||
))}
|
||||
</InfoBox>
|
||||
)}
|
||||
{!showNewAlertSplash && (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<div />
|
||||
<a href="/alerting/new">
|
||||
<Button icon="plus">New alert rule</Button>
|
||||
</a>
|
||||
</div>
|
||||
{dispatched && !loading && !haveResults && <NoRulesSplash />}
|
||||
)}
|
||||
{showNewAlertSplash && <NoRulesSplash />}
|
||||
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
||||
{haveResults && <SystemOrApplicationRules namespaces={systemNamespaces} />}
|
||||
</AlertingPageWrapper>
|
||||
|
39
public/app/features/alerting/unified/Silences.tsx
Normal file
39
public/app/features/alerting/unified/Silences.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { fetchSilencesAction } from './state/actions';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
|
||||
const Silences: FC = () => {
|
||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const silences = useUnifiedAlertingSelector((state) => state.silences);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchSilencesAction(alertManagerSourceName));
|
||||
}, [alertManagerSourceName, dispatch]);
|
||||
|
||||
const { result, loading, error } = silences[alertManagerSourceName] || initialAsyncRequestState;
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="silences">
|
||||
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||
<br />
|
||||
<br />
|
||||
{error && !loading && (
|
||||
<InfoBox severity="error" title={<h4>Error loading silences</h4>}>
|
||||
{error.message || 'Unknown error.'}
|
||||
</InfoBox>
|
||||
)}
|
||||
{loading && <LoadingPlaceholder text="loading silences..." />}
|
||||
{result && !loading && !error && <pre>{JSON.stringify(result, null, 2)}</pre>}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Silences;
|
@ -1,5 +1,12 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import {
|
||||
AlertmanagerAlert,
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerGroup,
|
||||
Silence,
|
||||
SilenceCreatePayload,
|
||||
SilenceMatcher,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
// "grafana" for grafana-managed, otherwise a datasource name
|
||||
@ -37,3 +44,75 @@ export async function updateAlertmanagerConfig(
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchSilences(alertmanagerSourceName: string): Promise<Silence[]> {
|
||||
const result = await getBackendSrv()
|
||||
.fetch<Silence[]>({
|
||||
url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`,
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.toPromise();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// returns the new silence ID. Even in the case of an update, a new silence is created and the previous one expired.
|
||||
export async function createOrUpdateSilence(
|
||||
alertmanagerSourceName: string,
|
||||
payload: SilenceCreatePayload
|
||||
): Promise<string> {
|
||||
const result = await getBackendSrv().post(
|
||||
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`,
|
||||
payload
|
||||
);
|
||||
return result.data.silenceID;
|
||||
}
|
||||
|
||||
export async function expireSilence(alertmanagerSourceName: string, silenceID: string): Promise<void> {
|
||||
await getBackendSrv().delete(
|
||||
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences/${encodeURIComponent(silenceID)}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAlerts(
|
||||
alertmanagerSourceName: string,
|
||||
matchers?: SilenceMatcher[]
|
||||
): Promise<AlertmanagerAlert[]> {
|
||||
const filters =
|
||||
matchers
|
||||
?.map(
|
||||
(matcher) =>
|
||||
`filter=${encodeURIComponent(
|
||||
`${escapeQuotes(matcher.name)}=${matcher.isRegex ? '~' : ''}"${escapeQuotes(matcher.value)}"`
|
||||
)}`
|
||||
)
|
||||
.join('&') || '';
|
||||
|
||||
const result = await getBackendSrv()
|
||||
.fetch<AlertmanagerAlert[]>({
|
||||
url:
|
||||
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts` +
|
||||
(filters ? '?' + filters : ''),
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function fetchAlertGroups(alertmanagerSourceName: string): Promise<AlertmanagerGroup[]> {
|
||||
const result = await getBackendSrv()
|
||||
.fetch<AlertmanagerGroup[]>({
|
||||
url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts/groups`,
|
||||
showErrorAlert: false,
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
function escapeQuotes(value: string): string {
|
||||
return value.replace(/"/g, '\\"');
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertManagerCortexConfig, Silence } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { RuleLocation, RuleNamespace } from 'app/types/unified-alerting';
|
||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { fetchAlertManagerConfig } from '../api/alertmanager';
|
||||
import { fetchAlertManagerConfig, fetchSilences } from '../api/alertmanager';
|
||||
import { fetchRules } from '../api/prometheus';
|
||||
import { deleteRulerRulesGroup, fetchRulerRules, fetchRulerRulesNamespace, setRulerRuleGroup } from '../api/ruler';
|
||||
import { getAllRulesSourceNames, isCloudRulesSource } from '../utils/datasource';
|
||||
@ -28,6 +28,13 @@ export const fetchRulerRulesAction = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchSilencesAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchSilences',
|
||||
(alertManagerSourceName: string): Promise<Silence[]> => {
|
||||
return withSerializedError(fetchSilences(alertManagerSourceName));
|
||||
}
|
||||
);
|
||||
|
||||
export function fetchAllPromAndRulerRules(force = false): ThunkResult<void> {
|
||||
return (dispatch, getStore) => {
|
||||
const { promRules, rulerRules } = getStore().unifiedAlerting;
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { createAsyncMapSlice } from '../utils/redux';
|
||||
import { fetchAlertManagerConfigAction, fetchPromRulesAction, fetchRulerRulesAction } from './actions';
|
||||
import {
|
||||
fetchAlertManagerConfigAction,
|
||||
fetchPromRulesAction,
|
||||
fetchRulerRulesAction,
|
||||
fetchSilencesAction,
|
||||
} from './actions';
|
||||
|
||||
export const reducer = combineReducers({
|
||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, (dataSourceName) => dataSourceName).reducer,
|
||||
@ -10,6 +15,8 @@ export const reducer = combineReducers({
|
||||
fetchAlertManagerConfigAction,
|
||||
(alertManagerSourceName) => alertManagerSourceName
|
||||
).reducer,
|
||||
silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName)
|
||||
.reducer,
|
||||
});
|
||||
|
||||
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
||||
|
@ -141,3 +141,70 @@ export type AlertmanagerConfig = {
|
||||
inhibit_rules?: InhibitRule[];
|
||||
receivers?: Receiver[];
|
||||
};
|
||||
|
||||
export type SilenceMatcher = {
|
||||
name: string;
|
||||
value: string;
|
||||
isRegex: boolean;
|
||||
};
|
||||
|
||||
export enum SilenceState {
|
||||
Active = 'active',
|
||||
Expired = 'expired',
|
||||
Pending = 'pending',
|
||||
}
|
||||
|
||||
export enum AlertState {
|
||||
Unprocessed = 'unprocessed',
|
||||
Active = 'active',
|
||||
Suppressed = 'suppressed',
|
||||
}
|
||||
|
||||
export type Silence = {
|
||||
id: string;
|
||||
matchers?: SilenceMatcher[];
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
comment: string;
|
||||
status: {
|
||||
state: SilenceState;
|
||||
};
|
||||
};
|
||||
|
||||
export type SilenceCreatePayload = {
|
||||
id?: string;
|
||||
matchers?: SilenceMatcher[];
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
createdBy: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type AlertmanagerAlert = {
|
||||
startsAt: string;
|
||||
updatedAt: string;
|
||||
endsAt: string;
|
||||
generatorURL?: string;
|
||||
labels: { [key: string]: string };
|
||||
annotations: { [key: string]: string };
|
||||
receivers: [
|
||||
{
|
||||
name: string;
|
||||
}
|
||||
];
|
||||
fingerprint: string;
|
||||
status: {
|
||||
state: AlertState;
|
||||
silencedBy: string[];
|
||||
inhibitedBy: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type AlertmanagerGroup = {
|
||||
labels: { [key: string]: string };
|
||||
receiver: { name: string };
|
||||
alerts: AlertmanagerAlert[];
|
||||
id: string;
|
||||
};
|
||||
|
@ -363,6 +363,12 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/silences',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/notifications',
|
||||
component: SafeDynamicImport(
|
||||
|
Loading…
Reference in New Issue
Block a user