mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 04:39:59 +08:00
Feat(UI): alerts (#363)
* chore(webpack): file-loader is added for font * chore(UI): monaco-editor is added * feat(UI): Editor component is added * feat(UI): List All Alerts is updated * feat(UI): Create Alert is updated * feat(API): create alert api is added * feat(page): EditRules is added * feat(UI): Alerts WIP * chore(typescript): typing are updated * update(UI): useFetch hook is updated * chore(UI): component for alerts is updated * chore(UI): create alert is updated * feat(UI): delete alert is now added * feat(api): Delete api is added * chore(route): edit rule route is updated * update(UI): get getAll put Alert functionality is added * update(UI): Alert Channels is updated in setting tab * chore(UI): alerts api is updated * chore(UI): getGroup api is updated * chore(UI): chprev api is updated * chore(UI): getGroup interface is exportable * feat(UI):Alerts is added * temp * feat(UI): triggered alerts is added * chore(UI): deafault key for the alert is updated * chore(UI): alerts linting is fixed * chore(UI): alerts linting is fixed * chore(UI): sort order is implemented * feat(FE): channels WIP * feat(UI): slack ui is updated * Channels is updated * feat(UI): slack ui is updated * fix(ROUTES): Channels have a seperate route * fix(build): production build is fixed by adding the file loader * fix(UI): create slack config is updated * fix(BUG): delete alert rule is fixed * fix(bug): after successfull edit user is navigated to all rules * fix(bug): alert is updated * fix(bug): expandable row is updated * fix(bug): filter and grouping of the alerts is fixed * chore(alerts): default title and description of the channels is updated * fix(UI): filtering is fixed * fix(UI): baseUrl is redirected to the nginx and text is updated * fix(BUG): destoryed the inactive pane * chore(UI): placeholder for the triggered alerts is updated * chore(FE): placeholder is updated * chore(UI): placeholder is updated for the create alert
This commit is contained in:
parent
556914f808
commit
e2a5729c5e
@ -73,6 +73,7 @@
|
||||
"jest-circus": "26.6.0",
|
||||
"jest-resolve": "26.6.0",
|
||||
"jest-watch-typeahead": "0.6.1",
|
||||
"monaco-editor": "^0.30.0",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-normalize": "8.0.1",
|
||||
|
@ -4,7 +4,7 @@ import ROUTES from 'constants/routes';
|
||||
import AppLayout from 'container/AppLayout';
|
||||
import history from 'lib/history';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Redirect, Route, Router, Switch, } from 'react-router-dom';
|
||||
import { Redirect, Route, Router, Switch } from 'react-router-dom';
|
||||
|
||||
import routes from './routes';
|
||||
|
||||
|
@ -70,3 +70,28 @@ export const DashboardWidget = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
||||
);
|
||||
|
||||
export const EditRulesPage = Loadable(
|
||||
() => import(/* webpackChunkName: "Alerts Edit Page" */ 'pages/EditRules'),
|
||||
);
|
||||
|
||||
export const ListAllALertsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "All Alerts Page" */ 'pages/AlertList'),
|
||||
);
|
||||
|
||||
export const CreateNewAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
|
||||
);
|
||||
|
||||
export const CreateAlertChannelAlerts = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
||||
);
|
||||
|
||||
export const EditAlertChannelsAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
|
||||
);
|
||||
|
||||
export const AllAlertChannels = Loadable(
|
||||
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
|
||||
);
|
||||
|
@ -3,8 +3,14 @@ import DashboardWidget from 'pages/DashboardWidget';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AllAlertChannels,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
DashboardPage,
|
||||
EditAlertChannelsAlerts,
|
||||
EditRulesPage,
|
||||
InstrumentationPage,
|
||||
ListAllALertsPage,
|
||||
NewDashboardPage,
|
||||
ServiceMapPage,
|
||||
ServiceMetricsPage,
|
||||
@ -78,11 +84,41 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
component: DashboardWidget,
|
||||
},
|
||||
{
|
||||
path: ROUTES.EDIT_ALERTS,
|
||||
exact: true,
|
||||
component: EditRulesPage,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIST_ALL_ALERT,
|
||||
exact: true,
|
||||
component: ListAllALertsPage,
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERTS_NEW,
|
||||
exact: true,
|
||||
component: CreateNewAlerts,
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE,
|
||||
exact: true,
|
||||
component: TraceDetailPages,
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_NEW,
|
||||
exact: true,
|
||||
component: CreateAlertChannelAlerts,
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_EDIT,
|
||||
exact: true,
|
||||
component: EditAlertChannelsAlerts,
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALL_CHANNELS,
|
||||
exact: true,
|
||||
component: AllAlertChannels,
|
||||
},
|
||||
];
|
||||
|
||||
interface AppRoutes {
|
||||
|
26
frontend/src/api/alerts/create.ts
Normal file
26
frontend/src/api/alerts/create.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/create';
|
||||
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/rules', {
|
||||
data: props.query,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
24
frontend/src/api/alerts/delete.ts
Normal file
24
frontend/src/api/alerts/delete.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
||||
|
||||
const deleteAlerts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/rules/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data.rules,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteAlerts;
|
24
frontend/src/api/alerts/get.ts
Normal file
24
frontend/src/api/alerts/get.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/rules/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
24
frontend/src/api/alerts/getAll.ts
Normal file
24
frontend/src/api/alerts/getAll.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/alerts/getAll';
|
||||
|
||||
const getAll = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get('/rules');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data.rules,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
31
frontend/src/api/alerts/getGroup.ts
Normal file
31
frontend/src/api/alerts/getGroup.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { apiV1, AxiosAlertManagerInstance } from 'api';
|
||||
import { apiV2 } from 'api/apiV1';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/getGroups';
|
||||
|
||||
const getGroups = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const queryParams = Object.keys(props)
|
||||
.map((e) => `${e}=${props[e]}`)
|
||||
.join('&');
|
||||
|
||||
const response = await AxiosAlertManagerInstance.get(
|
||||
`/alerts/groups?${queryParams}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getGroups;
|
26
frontend/src/api/alerts/put.ts
Normal file
26
frontend/src/api/alerts/put.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/put';
|
||||
|
||||
const put = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/rules/${props.id}`, {
|
||||
data: props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default put;
|
@ -1,3 +1,4 @@
|
||||
const apiV1 = '/api/v1/';
|
||||
export const apiV2 = '/api/alertmanager';
|
||||
|
||||
export default apiV1;
|
||||
|
35
frontend/src/api/channels/createSlack.ts
Normal file
35
frontend/src/api/channels/createSlack.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createSlack';
|
||||
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/channels', {
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
24
frontend/src/api/channels/delete.ts
Normal file
24
frontend/src/api/channels/delete.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/delete';
|
||||
|
||||
const deleteChannel = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/channels/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteChannel;
|
35
frontend/src/api/channels/editSlack.ts
Normal file
35
frontend/src/api/channels/editSlack.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editSlack';
|
||||
|
||||
const editSlack = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default editSlack;
|
24
frontend/src/api/channels/get.ts
Normal file
24
frontend/src/api/channels/get.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/get';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/channels/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
24
frontend/src/api/channels/getAll.ts
Normal file
24
frontend/src/api/channels/getAll.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/channels/getAll';
|
||||
|
||||
const getAll = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get('/channels');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
@ -1,10 +1,14 @@
|
||||
import axios from 'axios';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
|
||||
import apiV1 from './apiV1';
|
||||
import apiV1, { apiV2 } from './apiV1';
|
||||
|
||||
export default axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
});
|
||||
|
||||
export const AxiosAlertManagerInstance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV2}`,
|
||||
});
|
||||
|
||||
export { apiV1 };
|
||||
|
45
frontend/src/components/Editor/index.tsx
Normal file
45
frontend/src/components/Editor/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import * as monaco from 'monaco-editor';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
const Editor = ({ value }: EditorProps) => {
|
||||
const divEl = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
||||
|
||||
useEffect(() => {
|
||||
let editor = editorRef.current;
|
||||
|
||||
if (divEl.current) {
|
||||
editor = monaco.editor.create(divEl.current, {
|
||||
value: value.current || '',
|
||||
useShadowDOM: true,
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
fontSize: 16,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
language: 'yaml',
|
||||
});
|
||||
}
|
||||
|
||||
editor?.getModel()?.onDidChangeContent(() => {
|
||||
value.current = editor?.getValue() || '';
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
}
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
return <Container ref={divEl} />;
|
||||
};
|
||||
|
||||
interface EditorProps {
|
||||
value: React.MutableRefObject<string>;
|
||||
}
|
||||
|
||||
export default Editor;
|
8
frontend/src/components/Editor/styles.ts
Normal file
8
frontend/src/components/Editor/styles.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
&&& {
|
||||
min-height: 40vh;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
@ -12,6 +12,12 @@ const ROUTES = {
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||
EDIT_ALERTS: '/alerts/edit/:ruleId',
|
||||
LIST_ALL_ALERT: '/alerts',
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/setting/channels/new',
|
||||
CHANNELS_EDIT: '/setting/channels/edit/:id',
|
||||
};
|
||||
|
||||
export default ROUTES;
|
||||
|
64
frontend/src/container/AllAlertChannels/AlertChannels.tsx
Normal file
64
frontend/src/container/AllAlertChannels/AlertChannels.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Button, notification, Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Channels, PayloadProps } from 'types/api/channels/getAll';
|
||||
|
||||
import Delete from './Delete';
|
||||
|
||||
const AlertChannels = ({ allChannels }: AlertChannelsProps): JSX.Element => {
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const [channels, setChannels] = useState<Channels[]>(allChannels);
|
||||
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.replace(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
id,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const columns: ColumnsType<Channels> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
render: (id: string): JSX.Element => (
|
||||
<>
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
Edit
|
||||
</Button>
|
||||
<Delete id={id} setChannels={setChannels} notifications={notifications} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Table rowKey="id" dataSource={channels} columns={columns} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface AlertChannelsProps {
|
||||
allChannels: PayloadProps;
|
||||
}
|
||||
|
||||
export default AlertChannels;
|
61
frontend/src/container/AllAlertChannels/Delete.tsx
Normal file
61
frontend/src/container/AllAlertChannels/Delete.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Button } from 'antd';
|
||||
import { NotificationInstance } from 'antd/lib/notification';
|
||||
import deleteAlert from 'api/channels/delete';
|
||||
import React, { useState } from 'react';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
const Delete = ({
|
||||
notifications,
|
||||
setChannels,
|
||||
id,
|
||||
}: DeleteProps): JSX.Element => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await deleteAlert({
|
||||
id,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Channel Deleted Successfully',
|
||||
});
|
||||
setChannels((preChannels) => preChannels.filter((e) => e.id !== id));
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: error.toString() || 'Something went wrong',
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
type="link"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeleteProps {
|
||||
notifications: NotificationInstance;
|
||||
setChannels: React.Dispatch<React.SetStateAction<Channels[]>>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default Delete;
|
46
frontend/src/container/AllAlertChannels/index.tsx
Normal file
46
frontend/src/container/AllAlertChannels/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import getAll from 'api/channels/getAll';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback } from 'react';
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
import AlertChannlesComponent from './AlertChannels';
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
const AlertChannels = (): JSX.Element => {
|
||||
const onToggleHandler = useCallback(() => {
|
||||
history.push(ROUTES.CHANNELS_NEW);
|
||||
}, []);
|
||||
|
||||
const { loading, payload, error, errorMessage } = useFetch(getAll);
|
||||
|
||||
if (error) {
|
||||
return <Typography>{errorMessage}</Typography>;
|
||||
}
|
||||
|
||||
if (loading || payload === undefined) {
|
||||
return <Spinner tip="Loading Channels.." height={'90vh'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
<Paragraph ellipsis type="secondary">
|
||||
The latest added channel is used as the default channel for sending alerts
|
||||
</Paragraph>
|
||||
|
||||
<Button onClick={onToggleHandler} icon={<PlusOutlined />}>
|
||||
New Alert Channel
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
<AlertChannlesComponent allChannels={payload} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertChannels;
|
11
frontend/src/container/AllAlertChannels/styles.ts
Normal file
11
frontend/src/container/AllAlertChannels/styles.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
10
frontend/src/container/CreateAlertChannels/config.ts
Normal file
10
frontend/src/container/CreateAlertChannels/config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface SlackChannel {
|
||||
send_resolved: boolean;
|
||||
api_url: string;
|
||||
channel: string;
|
||||
title: string;
|
||||
text: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ChannelType = 'slack' | 'email';
|
116
frontend/src/container/CreateAlertChannels/index.tsx
Normal file
116
frontend/src/container/CreateAlertChannels/index.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import createSlackApi from 'api/channels/createSlack';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { ChannelType, SlackChannel } from './config';
|
||||
|
||||
const CreateAlertChannels = ({
|
||||
preType = 'slack',
|
||||
}: CreateAlertChannelsProps): JSX.Element => {
|
||||
const [formInstance] = Form.useForm();
|
||||
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
|
||||
text: ` {{ range .Alerts -}}
|
||||
*Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
*Description:* {{ .Annotations.description }}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
||||
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
|
||||
{{" "}}(
|
||||
{{- with .CommonLabels.Remove .GroupLabels.Names }}
|
||||
{{- range $index, $label := .SortedPairs -}}
|
||||
{{ if $index }}, {{ end }}
|
||||
{{- $label.Name }}="{{ $label.Value -}}"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
)
|
||||
{{- end }}`,
|
||||
});
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [notifications, NotificationElement] = notification.useNotification();
|
||||
|
||||
const [type, setType] = useState<ChannelType>(preType);
|
||||
const onTypeChangeHandler = useCallback((value: string) => {
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
const onTestHandler = useCallback(() => {
|
||||
console.log('test');
|
||||
}, []);
|
||||
|
||||
const onSlackHandler = useCallback(async () => {
|
||||
try {
|
||||
setSavingState(true);
|
||||
const response = await createSlackApi({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Successfully created the channel',
|
||||
});
|
||||
setTimeout(() => {
|
||||
history.replace(ROUTES.SETTINGS);
|
||||
}, 2000);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || 'Error while creating the channel',
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
} catch (error) {
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [notifications, selectedConfig]);
|
||||
|
||||
const onSaveHandler = useCallback(
|
||||
async (value: ChannelType) => {
|
||||
if (value == 'slack') {
|
||||
onSlackHandler();
|
||||
}
|
||||
},
|
||||
[onSlackHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormAlertChannels
|
||||
{...{
|
||||
formInstance,
|
||||
onTypeChangeHandler,
|
||||
setSelectedConfig,
|
||||
type,
|
||||
onTestHandler,
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
NotificationElement,
|
||||
title: 'New Notification Channels',
|
||||
initialValue: {
|
||||
type: type,
|
||||
...selectedConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface CreateAlertChannelsProps {
|
||||
preType?: ChannelType;
|
||||
}
|
||||
|
||||
export default CreateAlertChannels;
|
117
frontend/src/container/EditAlertChannels/index.tsx
Normal file
117
frontend/src/container/EditAlertChannels/index.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import editSlackApi from 'api/channels/editSlack';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
SlackChannel,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import history from 'lib/history';
|
||||
import { Store } from 'rc-field-form/lib/interface';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { ToggleSettingsTab } from 'store/actions';
|
||||
import AppActions from 'types/actions';
|
||||
import { SettingTab } from 'types/reducer/app';
|
||||
|
||||
const EditAlertChannels = ({
|
||||
initialValue,
|
||||
toggleSettingsTab,
|
||||
}: EditAlertChannelsProps): JSX.Element => {
|
||||
const [formInstance] = Form.useForm();
|
||||
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
|
||||
...initialValue,
|
||||
});
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [notifications, NotificationElement] = notification.useNotification();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const [type, setType] = useState<ChannelType>('slack');
|
||||
|
||||
const onTypeChangeHandler = useCallback((value: string) => {
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
const onSlackEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
const response = await editSlackApi({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
id,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Channels Edited Successfully',
|
||||
});
|
||||
toggleSettingsTab('Alert Channels');
|
||||
|
||||
setTimeout(() => {
|
||||
history.replace(ROUTES.SETTINGS);
|
||||
}, 2000);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || 'error while updating the Channels',
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
}, [selectedConfig, notifications, toggleSettingsTab, id]);
|
||||
|
||||
const onSaveHandler = useCallback(
|
||||
(value: ChannelType) => {
|
||||
if (value === 'slack') {
|
||||
onSlackEditHandler();
|
||||
}
|
||||
},
|
||||
[onSlackEditHandler],
|
||||
);
|
||||
|
||||
const onTestHandler = useCallback(() => {
|
||||
console.log('test');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormAlertChannels
|
||||
{...{
|
||||
formInstance,
|
||||
onTypeChangeHandler,
|
||||
setSelectedConfig,
|
||||
type,
|
||||
onTestHandler,
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
NotificationElement,
|
||||
title: 'Edit Notification Channels',
|
||||
initialValue,
|
||||
nameDisable: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
toggleSettingsTab: (props: SettingTab) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
toggleSettingsTab: bindActionCreators(ToggleSettingsTab, dispatch),
|
||||
});
|
||||
|
||||
interface EditAlertChannelsProps extends DispatchProps {
|
||||
initialValue: Store;
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(EditAlertChannels);
|
103
frontend/src/container/EditRules/index.tsx
Normal file
103
frontend/src/container/EditRules/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { SaveFilled } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { notification } from 'antd';
|
||||
import put from 'api/alerts/put';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/get';
|
||||
import { PayloadProps as PutPayloadProps } from 'types/api/alerts/put';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
const EditRules = ({ initialData, ruleId }: EditRulesProps): JSX.Element => {
|
||||
const value = useRef<string>(initialData);
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const [editButtonState, setEditButtonState] = useState<State<PutPayloadProps>>(
|
||||
{
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(async () => {
|
||||
try {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
const response = await put({
|
||||
data: value.current,
|
||||
id: parseInt(ruleId, 10),
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was Edited correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
error: true,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
}, [ruleId, notifications]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Editor value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={editButtonState.loading || false}
|
||||
disabled={editButtonState.loading || false}
|
||||
icon={<SaveFilled />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface EditRulesProps {
|
||||
initialData: PayloadProps['data'];
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
export default EditRules;
|
11
frontend/src/container/EditRules/styles.ts
Normal file
11
frontend/src/container/EditRules/styles.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
70
frontend/src/container/FormAlertChannels/Settings/Slack.tsx
Normal file
70
frontend/src/container/FormAlertChannels/Settings/Slack.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Input } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
|
||||
import { SlackChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const Slack = ({ setSelectedConfig }: SlackProps): JSX.Element => (
|
||||
<>
|
||||
<FormItem name="api_url" label="Webhook URL">
|
||||
<Input
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
api_url: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="channel"
|
||||
help={
|
||||
'Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace),'
|
||||
}
|
||||
label="Recipient"
|
||||
>
|
||||
<Input
|
||||
onChange={(event): void =>
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
channel: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="title" label="Title">
|
||||
<TextArea
|
||||
rows={4}
|
||||
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
|
||||
onChange={(event): void =>
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
title: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="text" label="Description">
|
||||
<TextArea
|
||||
onChange={(event): void =>
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
text: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="description"
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
|
||||
interface SlackProps {
|
||||
setSelectedConfig: React.Dispatch<React.SetStateAction<Partial<SlackChannel>>>;
|
||||
}
|
||||
|
||||
export default Slack;
|
103
frontend/src/container/FormAlertChannels/index.tsx
Normal file
103
frontend/src/container/FormAlertChannels/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { Form, FormInstance, Input, Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import {
|
||||
ChannelType,
|
||||
SlackChannel,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import React from 'react';
|
||||
const { Option } = Select;
|
||||
const { Title } = Typography;
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Store } from 'rc-field-form/lib/interface';
|
||||
|
||||
import SlackSettings from './Settings/Slack';
|
||||
import { Button } from './styles';
|
||||
|
||||
const FormAlertChannels = ({
|
||||
formInstance,
|
||||
type,
|
||||
setSelectedConfig,
|
||||
onTypeChangeHandler,
|
||||
onTestHandler,
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
NotificationElement,
|
||||
title,
|
||||
initialValue,
|
||||
nameDisable = false,
|
||||
}: FormAlertChannelsProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{NotificationElement}
|
||||
|
||||
<Title level={3}>{title}</Title>
|
||||
|
||||
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
|
||||
<FormItem label="Name" labelAlign="left" name="name">
|
||||
<Input
|
||||
disabled={nameDisable}
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
...state,
|
||||
name: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Type" labelAlign="left" name="type">
|
||||
<Select onChange={onTypeChangeHandler} value={type}>
|
||||
<Option value="slack" key="slack">
|
||||
Slack
|
||||
</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
{type === 'slack' && (
|
||||
<SlackSettings setSelectedConfig={setSelectedConfig} />
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Button
|
||||
disabled={savingState}
|
||||
loading={savingState}
|
||||
type="primary"
|
||||
onClick={(): void => onSaveHandler(type)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/* <Button onClick={onTestHandler}>Test</Button> */}
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
history.replace(ROUTES.SETTINGS);
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormAlertChannelsProps {
|
||||
formInstance: FormInstance;
|
||||
type: ChannelType;
|
||||
setSelectedConfig: React.Dispatch<React.SetStateAction<Partial<SlackChannel>>>;
|
||||
onTypeChangeHandler: (value: ChannelType) => void;
|
||||
onTestHandler: () => void;
|
||||
onSaveHandler: (props: ChannelType) => void;
|
||||
savingState: boolean;
|
||||
NotificationElement: React.ReactElement<
|
||||
any,
|
||||
string | React.JSXElementConstructor<any>
|
||||
>;
|
||||
title: string;
|
||||
initialValue: Store;
|
||||
nameDisable?: boolean;
|
||||
}
|
||||
|
||||
export default FormAlertChannels;
|
8
frontend/src/container/FormAlertChannels/styles.ts
Normal file
8
frontend/src/container/FormAlertChannels/styles.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Button as ButtonComponent } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Button = styled(ButtonComponent)`
|
||||
&&& {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
92
frontend/src/container/ListAlertRules/DeleteAlert.tsx
Normal file
92
frontend/src/container/ListAlertRules/DeleteAlert.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Button } from 'antd';
|
||||
import { NotificationInstance } from 'antd/lib/notification/index';
|
||||
import deleteAlerts from 'api/alerts/delete';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import React, { useState } from 'react';
|
||||
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
const DeleteAlert = ({
|
||||
id,
|
||||
setData,
|
||||
notifications,
|
||||
}: DeleteAlertProps): JSX.Element => {
|
||||
const [deleteAlertState, setDeleteAlertState] = useState<
|
||||
State<DeleteAlertPayloadProps>
|
||||
>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const onDeleteHandler = async (id: number): Promise<void> => {
|
||||
try {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
const response = await deleteAlerts({
|
||||
id,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setData((state) => state.filter((alert) => alert.id !== id));
|
||||
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
});
|
||||
} else {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: 'Something went wrong',
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: 'Something went wrong',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
disabled={deleteAlertState.loading || false}
|
||||
loading={deleteAlertState.loading || false}
|
||||
onClick={(): Promise<void> => onDeleteHandler(id)}
|
||||
type="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeleteAlertProps {
|
||||
id: Alerts['id'];
|
||||
setData: React.Dispatch<React.SetStateAction<Alerts[]>>;
|
||||
notifications: NotificationInstance;
|
||||
}
|
||||
|
||||
export default DeleteAlert;
|
133
frontend/src/container/ListAlertRules/ListAlert.tsx
Normal file
133
frontend/src/container/ListAlertRules/ListAlert.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, notification, Tag, Typography } from 'antd';
|
||||
import Table, { ColumnsType } from 'antd/lib/table';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import DeleteAlert from './DeleteAlert';
|
||||
import { ButtonContainer } from './styles';
|
||||
import Status from './TableComponents/Status';
|
||||
|
||||
const ListAlert = ({ allAlertRules }: ListAlertProps): JSX.Element => {
|
||||
const [data, setData] = useState<Alerts[]>(allAlertRules || []);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}, []);
|
||||
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
|
||||
const onEditHandler = (id: string): void => {
|
||||
history.push(
|
||||
generatePath(ROUTES.EDIT_ALERTS, {
|
||||
ruleId: id,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Alerts> = [
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
sorter: (a, b): number =>
|
||||
b.labels.severity.length - a.labels.severity.length,
|
||||
render: (value): JSX.Element => <Status status={value} />,
|
||||
},
|
||||
{
|
||||
title: 'Alert Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: (a, b): number => a.name.length - b.name.length,
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
dataIndex: 'labels',
|
||||
key: 'severity',
|
||||
sorter: (a, b): number =>
|
||||
a.labels['severity'].length - b.labels['severity'].length,
|
||||
render: (value): JSX.Element => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';
|
||||
const severityValue = value[withSeverityKey];
|
||||
|
||||
return <Typography>{severityValue}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'labels',
|
||||
key: 'tags',
|
||||
align: 'center',
|
||||
sorter: (a, b): number => {
|
||||
const alength = Object.keys(a.labels).filter((e) => e !== 'severity')
|
||||
.length;
|
||||
const blength = Object.keys(b.labels).filter((e) => e !== 'severity')
|
||||
.length;
|
||||
|
||||
return blength - alength;
|
||||
},
|
||||
render: (value): JSX.Element => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
|
||||
|
||||
if (withOutSeverityKeys.length === 0) {
|
||||
return <Typography>-</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return (
|
||||
<Tag key={e} color="magenta">
|
||||
{e}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
render: (id: Alerts['id']): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<DeleteAlert notifications={notifications} setData={setData} id={id} />
|
||||
|
||||
<Button onClick={(): void => onEditHandler(id.toString())} type="link">
|
||||
Edit
|
||||
</Button>
|
||||
{/* <Button type="link">Pause</Button> */}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<ButtonContainer>
|
||||
<Button onClick={onClickNewAlertHandler} icon={<PlusOutlined />}>
|
||||
New Alert
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
<Table rowKey="id" columns={columns} dataSource={data} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ListAlertProps {
|
||||
allAlertRules: Alerts[];
|
||||
}
|
||||
|
||||
export default ListAlert;
|
@ -0,0 +1,29 @@
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
const Status = ({ status }: StatusProps): JSX.Element => {
|
||||
switch (status) {
|
||||
case 'inactive': {
|
||||
return <Tag color="green">OK</Tag>;
|
||||
}
|
||||
|
||||
case 'pending': {
|
||||
return <Tag color="orange">Pending</Tag>;
|
||||
}
|
||||
|
||||
case 'firing': {
|
||||
return <Tag color="red">Firing</Tag>;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <Tag color="default">Unknown Status</Tag>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface StatusProps {
|
||||
status: Alerts['state'];
|
||||
}
|
||||
|
||||
export default Status;
|
32
frontend/src/container/ListAlertRules/index.tsx
Normal file
32
frontend/src/container/ListAlertRules/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import Spinner from 'components/Spinner';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import React from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/getAll';
|
||||
|
||||
import ListAlert from './ListAlert';
|
||||
|
||||
const ListAlertRules = (): JSX.Element => {
|
||||
const { loading, payload, error, errorMessage } = useFetch<
|
||||
PayloadProps,
|
||||
undefined
|
||||
>(getAll);
|
||||
|
||||
if (error) {
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
|
||||
if (loading || payload === undefined) {
|
||||
return <Spinner height="75vh" tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListAlert
|
||||
{...{
|
||||
allAlertRules: payload,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListAlertRules;
|
9
frontend/src/container/ListAlertRules/styles.ts
Normal file
9
frontend/src/container/ListAlertRules/styles.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
`;
|
7
frontend/src/container/MapAlertChannels/index.tsx
Normal file
7
frontend/src/container/MapAlertChannels/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const MapAlertChannels = () => {
|
||||
return <div>MapAlertChannels</div>;
|
||||
};
|
||||
|
||||
export default MapAlertChannels;
|
68
frontend/src/container/SettingsWrapper/index.tsx
Normal file
68
frontend/src/container/SettingsWrapper/index.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { Tabs } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { ToggleSettingsTab } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import AppReducer, { SettingTab } from 'types/reducer/app';
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const SettingsWrapper = ({
|
||||
AlertChannels,
|
||||
General,
|
||||
toggleSettingsTab,
|
||||
}: SettingsWrapperProps): JSX.Element => {
|
||||
const { settingsActiveTab } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(value: SettingTab) => {
|
||||
toggleSettingsTab(value);
|
||||
if (value === 'General') {
|
||||
history.push(ROUTES.SETTINGS);
|
||||
}
|
||||
|
||||
if (value === 'Alert Channels') {
|
||||
history.push(ROUTES.ALL_CHANNELS);
|
||||
}
|
||||
},
|
||||
[toggleSettingsTab],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
onChange={(value): void => onChangeHandler(value as SettingTab)}
|
||||
activeKey={settingsActiveTab}
|
||||
>
|
||||
<TabPane tab="General" key="General">
|
||||
<General />
|
||||
</TabPane>
|
||||
<TabPane tab="Alert Channels" key="Alert Channels">
|
||||
<AlertChannels />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
toggleSettingsTab: (props: SettingTab) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
toggleSettingsTab: bindActionCreators(ToggleSettingsTab, dispatch),
|
||||
});
|
||||
|
||||
interface SettingsWrapperProps extends DispatchProps {
|
||||
General: () => JSX.Element;
|
||||
AlertChannels: () => JSX.Element;
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SettingsWrapper);
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
AlertOutlined,
|
||||
AlignLeftOutlined,
|
||||
ApiOutlined,
|
||||
BarChartOutlined,
|
||||
@ -25,6 +26,11 @@ const menus: SidebarMenu[] = [
|
||||
to: ROUTES.ALL_DASHBOARD,
|
||||
name: 'Dashboard',
|
||||
},
|
||||
{
|
||||
Icon: AlertOutlined,
|
||||
to: ROUTES.LIST_ALL_ALERT,
|
||||
name: 'Alerts',
|
||||
},
|
||||
{
|
||||
to: ROUTES.SERVICE_MAP,
|
||||
name: 'Service Map',
|
||||
|
111
frontend/src/container/TriggeredAlerts/Filter.tsx
Normal file
111
frontend/src/container/TriggeredAlerts/Filter.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { Tag } from 'antd';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import { Container, Select } from './styles';
|
||||
|
||||
const Filter = ({
|
||||
setSelectedFilter,
|
||||
setSelectedGroup,
|
||||
allAlerts,
|
||||
selectedGroup,
|
||||
selectedFilter,
|
||||
}: FilterProps): JSX.Element => {
|
||||
const onChangeSelectGroupHandler = useCallback(
|
||||
(value: string[]) => {
|
||||
setSelectedGroup(
|
||||
value.map((e) => ({
|
||||
value: e,
|
||||
})),
|
||||
);
|
||||
},
|
||||
[setSelectedGroup],
|
||||
);
|
||||
|
||||
const onChangeSelectedFilterHandler = useCallback(
|
||||
(value: string[]) => {
|
||||
setSelectedFilter(
|
||||
value.map((e) => ({
|
||||
value: e,
|
||||
})),
|
||||
);
|
||||
},
|
||||
[setSelectedFilter],
|
||||
);
|
||||
|
||||
const uniqueLabels: Array<string> = useMemo(() => {
|
||||
const allLabelsSet = new Set<string>();
|
||||
allAlerts.forEach((e) =>
|
||||
Object.keys(e.labels).map((e) => {
|
||||
allLabelsSet.add(e);
|
||||
}),
|
||||
);
|
||||
return [...allLabelsSet];
|
||||
}, [allAlerts]);
|
||||
|
||||
const options = uniqueLabels.map((e) => ({
|
||||
value: e,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Select
|
||||
allowClear
|
||||
onChange={onChangeSelectedFilterHandler}
|
||||
mode="tags"
|
||||
value={selectedFilter.map((e) => e.value)}
|
||||
placeholder="Filter by Tags - e.g. severity:warning, alertname:Sample Alert"
|
||||
tagRender={(props): JSX.Element => {
|
||||
const { label, closable, onClose } = props;
|
||||
return (
|
||||
<Tag
|
||||
color={'magenta'}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
options={[]}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
onChange={onChangeSelectGroupHandler}
|
||||
mode="tags"
|
||||
defaultValue={selectedGroup.map((e) => e.value)}
|
||||
showArrow
|
||||
placeholder="Group by any tag"
|
||||
tagRender={(props): JSX.Element => {
|
||||
const { label, closable, onClose } = props;
|
||||
return (
|
||||
<Tag
|
||||
color={'magenta'}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterProps {
|
||||
setSelectedFilter: React.Dispatch<React.SetStateAction<Array<Value>>>;
|
||||
setSelectedGroup: React.Dispatch<React.SetStateAction<Array<Value>>>;
|
||||
allAlerts: Alerts[];
|
||||
selectedGroup: Array<Value>;
|
||||
selectedFilter: Array<Value>;
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default Filter;
|
@ -0,0 +1,73 @@
|
||||
import { Tag, Typography } from 'antd';
|
||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||
import getFormattedDate from 'lib/getFormatedDate';
|
||||
import React from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import Status from '../TableComponents/AlertStatus';
|
||||
import { TableCell, TableRow } from './styles';
|
||||
|
||||
const ExapandableRow = ({ allAlerts }: ExapandableRowProps): JSX.Element => (
|
||||
<>
|
||||
{allAlerts.map((alert) => {
|
||||
const labels = alert.labels;
|
||||
const labelsObject = Object.keys(labels);
|
||||
|
||||
const tags = labelsObject.filter((e) => e !== 'severity');
|
||||
|
||||
const formatedDate = new Date(alert.startsAt);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
bodyStyle={{
|
||||
minHeight: '5rem',
|
||||
marginLeft: '2rem',
|
||||
}}
|
||||
translate="yes"
|
||||
hoverable
|
||||
key={alert.fingerprint}
|
||||
>
|
||||
<TableCell>
|
||||
<Status severity={alert.status.state} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography>{labels['alertname']}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography>{labels['severity']}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography>{`${getFormattedDate(formatedDate)} ${convertDateToAmAndPm(
|
||||
formatedDate,
|
||||
)}`}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div>
|
||||
{tags.map((e) => (
|
||||
<Tag key={e}>{`${e}:${labels[e]}`}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* <TableCell>
|
||||
<TableHeaderContainer>
|
||||
<Button type="link">Edit</Button>
|
||||
<Button type="link">Delete</Button>
|
||||
<Button type="link">Pause</Button>
|
||||
</TableHeaderContainer>
|
||||
</TableCell> */}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
interface ExapandableRowProps {
|
||||
allAlerts: Alerts[];
|
||||
}
|
||||
|
||||
export default ExapandableRow;
|
@ -0,0 +1,54 @@
|
||||
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
import { Tag } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import ExapandableRow from './ExapandableRow';
|
||||
import { IconContainer, StatusContainer, TableCell, TableRow } from './styles';
|
||||
|
||||
const TableRowComponent = ({
|
||||
tags,
|
||||
tagsAlert,
|
||||
}: TableRowComponentProps): JSX.Element => {
|
||||
const [isClicked, setIsClicked] = useState<boolean>(false);
|
||||
|
||||
const onClickHandler = (): void => {
|
||||
setIsClicked((state) => !state);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<StatusContainer>
|
||||
<IconContainer onClick={onClickHandler}>
|
||||
{!isClicked ? <PlusSquareOutlined /> : <MinusSquareOutlined />}
|
||||
</IconContainer>
|
||||
<>
|
||||
{tags.map((tag) => (
|
||||
<Tag color="magenta" key={tag}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
</StatusContainer>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
{/* <TableCell minWidth="200px">
|
||||
<Button type="primary">Resume Group</Button>
|
||||
</TableCell> */}
|
||||
</TableRow>
|
||||
{isClicked && <ExapandableRow allAlerts={tagsAlert} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableRowComponentProps {
|
||||
tags: string[];
|
||||
tagsAlert: Alerts[];
|
||||
}
|
||||
|
||||
export default TableRowComponent;
|
@ -0,0 +1,76 @@
|
||||
import { Dictionary } from 'lodash';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import { Value } from '../Filter';
|
||||
import { FilterAlerts } from '../utils';
|
||||
import { Container, TableHeader, TableHeaderContainer } from './styles';
|
||||
import TableRowComponent from './TableRow';
|
||||
|
||||
const FilteredTable = ({
|
||||
selectedGroup,
|
||||
allAlerts,
|
||||
selectedFilter,
|
||||
}: FilteredTableProps): JSX.Element => {
|
||||
const allGroupsAlerts: Dictionary<Alerts[]> = useMemo(
|
||||
() =>
|
||||
groupBy(FilterAlerts(allAlerts, selectedFilter), (obj) =>
|
||||
selectedGroup.map((e) => obj.labels[`${e.value}`]).join('+'),
|
||||
),
|
||||
[selectedGroup, allAlerts, selectedFilter],
|
||||
);
|
||||
|
||||
const tags = Object.keys(allGroupsAlerts);
|
||||
const tagsAlerts = Object.values(allGroupsAlerts);
|
||||
|
||||
const headers = [
|
||||
'Status',
|
||||
'Alert Name',
|
||||
'Severity',
|
||||
'Firing Since',
|
||||
'Tags',
|
||||
// 'Actions',
|
||||
];
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TableHeaderContainer>
|
||||
{headers.map((header) => (
|
||||
<TableHeader key={header}>{header}</TableHeader>
|
||||
))}
|
||||
</TableHeaderContainer>
|
||||
|
||||
{tags.map((e, index) => {
|
||||
const tagsValue = e.split('+').filter((e) => e);
|
||||
const tagsAlert: Alerts[] = tagsAlerts[index];
|
||||
|
||||
if (tagsAlert.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objects = tagsAlert[0].labels;
|
||||
const keysArray = Object.keys(objects);
|
||||
const valueArray: string[] = [];
|
||||
|
||||
keysArray.forEach((e) => {
|
||||
valueArray.push(objects[e]);
|
||||
});
|
||||
|
||||
const tags = tagsValue
|
||||
.map((e) => keysArray[valueArray.findIndex((value) => value === e) || 0])
|
||||
.map((e, index) => `${e}:${tagsValue[index]}`);
|
||||
|
||||
return <TableRowComponent key={e} tagsAlert={tagsAlert} tags={tags} />;
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilteredTableProps {
|
||||
selectedGroup: Value[];
|
||||
allAlerts: Alerts[];
|
||||
selectedFilter: Value[];
|
||||
}
|
||||
|
||||
export default FilteredTable;
|
@ -0,0 +1,64 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TableHeader = styled(Card)`
|
||||
&&& {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
.ant-card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TableHeaderContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TableRow = styled(Card)`
|
||||
&&& {
|
||||
flex: 1;
|
||||
.ant-card-body {
|
||||
padding: 0rem;
|
||||
display: flex;
|
||||
|
||||
min-height: 3rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
minWidth?: string;
|
||||
}
|
||||
export const TableCell = styled.div<Props>`
|
||||
&&& {
|
||||
flex: 1;
|
||||
min-width: ${(props): string => props.minWidth || ''};
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StatusContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
&&& {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
104
frontend/src/container/TriggeredAlerts/NoFilterTable.tsx
Normal file
104
frontend/src/container/TriggeredAlerts/NoFilterTable.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Table, Tag, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import AlertStatus from 'container/TriggeredAlerts/TableComponents/AlertStatus';
|
||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||
import getFormattedDate from 'lib/getFormatedDate';
|
||||
import React from 'react';
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import { Value } from './Filter';
|
||||
import { FilterAlerts } from './utils';
|
||||
|
||||
const NoFilterTable = ({
|
||||
allAlerts,
|
||||
selectedFilter,
|
||||
}: NoFilterTableProps): JSX.Element => {
|
||||
const filteredAlerts = FilterAlerts(allAlerts, selectedFilter);
|
||||
|
||||
// need to add the filter
|
||||
const columns: ColumnsType<Alerts> = [
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
sorter: (a, b): number =>
|
||||
b.labels.severity.length - a.labels.severity.length,
|
||||
render: (value): JSX.Element => <AlertStatus severity={value.state} />,
|
||||
},
|
||||
{
|
||||
title: 'Alert Name',
|
||||
dataIndex: 'labels',
|
||||
key: 'alertName',
|
||||
sorter: (a, b): number => a.name.length - b.name.length,
|
||||
render: (data): JSX.Element => {
|
||||
const name = data?.alertname || '';
|
||||
return <Typography>{name}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'labels',
|
||||
key: 'tags',
|
||||
render: (labels): JSX.Element => {
|
||||
const objectKeys = Object.keys(labels);
|
||||
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
|
||||
|
||||
if (withOutSeverityKeys.length === 0) {
|
||||
return <Typography>-</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return <Tag key={e} color="magenta">{`${e} : ${labels[e]}`}</Tag>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
dataIndex: 'labels',
|
||||
key: 'severity',
|
||||
sorter: (a, b): number => {
|
||||
const severityValueA = a.labels['severity'];
|
||||
const severityValueB = b.labels['severity'];
|
||||
return severityValueA.length - severityValueB.length;
|
||||
},
|
||||
render: (value): JSX.Element => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';
|
||||
const severityValue = value[withSeverityKey];
|
||||
|
||||
return <Typography>{severityValue}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Firing Since',
|
||||
dataIndex: 'startsAt',
|
||||
sorter: (a, b): number =>
|
||||
new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(),
|
||||
render: (date): JSX.Element => {
|
||||
const formatedDate = new Date(date);
|
||||
|
||||
return (
|
||||
<Typography>{`${getFormattedDate(formatedDate)} ${convertDateToAmAndPm(
|
||||
formatedDate,
|
||||
)}`}</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table rowKey="startsAt" dataSource={filteredAlerts} columns={columns} />
|
||||
);
|
||||
};
|
||||
|
||||
interface NoFilterTableProps {
|
||||
allAlerts: Alerts[];
|
||||
selectedFilter: Value[];
|
||||
}
|
||||
|
||||
export default NoFilterTable;
|
@ -0,0 +1,28 @@
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const Severity = ({ severity }: SeverityProps): JSX.Element => {
|
||||
switch (severity) {
|
||||
case 'unprocessed': {
|
||||
return <Tag color="green">UnProcessed</Tag>;
|
||||
}
|
||||
|
||||
case 'active': {
|
||||
return <Tag color="red">Firing</Tag>;
|
||||
}
|
||||
|
||||
case 'suppressed': {
|
||||
return <Tag color="red">Suppressed</Tag>;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <Tag color="default">Unknown Status</Tag>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface SeverityProps {
|
||||
severity: string;
|
||||
}
|
||||
|
||||
export default Severity;
|
63
frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx
Normal file
63
frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Group } from 'types/api/alerts/getGroups';
|
||||
|
||||
import { Value } from './Filter';
|
||||
import Filter from './Filter';
|
||||
import FilteredTable from './FilteredTable';
|
||||
import NoFilterTable from './NoFilterTable';
|
||||
import { NoTableContainer } from './styles';
|
||||
|
||||
const TriggeredAlerts = ({ allAlerts }: TriggeredAlertsProps): JSX.Element => {
|
||||
const allInitialAlerts = allAlerts?.alerts || [];
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<Value[]>([]);
|
||||
const [selectedFilter, setSelectedFilter] = useState<Value[]>([]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Filter
|
||||
{...{
|
||||
allAlerts: allInitialAlerts,
|
||||
selectedFilter,
|
||||
selectedGroup,
|
||||
setSelectedFilter,
|
||||
setSelectedGroup,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedFilter.length === 0 && selectedGroup.length === 0 ? (
|
||||
<NoTableContainer>
|
||||
<NoFilterTable
|
||||
selectedFilter={selectedFilter}
|
||||
allAlerts={allInitialAlerts}
|
||||
/>
|
||||
</NoTableContainer>
|
||||
) : (
|
||||
<>
|
||||
{selectedFilter.length !== 0 && selectedGroup.length === 0 ? (
|
||||
<NoTableContainer>
|
||||
<NoFilterTable
|
||||
selectedFilter={selectedFilter}
|
||||
allAlerts={allInitialAlerts}
|
||||
/>
|
||||
</NoTableContainer>
|
||||
) : (
|
||||
<FilteredTable
|
||||
{...{
|
||||
allAlerts: allInitialAlerts,
|
||||
selectedFilter,
|
||||
selectedGroup,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TriggeredAlertsProps {
|
||||
allAlerts: Group;
|
||||
}
|
||||
|
||||
export default TriggeredAlerts;
|
70
frontend/src/container/TriggeredAlerts/index.tsx
Normal file
70
frontend/src/container/TriggeredAlerts/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import getGroupApi from 'api/alerts/getGroup';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/getGroups';
|
||||
|
||||
import TriggerComponent from './TriggeredAlert';
|
||||
|
||||
const TriggeredAlerts = (): JSX.Element => {
|
||||
const [groupState, setGroupState] = useState<State<PayloadProps>>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
success: false,
|
||||
payload: [],
|
||||
});
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setGroupState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
const response = await getGroupApi({
|
||||
active: true,
|
||||
inhibited: true,
|
||||
silenced: false,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setGroupState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload || [],
|
||||
}));
|
||||
} else {
|
||||
setGroupState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
setGroupState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
loading: false,
|
||||
errorMessage: 'Something went wrong',
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (groupState.error) {
|
||||
return <div>{groupState.errorMessage}</div>;
|
||||
}
|
||||
|
||||
if (groupState.loading || groupState.payload === undefined) {
|
||||
return <Spinner height="75vh" tip="Loading Alerts..." />;
|
||||
}
|
||||
|
||||
return <TriggerComponent allAlerts={groupState.payload[0]} />;
|
||||
};
|
||||
|
||||
export default TriggeredAlerts;
|
29
frontend/src/container/TriggeredAlerts/styles.ts
Normal file
29
frontend/src/container/TriggeredAlerts/styles.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Select as SelectComponent } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Select = styled(SelectComponent)`
|
||||
&&& {
|
||||
min-width: 350px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TableContainer = styled.div`
|
||||
&&& {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NoTableContainer = styled.div`
|
||||
&&& {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
51
frontend/src/container/TriggeredAlerts/utils.ts
Normal file
51
frontend/src/container/TriggeredAlerts/utils.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Alerts } from 'types/api/alerts/getAll';
|
||||
|
||||
import { Value } from './Filter';
|
||||
|
||||
export const FilterAlerts = (
|
||||
allAlerts: Alerts[],
|
||||
selectedFilter: Value[],
|
||||
): Alerts[] => {
|
||||
// also we need to update the alerts
|
||||
// [[key,value]]
|
||||
|
||||
if (selectedFilter?.length === 0 || selectedFilter === undefined) {
|
||||
return allAlerts;
|
||||
}
|
||||
|
||||
const filter: string[] = [];
|
||||
|
||||
// filtering the value
|
||||
selectedFilter.forEach((e) => {
|
||||
const valueKey = e.value.split(':');
|
||||
if (valueKey.length === 2) {
|
||||
filter.push(e.value);
|
||||
}
|
||||
});
|
||||
|
||||
const tags = filter.map((e) => e.split(':'));
|
||||
const objectMap = new Map();
|
||||
|
||||
const filteredKey = tags.reduce((acc, curr) => [...acc, curr[0]], []);
|
||||
const filteredValue = tags.reduce((acc, curr) => [...acc, curr[1]], []);
|
||||
|
||||
filteredKey.forEach((key, index) =>
|
||||
objectMap.set(key.trim(), filteredValue[index].trim()),
|
||||
);
|
||||
|
||||
const filteredAlerts: Set<string> = new Set();
|
||||
|
||||
allAlerts.forEach((alert) => {
|
||||
const { labels } = alert;
|
||||
Object.keys(labels).forEach((e) => {
|
||||
const selectedKey = objectMap.get(e);
|
||||
|
||||
// alerts which does not have the key with value
|
||||
if (selectedKey && labels[e] === selectedKey) {
|
||||
filteredAlerts.add(alert.fingerprint);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return allAlerts.filter((e) => filteredAlerts.has(e.fingerprint));
|
||||
};
|
@ -22,15 +22,12 @@ function useFetch<PayloadProps, FunctionParams>(
|
||||
const loadingRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let abortController = new window.AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
try {
|
||||
(async (): Promise<void> => {
|
||||
if (state.loading) {
|
||||
const response = await functions(param);
|
||||
|
||||
if (!signal.aborted && loadingRef.current === 0) {
|
||||
if (loadingRef.current === 0) {
|
||||
loadingRef.current = 1;
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
@ -54,21 +51,19 @@ function useFetch<PayloadProps, FunctionParams>(
|
||||
}
|
||||
})();
|
||||
} catch (error) {
|
||||
if (!signal.aborted) {
|
||||
setStates({
|
||||
payload: undefined,
|
||||
loading: false,
|
||||
success: false,
|
||||
error: true,
|
||||
errorMessage: error,
|
||||
});
|
||||
}
|
||||
setStates({
|
||||
payload: undefined,
|
||||
loading: false,
|
||||
success: false,
|
||||
error: true,
|
||||
errorMessage: error,
|
||||
});
|
||||
}
|
||||
return (): void => {
|
||||
abortController.abort();
|
||||
abortController = new window.AbortController();
|
||||
loadingRef.current = 1;
|
||||
};
|
||||
}, [functions, param, state.loading]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
@ -78,7 +73,7 @@ export interface State<T> {
|
||||
loading: boolean | null;
|
||||
error: boolean | null;
|
||||
success: boolean | null;
|
||||
payload: T;
|
||||
payload?: T;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
|
21
frontend/src/pages/AlertChannelCreate/index.tsx
Normal file
21
frontend/src/pages/AlertChannelCreate/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import SettingsWrapper from 'container/SettingsWrapper';
|
||||
import React from 'react';
|
||||
|
||||
const SettingsPage = (): JSX.Element => {
|
||||
const AlertChannels = (): JSX.Element => {
|
||||
return <CreateAlertChannels />;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsWrapper
|
||||
{...{
|
||||
AlertChannels,
|
||||
General: GeneralSettings,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
34
frontend/src/pages/AlertList/index.tsx
Normal file
34
frontend/src/pages/AlertList/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Tabs } from 'antd';
|
||||
import AllAlertRules from 'container/ListAlertRules';
|
||||
// import MapAlertChannels from 'container/MapAlertChannels';
|
||||
import TriggeredAlerts from 'container/TriggeredAlerts';
|
||||
import React from 'react';
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const AllAlertList = (): JSX.Element => {
|
||||
return (
|
||||
<Tabs destroyInactiveTabPane defaultActiveKey="Alert Rules">
|
||||
<TabPane tabKey="Alert Rules" tab="Alert Rules" key="Alert Rules">
|
||||
<AllAlertRules />
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tabKey="Triggered Alerts"
|
||||
key="Triggered Alerts"
|
||||
tab="Triggered Alerts"
|
||||
>
|
||||
<TriggeredAlerts />
|
||||
</TabPane>
|
||||
|
||||
{/* <TabPane
|
||||
tabKey="Map Alert Channels"
|
||||
key="Map Alert Channels"
|
||||
tab="Map Alert Channels"
|
||||
>
|
||||
<MapAlertChannels />
|
||||
</TabPane> */}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllAlertList;
|
17
frontend/src/pages/AllAlertChannels/index.tsx
Normal file
17
frontend/src/pages/AllAlertChannels/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import SettingsWrapper from 'container/SettingsWrapper';
|
||||
import React from 'react';
|
||||
|
||||
const AllAlertChannels = (): JSX.Element => {
|
||||
return (
|
||||
<SettingsWrapper
|
||||
{...{
|
||||
AlertChannels,
|
||||
General: GeneralSettings,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllAlertChannels;
|
51
frontend/src/pages/ChannelsEdit/index.tsx
Normal file
51
frontend/src/pages/ChannelsEdit/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Typography } from 'antd';
|
||||
import get from 'api/channels/get';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SlackChannel } from 'container/CreateAlertChannels/config';
|
||||
import EditAlertChannels from 'container/EditAlertChannels';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { PayloadProps, Props } from 'types/api/channels/get';
|
||||
|
||||
const ChannelsEdit = (): JSX.Element => {
|
||||
const { id } = useParams<Params>();
|
||||
|
||||
const { errorMessage, payload, error, loading } = useFetch<
|
||||
PayloadProps,
|
||||
Props
|
||||
>(get, {
|
||||
id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Typography>{errorMessage}</Typography>;
|
||||
}
|
||||
|
||||
if (loading || payload === undefined) {
|
||||
return <Spinner tip="Loading Channels..." />;
|
||||
}
|
||||
|
||||
const { data } = payload;
|
||||
|
||||
const value = JSON.parse(data);
|
||||
|
||||
const channel: SlackChannel = value['slack_configs'][0];
|
||||
|
||||
return (
|
||||
<EditAlertChannels
|
||||
{...{
|
||||
initialValue: {
|
||||
...channel,
|
||||
type: 'slack',
|
||||
name: value.name,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default ChannelsEdit;
|
111
frontend/src/pages/CreateAlert/index.tsx
Normal file
111
frontend/src/pages/CreateAlert/index.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import createAlertsApi from 'api/alerts/create';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { PayloadProps as CreateAlertPayloadProps } from 'types/api/alerts/create';
|
||||
|
||||
import { ButtonContainer, Title } from './styles';
|
||||
|
||||
const CreateAlert = (): JSX.Element => {
|
||||
const value = useRef<string>(
|
||||
`\n alert: <alert name>\n expr: system_cpu_load_average_1m > 0.01\n for: 0m\n labels:\n severity: warning\n annotations:\n summary: High CPU load\n description: \"CPU load is > 0.01\n VALUE = {{ $value }}\n LABELS = {{ $labels }}\"\n `,
|
||||
);
|
||||
|
||||
const [newAlertState, setNewAlertState] = useState<
|
||||
State<CreateAlertPayloadProps>
|
||||
>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
payload: undefined,
|
||||
success: false,
|
||||
});
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
if (value.current.length === 0) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
description: `Oops! We didn't catch that. Please make sure the alert settings are not empty or try again`,
|
||||
message: 'Error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await createAlertsApi({
|
||||
query: value.current,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was saved correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 3000);
|
||||
} else {
|
||||
notifications.error({
|
||||
description:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io',
|
||||
message: 'Error',
|
||||
});
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io',
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message:
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Title>Create New Alert</Title>
|
||||
<Editor value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={newAlertState.loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAlert;
|
18
frontend/src/pages/CreateAlert/styles.ts
Normal file
18
frontend/src/pages/CreateAlert/styles.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Title = styled(Typography)`
|
||||
&&& {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
34
frontend/src/pages/EditRules/index.tsx
Normal file
34
frontend/src/pages/EditRules/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import get from 'api/alerts/get';
|
||||
import Spinner from 'components/Spinner';
|
||||
import EditRulesContainer from 'container/EditRules';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||
|
||||
const EditRules = () => {
|
||||
const { ruleId } = useParams<EditRulesParam>();
|
||||
|
||||
const { loading, error, payload, errorMessage } = useFetch<
|
||||
PayloadProps,
|
||||
Props
|
||||
>(get, {
|
||||
id: parseInt(ruleId),
|
||||
});
|
||||
|
||||
if (loading || payload === undefined) {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
|
||||
return <EditRulesContainer ruleId={ruleId} initialData={payload.data} />;
|
||||
};
|
||||
|
||||
interface EditRulesParam {
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
export default EditRules;
|
@ -1,22 +1,16 @@
|
||||
import { Tabs } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import SettingsWrapper from 'container/SettingsWrapper';
|
||||
import React from 'react';
|
||||
|
||||
const SettingsPage = (): JSX.Element => {
|
||||
return (
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="General" key="1">
|
||||
<GeneralSettings />
|
||||
</TabPane>
|
||||
{/* <TabPane tab="Alert Channels" key="2">
|
||||
Alerts
|
||||
</TabPane>
|
||||
<TabPane tab="Users" key="3">
|
||||
Users
|
||||
</TabPane> */}
|
||||
</Tabs>
|
||||
<SettingsWrapper
|
||||
{...{
|
||||
AlertChannels,
|
||||
General: GeneralSettings,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './toggleDarkMode';
|
||||
export * from './toggleSettingsTab';
|
||||
export * from './userLoggedIn';
|
||||
|
16
frontend/src/store/actions/app/toggleSettingsTab.ts
Normal file
16
frontend/src/store/actions/app/toggleSettingsTab.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SettingTab } from 'types/reducer/app';
|
||||
|
||||
export const ToggleSettingsTab = (
|
||||
props: SettingTab,
|
||||
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||
return (dispatch: Dispatch<AppActions>): void => {
|
||||
dispatch({
|
||||
type: 'TOGGLE_SETTINGS_TABS',
|
||||
payload: {
|
||||
activeTab: props,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
@ -1,10 +1,16 @@
|
||||
import { IS_LOGGED_IN } from 'constants/auth';
|
||||
import { AppAction, LOGGED_IN, SWITCH_DARK_MODE } from 'types/actions/app';
|
||||
import {
|
||||
AppAction,
|
||||
LOGGED_IN,
|
||||
SWITCH_DARK_MODE,
|
||||
TOGGLE_SETTINGS_TABS,
|
||||
} from 'types/actions/app';
|
||||
import InitialValueTypes from 'types/reducer/app';
|
||||
|
||||
const InitialValue: InitialValueTypes = {
|
||||
isDarkMode: true,
|
||||
isLoggedIn: localStorage.getItem(IS_LOGGED_IN) === 'yes',
|
||||
settingsActiveTab: 'General',
|
||||
};
|
||||
|
||||
const appReducer = (
|
||||
@ -26,6 +32,13 @@ const appReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case TOGGLE_SETTINGS_TABS: {
|
||||
return {
|
||||
...state,
|
||||
settingsActiveTab: action.payload.activeTab,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { SettingTab } from 'types/reducer/app';
|
||||
|
||||
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
|
||||
export const LOGGED_IN = 'LOGGED_IN';
|
||||
export const TOGGLE_SETTINGS_TABS = 'TOGGLE_SETTINGS_TABS';
|
||||
|
||||
export interface SwitchDarkMode {
|
||||
type: typeof SWITCH_DARK_MODE;
|
||||
@ -9,4 +12,11 @@ export interface LoggedInUser {
|
||||
type: typeof LOGGED_IN;
|
||||
}
|
||||
|
||||
export type AppAction = SwitchDarkMode | LoggedInUser;
|
||||
export interface ToggleSettingsTab {
|
||||
type: typeof TOGGLE_SETTINGS_TABS;
|
||||
payload: {
|
||||
activeTab: SettingTab;
|
||||
};
|
||||
}
|
||||
|
||||
export type AppAction = SwitchDarkMode | LoggedInUser | ToggleSettingsTab;
|
||||
|
8
frontend/src/types/api/alerts/create.ts
Normal file
8
frontend/src/types/api/alerts/create.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Props {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
10
frontend/src/types/api/alerts/delete.ts
Normal file
10
frontend/src/types/api/alerts/delete.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
9
frontend/src/types/api/alerts/get.ts
Normal file
9
frontend/src/types/api/alerts/get.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
data: string;
|
||||
};
|
32
frontend/src/types/api/alerts/getAll.ts
Normal file
32
frontend/src/types/api/alerts/getAll.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface Alerts {
|
||||
labels: AlertsLabel;
|
||||
annotations: {
|
||||
description: string;
|
||||
summary: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
state: string;
|
||||
name: string;
|
||||
id: number;
|
||||
endsAt: string;
|
||||
fingerprint: string;
|
||||
generatorURL: string;
|
||||
receivers: Receivers[];
|
||||
startsAt: string;
|
||||
status: {
|
||||
inhibitedBy: [];
|
||||
silencedBy: [];
|
||||
state: string;
|
||||
};
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Receivers {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AlertsLabel {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = Alerts[];
|
17
frontend/src/types/api/alerts/getGroups.ts
Normal file
17
frontend/src/types/api/alerts/getGroups.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export interface Props {
|
||||
silenced: boolean;
|
||||
inhibited: boolean;
|
||||
active: boolean;
|
||||
[key: string]: string | boolean;
|
||||
}
|
||||
export interface Group {
|
||||
alerts: Alerts[];
|
||||
label: Alerts['labels'];
|
||||
receiver: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PayloadProps = Group[];
|
9
frontend/src/types/api/alerts/put.ts
Normal file
9
frontend/src/types/api/alerts/put.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { PayloadProps as DeletePayloadProps } from './delete';
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export type PayloadProps = DeletePayloadProps;
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
data: DeletePayloadProps['data'];
|
||||
}
|
8
frontend/src/types/api/channels/createSlack.ts
Normal file
8
frontend/src/types/api/channels/createSlack.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SlackChannel } from 'container/CreateAlertChannels/config';
|
||||
|
||||
export type Props = SlackChannel;
|
||||
|
||||
export interface PayloadProps {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
10
frontend/src/types/api/channels/delete.ts
Normal file
10
frontend/src/types/api/channels/delete.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Channels } from './getAll';
|
||||
|
||||
export interface Props {
|
||||
id: Channels['id'];
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
10
frontend/src/types/api/channels/editSlack.ts
Normal file
10
frontend/src/types/api/channels/editSlack.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { SlackChannel } from 'container/CreateAlertChannels/config';
|
||||
|
||||
export interface Props extends SlackChannel {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
7
frontend/src/types/api/channels/get.ts
Normal file
7
frontend/src/types/api/channels/get.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Channels } from './getAll';
|
||||
|
||||
export interface Props {
|
||||
id: Channels['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = Channels;
|
10
frontend/src/types/api/channels/getAll.ts
Normal file
10
frontend/src/types/api/channels/getAll.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type PayloadProps = Channels[];
|
||||
|
||||
export interface Channels {
|
||||
created_at: string;
|
||||
data: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
updated_at: string;
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export type SettingTab = 'General' | 'Alert Channels';
|
||||
export default interface AppReducer {
|
||||
isDarkMode: boolean;
|
||||
isLoggedIn: boolean;
|
||||
settingsActiveTab: SettingTab;
|
||||
}
|
||||
|
11
frontend/src/typings/environment.d.ts
vendored
11
frontend/src/typings/environment.d.ts
vendored
@ -1,11 +0,0 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
FRONTEND_API_ENDPOINT: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this file has no import/export statements (i.e. is a script)
|
||||
// convert it into a module by adding an empty export statement.
|
||||
export {};
|
9
frontend/src/typings/environment.ts
Normal file
9
frontend/src/typings/environment.ts
Normal file
@ -0,0 +1,9 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
FRONTEND_API_ENDPOINT: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
@ -48,6 +48,10 @@ const config: webpack.Configuration = {
|
||||
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -69,6 +69,10 @@ const config: Configuration = {
|
||||
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -10283,6 +10283,11 @@ moment@>=2.13.0, moment@^2.24.0, moment@^2.25.3:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
monaco-editor@^0.30.0:
|
||||
version "0.30.0"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.30.0.tgz#1c7f9ba1d18c21868ce3a5413ef56351f9df7933"
|
||||
integrity sha512-/k++/ofRmwnwWTpOWYOMGVcqBrqrlt3MP0Mt/cRTQojW7A9fnekcvPQ2iIFA0YSZdPWPN9yYXrYq0xqiUuxT/A==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
Loading…
x
Reference in New Issue
Block a user