mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 01:25:53 +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-circus": "26.6.0",
|
||||||
"jest-resolve": "26.6.0",
|
"jest-resolve": "26.6.0",
|
||||||
"jest-watch-typeahead": "0.6.1",
|
"jest-watch-typeahead": "0.6.1",
|
||||||
|
"monaco-editor": "^0.30.0",
|
||||||
"pnp-webpack-plugin": "1.6.4",
|
"pnp-webpack-plugin": "1.6.4",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
"postcss-normalize": "8.0.1",
|
"postcss-normalize": "8.0.1",
|
||||||
|
@ -4,7 +4,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { Suspense } from 'react';
|
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';
|
import routes from './routes';
|
||||||
|
|
||||||
|
@ -70,3 +70,28 @@ export const DashboardWidget = Loadable(
|
|||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
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 { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AllAlertChannels,
|
||||||
|
CreateAlertChannelAlerts,
|
||||||
|
CreateNewAlerts,
|
||||||
DashboardPage,
|
DashboardPage,
|
||||||
|
EditAlertChannelsAlerts,
|
||||||
|
EditRulesPage,
|
||||||
InstrumentationPage,
|
InstrumentationPage,
|
||||||
|
ListAllALertsPage,
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
ServiceMapPage,
|
ServiceMapPage,
|
||||||
ServiceMetricsPage,
|
ServiceMetricsPage,
|
||||||
@ -78,11 +84,41 @@ const routes: AppRoutes[] = [
|
|||||||
exact: true,
|
exact: true,
|
||||||
component: DashboardWidget,
|
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,
|
path: ROUTES.TRACE,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: TraceDetailPages,
|
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 {
|
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/';
|
const apiV1 = '/api/v1/';
|
||||||
|
export const apiV2 = '/api/alertmanager';
|
||||||
|
|
||||||
export default apiV1;
|
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 axios from 'axios';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
|
|
||||||
import apiV1 from './apiV1';
|
import apiV1, { apiV2 } from './apiV1';
|
||||||
|
|
||||||
export default axios.create({
|
export default axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AxiosAlertManagerInstance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV2}`,
|
||||||
|
});
|
||||||
|
|
||||||
export { apiV1 };
|
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',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
DASHBOARD: '/dashboard/:dashboardId',
|
DASHBOARD: '/dashboard/:dashboardId',
|
||||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
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;
|
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 {
|
import {
|
||||||
|
AlertOutlined,
|
||||||
AlignLeftOutlined,
|
AlignLeftOutlined,
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
@ -25,6 +26,11 @@ const menus: SidebarMenu[] = [
|
|||||||
to: ROUTES.ALL_DASHBOARD,
|
to: ROUTES.ALL_DASHBOARD,
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Icon: AlertOutlined,
|
||||||
|
to: ROUTES.LIST_ALL_ALERT,
|
||||||
|
name: 'Alerts',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
to: ROUTES.SERVICE_MAP,
|
to: ROUTES.SERVICE_MAP,
|
||||||
name: '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);
|
const loadingRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let abortController = new window.AbortController();
|
|
||||||
const { signal } = abortController;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
const response = await functions(param);
|
const response = await functions(param);
|
||||||
|
|
||||||
if (!signal.aborted && loadingRef.current === 0) {
|
if (loadingRef.current === 0) {
|
||||||
loadingRef.current = 1;
|
loadingRef.current = 1;
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
@ -54,21 +51,19 @@ function useFetch<PayloadProps, FunctionParams>(
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!signal.aborted) {
|
setStates({
|
||||||
setStates({
|
payload: undefined,
|
||||||
payload: undefined,
|
loading: false,
|
||||||
loading: false,
|
success: false,
|
||||||
success: false,
|
error: true,
|
||||||
error: true,
|
errorMessage: error,
|
||||||
errorMessage: error,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (): void => {
|
return (): void => {
|
||||||
abortController.abort();
|
loadingRef.current = 1;
|
||||||
abortController = new window.AbortController();
|
|
||||||
};
|
};
|
||||||
}, [functions, param, state.loading]);
|
}, [functions, param, state.loading]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
@ -78,7 +73,7 @@ export interface State<T> {
|
|||||||
loading: boolean | null;
|
loading: boolean | null;
|
||||||
error: boolean | null;
|
error: boolean | null;
|
||||||
success: boolean | null;
|
success: boolean | null;
|
||||||
payload: T;
|
payload?: T;
|
||||||
errorMessage: string;
|
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 AlertChannels from 'container/AllAlertChannels';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
import GeneralSettings from 'container/GeneralSettings';
|
import GeneralSettings from 'container/GeneralSettings';
|
||||||
|
import SettingsWrapper from 'container/SettingsWrapper';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const SettingsPage = (): JSX.Element => {
|
const SettingsPage = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Tabs defaultActiveKey="1">
|
<SettingsWrapper
|
||||||
<TabPane tab="General" key="1">
|
{...{
|
||||||
<GeneralSettings />
|
AlertChannels,
|
||||||
</TabPane>
|
General: GeneralSettings,
|
||||||
{/* <TabPane tab="Alert Channels" key="2">
|
}}
|
||||||
Alerts
|
/>
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Users" key="3">
|
|
||||||
Users
|
|
||||||
</TabPane> */}
|
|
||||||
</Tabs>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './toggleDarkMode';
|
export * from './toggleDarkMode';
|
||||||
|
export * from './toggleSettingsTab';
|
||||||
export * from './userLoggedIn';
|
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 { 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';
|
import InitialValueTypes from 'types/reducer/app';
|
||||||
|
|
||||||
const InitialValue: InitialValueTypes = {
|
const InitialValue: InitialValueTypes = {
|
||||||
isDarkMode: true,
|
isDarkMode: true,
|
||||||
isLoggedIn: localStorage.getItem(IS_LOGGED_IN) === 'yes',
|
isLoggedIn: localStorage.getItem(IS_LOGGED_IN) === 'yes',
|
||||||
|
settingsActiveTab: 'General',
|
||||||
};
|
};
|
||||||
|
|
||||||
const appReducer = (
|
const appReducer = (
|
||||||
@ -26,6 +32,13 @@ const appReducer = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case TOGGLE_SETTINGS_TABS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
settingsActiveTab: action.payload.activeTab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import { SettingTab } from 'types/reducer/app';
|
||||||
|
|
||||||
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
|
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
|
||||||
export const LOGGED_IN = 'LOGGED_IN';
|
export const LOGGED_IN = 'LOGGED_IN';
|
||||||
|
export const TOGGLE_SETTINGS_TABS = 'TOGGLE_SETTINGS_TABS';
|
||||||
|
|
||||||
export interface SwitchDarkMode {
|
export interface SwitchDarkMode {
|
||||||
type: typeof SWITCH_DARK_MODE;
|
type: typeof SWITCH_DARK_MODE;
|
||||||
@ -9,4 +12,11 @@ export interface LoggedInUser {
|
|||||||
type: typeof LOGGED_IN;
|
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 {
|
export default interface AppReducer {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
isLoggedIn: 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',
|
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(ttf|eot|woff|woff2)$/,
|
||||||
|
use: ['file-loader'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -69,6 +69,10 @@ const config: Configuration = {
|
|||||||
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
|
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(ttf|eot|woff|woff2)$/,
|
||||||
|
use: ['file-loader'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
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"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
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:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user