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:
pal-sig 2021-11-22 11:49:09 +05:30 committed by GitHub
parent 556914f808
commit e2a5729c5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 2746 additions and 46 deletions

View File

@ -73,6 +73,7 @@
"jest-circus": "26.6.0",
"jest-resolve": "26.6.0",
"jest-watch-typeahead": "0.6.1",
"monaco-editor": "^0.30.0",
"pnp-webpack-plugin": "1.6.4",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",

View File

@ -4,7 +4,7 @@ import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import history from 'lib/history';
import React, { Suspense } from 'react';
import { Redirect, Route, Router, Switch, } from 'react-router-dom';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import routes from './routes';

View File

@ -70,3 +70,28 @@ export const DashboardWidget = Loadable(
() =>
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
);
export const EditRulesPage = Loadable(
() => import(/* webpackChunkName: "Alerts Edit Page" */ 'pages/EditRules'),
);
export const ListAllALertsPage = Loadable(
() => import(/* webpackChunkName: "All Alerts Page" */ 'pages/AlertList'),
);
export const CreateNewAlerts = Loadable(
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
);
export const CreateAlertChannelAlerts = Loadable(
() =>
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
);
export const EditAlertChannelsAlerts = Loadable(
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
);
export const AllAlertChannels = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
);

View File

@ -3,8 +3,14 @@ import DashboardWidget from 'pages/DashboardWidget';
import { RouteProps } from 'react-router-dom';
import {
AllAlertChannels,
CreateAlertChannelAlerts,
CreateNewAlerts,
DashboardPage,
EditAlertChannelsAlerts,
EditRulesPage,
InstrumentationPage,
ListAllALertsPage,
NewDashboardPage,
ServiceMapPage,
ServiceMetricsPage,
@ -78,11 +84,41 @@ const routes: AppRoutes[] = [
exact: true,
component: DashboardWidget,
},
{
path: ROUTES.EDIT_ALERTS,
exact: true,
component: EditRulesPage,
},
{
path: ROUTES.LIST_ALL_ALERT,
exact: true,
component: ListAllALertsPage,
},
{
path: ROUTES.ALERTS_NEW,
exact: true,
component: CreateNewAlerts,
},
{
path: ROUTES.TRACE,
exact: true,
component: TraceDetailPages,
},
{
path: ROUTES.CHANNELS_NEW,
exact: true,
component: CreateAlertChannelAlerts,
},
{
path: ROUTES.CHANNELS_EDIT,
exact: true,
component: EditAlertChannelsAlerts,
},
{
path: ROUTES.ALL_CHANNELS,
exact: true,
component: AllAlertChannels,
},
];
interface AppRoutes {

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -1,3 +1,4 @@
const apiV1 = '/api/v1/';
export const apiV2 = '/api/alertmanager';
export default apiV1;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -1,10 +1,14 @@
import axios from 'axios';
import { ENVIRONMENT } from 'constants/env';
import apiV1 from './apiV1';
import apiV1, { apiV2 } from './apiV1';
export default axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
export const AxiosAlertManagerInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV2}`,
});
export { apiV1 };

View 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;

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
export const Container = styled.div`
&&& {
min-height: 40vh;
width: 100%;
}
`;

View File

@ -12,6 +12,12 @@ const ROUTES = {
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
EDIT_ALERTS: '/alerts/edit/:ruleId',
LIST_ALL_ALERT: '/alerts',
ALERTS_NEW: '/alerts/new',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
};
export default ROUTES;

View 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;

View 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;

View 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;

View 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;
}
`;

View 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';

View 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;

View 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);

View 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;

View 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;
}
`;

View 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;

View 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;

View File

@ -0,0 +1,8 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const Button = styled(ButtonComponent)`
&&& {
margin-right: 1rem;
}
`;

View 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;

View 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;

View File

@ -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;

View 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;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-end;
margin-bottom: 2rem;
}
`;

View File

@ -0,0 +1,7 @@
import React from 'react';
const MapAlertChannels = () => {
return <div>MapAlertChannels</div>;
};
export default MapAlertChannels;

View 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);

View File

@ -1,4 +1,5 @@
import {
AlertOutlined,
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
@ -25,6 +26,11 @@ const menus: SidebarMenu[] = [
to: ROUTES.ALL_DASHBOARD,
name: 'Dashboard',
},
{
Icon: AlertOutlined,
to: ROUTES.LIST_ALL_ALERT,
name: 'Alerts',
},
{
to: ROUTES.SERVICE_MAP,
name: 'Service Map',

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
`;

View 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;

View File

@ -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;

View 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;

View 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;

View 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;
}
`;

View 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));
};

View File

