Alerting: bootstrap silences page (#32810)

pull/33012/head
Domas 4 years ago committed by GitHub
parent c9e5088e8b
commit e6a98ce1e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      pkg/api/index.go
  2. 12
      public/app/core/components/Page/Page.tsx
  3. 10
      public/app/core/components/PageHeader/PageHeader.tsx
  4. 18
      public/app/features/alerting/unified/RuleList.tsx
  5. 39
      public/app/features/alerting/unified/Silences.tsx
  6. 81
      public/app/features/alerting/unified/api/alertmanager.ts
  7. 11
      public/app/features/alerting/unified/state/actions.ts
  8. 9
      public/app/features/alerting/unified/state/reducers.ts
  9. 67
      public/app/plugins/datasource/alertmanager/types.ts
  10. 6
      public/app/routes/routes.tsx

@ -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>
)}
<div className={styles.buttonsContainer}>
<div />
<a href="/alerting/new">
<Button icon="plus">New alert rule</Button>
</a>
</div>
{dispatched && !loading && !haveResults && <NoRulesSplash />}
{!showNewAlertSplash && (
<div className={styles.buttonsContainer}>
<div />
<a href="/alerting/new">
<Button icon="plus">New alert rule</Button>
</a>
</div>
)}
{showNewAlertSplash && <NoRulesSplash />}
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
{haveResults && <SystemOrApplicationRules namespaces={systemNamespaces} />}
</AlertingPageWrapper>

@ -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…
Cancel
Save