diff --git a/frontend/package.json b/frontend/package.json index 905dbe4704..1f0caa6945 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 3ea9912327..ab0b5e7acb 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -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'; diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index c214e2d116..9ffc25aff3 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -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'), +); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index a73f1c24e2..59ba0ebf30 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -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 { diff --git a/frontend/src/api/alerts/create.ts b/frontend/src/api/alerts/create.ts new file mode 100644 index 0000000000..10dbff99b6 --- /dev/null +++ b/frontend/src/api/alerts/create.ts @@ -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 | 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; diff --git a/frontend/src/api/alerts/delete.ts b/frontend/src/api/alerts/delete.ts new file mode 100644 index 0000000000..278e3e2935 --- /dev/null +++ b/frontend/src/api/alerts/delete.ts @@ -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 | 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; diff --git a/frontend/src/api/alerts/get.ts b/frontend/src/api/alerts/get.ts new file mode 100644 index 0000000000..aeddf67fd0 --- /dev/null +++ b/frontend/src/api/alerts/get.ts @@ -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 | 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; diff --git a/frontend/src/api/alerts/getAll.ts b/frontend/src/api/alerts/getAll.ts new file mode 100644 index 0000000000..e6b1fdb0c2 --- /dev/null +++ b/frontend/src/api/alerts/getAll.ts @@ -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 | 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; diff --git a/frontend/src/api/alerts/getGroup.ts b/frontend/src/api/alerts/getGroup.ts new file mode 100644 index 0000000000..9fd69bc49c --- /dev/null +++ b/frontend/src/api/alerts/getGroup.ts @@ -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 | 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; diff --git a/frontend/src/api/alerts/put.ts b/frontend/src/api/alerts/put.ts new file mode 100644 index 0000000000..15d4c7c698 --- /dev/null +++ b/frontend/src/api/alerts/put.ts @@ -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 | 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; diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts index d091514a24..22054bf229 100644 --- a/frontend/src/api/apiV1.ts +++ b/frontend/src/api/apiV1.ts @@ -1,3 +1,4 @@ const apiV1 = '/api/v1/'; +export const apiV2 = '/api/alertmanager'; export default apiV1; diff --git a/frontend/src/api/channels/createSlack.ts b/frontend/src/api/channels/createSlack.ts new file mode 100644 index 0000000000..f9e430fbc9 --- /dev/null +++ b/frontend/src/api/channels/createSlack.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/delete.ts b/frontend/src/api/channels/delete.ts new file mode 100644 index 0000000000..a5366af6cc --- /dev/null +++ b/frontend/src/api/channels/delete.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/editSlack.ts b/frontend/src/api/channels/editSlack.ts new file mode 100644 index 0000000000..9a34f41318 --- /dev/null +++ b/frontend/src/api/channels/editSlack.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/get.ts b/frontend/src/api/channels/get.ts new file mode 100644 index 0000000000..39c40ec935 --- /dev/null +++ b/frontend/src/api/channels/get.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/getAll.ts b/frontend/src/api/channels/getAll.ts new file mode 100644 index 0000000000..11b530a84c --- /dev/null +++ b/frontend/src/api/channels/getAll.ts @@ -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 | 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; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index b1592ba7dc..feaac180e4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -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 }; diff --git a/frontend/src/components/Editor/index.tsx b/frontend/src/components/Editor/index.tsx new file mode 100644 index 0000000000..ae928d0084 --- /dev/null +++ b/frontend/src/components/Editor/index.tsx @@ -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(null); + const editorRef = useRef(); + + 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 ; +}; + +interface EditorProps { + value: React.MutableRefObject; +} + +export default Editor; diff --git a/frontend/src/components/Editor/styles.ts b/frontend/src/components/Editor/styles.ts new file mode 100644 index 0000000000..f1208237be --- /dev/null +++ b/frontend/src/components/Editor/styles.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + &&& { + min-height: 40vh; + width: 100%; + } +`; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 9f84fbf3e1..f8ab430edb 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -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; diff --git a/frontend/src/container/AllAlertChannels/AlertChannels.tsx b/frontend/src/container/AllAlertChannels/AlertChannels.tsx new file mode 100644 index 0000000000..c83bb1f9fa --- /dev/null +++ b/frontend/src/container/AllAlertChannels/AlertChannels.tsx @@ -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(allChannels); + + const onClickEditHandler = useCallback((id: string) => { + history.replace( + generatePath(ROUTES.CHANNELS_EDIT, { + id, + }), + ); + }, []); + + const columns: ColumnsType = [ + { + 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 => ( + <> + + + + ), + }, + ]; + + return ( + <> + {Element} + + + + ); +}; + +interface AlertChannelsProps { + allChannels: PayloadProps; +} + +export default AlertChannels; diff --git a/frontend/src/container/AllAlertChannels/Delete.tsx b/frontend/src/container/AllAlertChannels/Delete.tsx new file mode 100644 index 0000000000..2b08892ff8 --- /dev/null +++ b/frontend/src/container/AllAlertChannels/Delete.tsx @@ -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 => { + 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 ( + + ); +}; + +interface DeleteProps { + notifications: NotificationInstance; + setChannels: React.Dispatch>; + id: string; +} + +export default Delete; diff --git a/frontend/src/container/AllAlertChannels/index.tsx b/frontend/src/container/AllAlertChannels/index.tsx new file mode 100644 index 0000000000..330e6dcdc9 --- /dev/null +++ b/frontend/src/container/AllAlertChannels/index.tsx @@ -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 {errorMessage}; + } + + if (loading || payload === undefined) { + return ; + } + + return ( + <> + + + The latest added channel is used as the default channel for sending alerts + + + + + + + + ); +}; + +export default AlertChannels; diff --git a/frontend/src/container/AllAlertChannels/styles.ts b/frontend/src/container/AllAlertChannels/styles.ts new file mode 100644 index 0000000000..c8caa45fda --- /dev/null +++ b/frontend/src/container/AllAlertChannels/styles.ts @@ -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; + } +`; diff --git a/frontend/src/container/CreateAlertChannels/config.ts b/frontend/src/container/CreateAlertChannels/config.ts new file mode 100644 index 0000000000..364d367806 --- /dev/null +++ b/frontend/src/container/CreateAlertChannels/config.ts @@ -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'; diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx new file mode 100644 index 0000000000..a317d934a8 --- /dev/null +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -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>({ + 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(false); + const [notifications, NotificationElement] = notification.useNotification(); + + const [type, setType] = useState(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 ( + <> + + + ); +}; + +interface CreateAlertChannelsProps { + preType?: ChannelType; +} + +export default CreateAlertChannels; diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx new file mode 100644 index 0000000000..e947f65dd2 --- /dev/null +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -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>({ + ...initialValue, + }); + const [savingState, setSavingState] = useState(false); + const [notifications, NotificationElement] = notification.useNotification(); + const { id } = useParams<{ id: string }>(); + + const [type, setType] = useState('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 ( + <> + + + ); +}; + +interface DispatchProps { + toggleSettingsTab: (props: SettingTab) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + toggleSettingsTab: bindActionCreators(ToggleSettingsTab, dispatch), +}); + +interface EditAlertChannelsProps extends DispatchProps { + initialValue: Store; +} + +export default connect(null, mapDispatchToProps)(EditAlertChannels); diff --git a/frontend/src/container/EditRules/index.tsx b/frontend/src/container/EditRules/index.tsx new file mode 100644 index 0000000000..78eb4438a8 --- /dev/null +++ b/frontend/src/container/EditRules/index.tsx @@ -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(initialData); + const [notifications, Element] = notification.useNotification(); + const [editButtonState, setEditButtonState] = useState>( + { + 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} + + + + + + + + ); +}; + +interface EditRulesProps { + initialData: PayloadProps['data']; + ruleId: string; +} + +export default EditRules; diff --git a/frontend/src/container/EditRules/styles.ts b/frontend/src/container/EditRules/styles.ts new file mode 100644 index 0000000000..0a201f8853 --- /dev/null +++ b/frontend/src/container/EditRules/styles.ts @@ -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; + } +`; diff --git a/frontend/src/container/FormAlertChannels/Settings/Slack.tsx b/frontend/src/container/FormAlertChannels/Settings/Slack.tsx new file mode 100644 index 0000000000..bd16bd0fcd --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/Slack.tsx @@ -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 => ( + <> + + { + setSelectedConfig((value) => ({ + ...value, + api_url: event.target.value, + })); + }} + /> + + + + + setSelectedConfig((value) => ({ + ...value, + channel: event.target.value, + })) + } + /> + + + +