@ -22,15 +22,12 @@ function useFetch<PayloadProps, FunctionParams>(
const loadingRef = useRef(0);
useEffect(() => {
let abortController = new window.AbortController();
const { signal } = abortController;
try {
(async (): Promise<void> => {
if (state.loading) {
const response = await functions(param);
if (!signal.aborted && loadingRef.current === 0) {
if (loadingRef.current === 0) {
loadingRef.current = 1;
if (response.statusCode === 200) {
@ -54,21 +51,19 @@ function useFetch<PayloadProps, FunctionParams>(
}
})();
} catch (error) {
if (!signal.aborted) {
setStates({
payload: undefined,
loading: false,
success: false,
error: true,
errorMessage: error,
});
}
setStates({
payload: undefined,
loading: false,
success: false,
error: true,
errorMessage: error,
});
}
return (): void => {
abortController.abort();
abortController = new window.AbortController();
loadingRef.current = 1;
};
}, [functions, param, state.loading]);
return {
...state,
};
@ -78,7 +73,7 @@ export interface State<T> {
loading: boolean | null;
error: boolean | null;
success: boolean | null;
payload: T;
payload?: T;
errorMessage: string;
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}
`;

View 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;

View File

@ -1,22 +1,16 @@
import { Tabs } from 'antd';
import React from 'react';
const { TabPane } = Tabs;
import AlertChannels from 'container/AllAlertChannels';
import GeneralSettings from 'container/GeneralSettings';
import SettingsWrapper from 'container/SettingsWrapper';
import React from 'react';
const SettingsPage = (): JSX.Element => {
return (
<Tabs defaultActiveKey="1">
<TabPane tab="General" key="1">
<GeneralSettings />
</TabPane>
{/* <TabPane tab="Alert Channels" key="2">
Alerts
</TabPane>
<TabPane tab="Users" key="3">
Users
</TabPane> */}
</Tabs>
<SettingsWrapper
{...{
AlertChannels,
General: GeneralSettings,
}}
/>
);
};

View File

@ -1,2 +1,3 @@
export * from './toggleDarkMode';
export * from './toggleSettingsTab';
export * from './userLoggedIn';

View 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,
},
});
};
};

View File

@ -1,10 +1,16 @@
import { IS_LOGGED_IN } from 'constants/auth';
import { AppAction, LOGGED_IN, SWITCH_DARK_MODE } from 'types/actions/app';
import {
AppAction,
LOGGED_IN,
SWITCH_DARK_MODE,
TOGGLE_SETTINGS_TABS,
} from 'types/actions/app';
import InitialValueTypes from 'types/reducer/app';
const InitialValue: InitialValueTypes = {
isDarkMode: true,
isLoggedIn: localStorage.getItem(IS_LOGGED_IN) === 'yes',
settingsActiveTab: 'General',
};
const appReducer = (
@ -26,6 +32,13 @@ const appReducer = (
};
}
case TOGGLE_SETTINGS_TABS: {
return {
...state,
settingsActiveTab: action.payload.activeTab,
};
}
default:
return state;
}

View File

@ -1,5 +1,8 @@
import { SettingTab } from 'types/reducer/app';
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
export const LOGGED_IN = 'LOGGED_IN';
export const TOGGLE_SETTINGS_TABS = 'TOGGLE_SETTINGS_TABS';
export interface SwitchDarkMode {
type: typeof SWITCH_DARK_MODE;
@ -9,4 +12,11 @@ export interface LoggedInUser {
type: typeof LOGGED_IN;
}
export type AppAction = SwitchDarkMode | LoggedInUser;
export interface ToggleSettingsTab {
type: typeof TOGGLE_SETTINGS_TABS;
payload: {
activeTab: SettingTab;
};
}
export type AppAction = SwitchDarkMode | LoggedInUser | ToggleSettingsTab;

View File

@ -0,0 +1,8 @@
export interface Props {
query: string;
}
export interface PayloadProps {
status: string;
data: string;
}

View File

@ -0,0 +1,10 @@
import { Alerts } from './getAll';
export interface Props {
id: Alerts['id'];
}
export interface PayloadProps {
status: string;
data: string;
}

View File

@ -0,0 +1,9 @@
import { Alerts } from './getAll';
export interface Props {
id: Alerts['id'];
}
export type PayloadProps = {
data: string;
};

View 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[];

View 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[];

View 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'];
}

View File

@ -0,0 +1,8 @@
import { SlackChannel } from 'container/CreateAlertChannels/config';
export type Props = SlackChannel;
export interface PayloadProps {
data: string;
status: string;
}

View File

@ -0,0 +1,10 @@
import { Channels } from './getAll';
export interface Props {
id: Channels['id'];
}
export interface PayloadProps {
status: string;
data: string;
}

View 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;
}

View File

@ -0,0 +1,7 @@
import { Channels } from './getAll';
export interface Props {
id: Channels['id'];
}
export type PayloadProps = Channels;

View 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;
}

View File

@ -1,4 +1,6 @@
export type SettingTab = 'General' | 'Alert Channels';
export default interface AppReducer {
isDarkMode: boolean;
isLoggedIn: boolean;
settingsActiveTab: SettingTab;
}

View File

@ -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 {};

View File

@ -0,0 +1,9 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
FRONTEND_API_ENDPOINT: string | undefined;
}
}
}
export {};

View File

@ -48,6 +48,10 @@ const config: webpack.Configuration = {
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
],
},
{
test: /\.(ttf|eot|woff|woff2)$/,
use: ['file-loader'],
},
],
},
plugins: [

View File

@ -69,6 +69,10 @@ const config: Configuration = {
'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false',
],
},
{
test: /\.(ttf|eot|woff|woff2)$/,
use: ['file-loader'],
},
],
},
plugins: [

View File

@ -10283,6 +10283,11 @@ moment@>=2.13.0, moment@^2.24.0, moment@^2.25.3:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
monaco-editor@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.30.0.tgz#1c7f9ba1d18c21868ce3a5413ef56351f9df7933"
integrity sha512-/k++/ofRmwnwWTpOWYOMGVcqBrqrlt3MP0Mt/cRTQojW7A9fnekcvPQ2iIFA0YSZdPWPN9yYXrYq0xqiUuxT/A==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"