diff --git a/web/src/components/Playbooks/workflows/BasicDetails.jsx b/web/src/components/Playbooks/workflows/BasicDetails.jsx new file mode 100644 index 000000000..11b7f76c6 --- /dev/null +++ b/web/src/components/Playbooks/workflows/BasicDetails.jsx @@ -0,0 +1,110 @@ +import React from "react"; +import ValueComponent from "../../ValueComponent"; +import SelectComponent from "../../SelectComponent"; +import { RefreshRounded } from "@mui/icons-material"; +import { CircularProgress } from "@mui/material"; +import { useGetPlaybooksQuery } from "../../../store/features/playbook/api/index.ts"; +import { useSelector } from "react-redux"; +import { currentWorkflowSelector } from "../../../store/features/workflow/workflowSlice.ts"; +import SlackTriggerForm from "./triggers/SlackTriggerForm.jsx"; +import { triggerTypes } from "../../../utils/workflow/triggerTypes.ts"; +import { handleInput, handleSelect } from "./utils/handleInputs.ts"; + +function BasicDetails() { + const { + data, + isFetching: playbooksLoading, + refetch, + } = useGetPlaybooksQuery({}); + const currentWorkflow = useSelector(currentWorkflowSelector); + + return ( + <> +
+
+ + handleInput("name", val)} + /> +
+
+ +
+ { + return { + id: e.id, + label: e.name, + playbook: e, + }; + })} + placeholder={`Select Playbook`} + onSelectionChange={(_, val) => { + handleSelect("playbookId", val); + }} + selected={currentWorkflow?.playbookId} + searchable={true} + /> + {playbooksLoading && } + +
+
+
+
+
+ +
+ { + return { + id: e.id, + label: e.label, + }; + })} + placeholder={`Select Workflow Type`} + onSelectionChange={(_, val) => { + handleSelect("workflowType", val); + }} + selected={currentWorkflow?.workflowType} + searchable={true} + /> +
+
+ {currentWorkflow.workflowType === "slack" && ( + + )} +
+ + ); +} + +export default BasicDetails; diff --git a/web/src/components/Playbooks/workflows/CreateWorkflow.jsx b/web/src/components/Playbooks/workflows/CreateWorkflow.jsx new file mode 100644 index 000000000..c90ad9410 --- /dev/null +++ b/web/src/components/Playbooks/workflows/CreateWorkflow.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import Heading from "../../Heading.js"; +import BasicDetails from "./BasicDetails.jsx"; +import ScheduleDetails from "./ScheduleDetails.jsx"; +import NotificationDetails from "./NotificationDetails.jsx"; + +function CreateTrigger() { + return ( +
+ +
+ +
+ +
+ +
+ +
+
+
+ ); +} + +export default CreateTrigger; diff --git a/web/src/components/Playbooks/workflows/NotificationDetails.jsx b/web/src/components/Playbooks/workflows/NotificationDetails.jsx new file mode 100644 index 000000000..1ef48551b --- /dev/null +++ b/web/src/components/Playbooks/workflows/NotificationDetails.jsx @@ -0,0 +1,19 @@ +import React from "react"; +import { notificationOptions } from "../../../utils/workflow/notificationOptions.ts"; +import { HandleInputRender } from "../../common/HandleInputRender/HandleInputRender.jsx"; + +function NotificationDetails() { + return ( +
+ + + {notificationOptions.map((option) => ( + + ))} +
+ ); +} + +export default NotificationDetails; diff --git a/web/src/components/Playbooks/workflows/ScheduleDetails.jsx b/web/src/components/Playbooks/workflows/ScheduleDetails.jsx new file mode 100644 index 000000000..b818ad37e --- /dev/null +++ b/web/src/components/Playbooks/workflows/ScheduleDetails.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { currentWorkflowSelector } from "../../../store/features/workflow/workflowSlice.ts"; +import { scheduleOptions } from "../../../utils/workflow/scheduleOptions.tsx"; +import { HandleInputRender } from "../../common/HandleInputRender/HandleInputRender.jsx"; +import { handleSelect } from "./utils/handleInputs.ts"; + +function ScheduleDetails() { + const currentWorkflow = useSelector(currentWorkflowSelector); + + return ( +
+ +
+ {scheduleOptions.map((option) => ( + + ))} +
+
+ {scheduleOptions + .find((e) => e.id === currentWorkflow.schedule) + ?.options.map((option) => ( + + ))} +
+ {(currentWorkflow["cron-schedule"] || + (currentWorkflow.interval && currentWorkflow.duration)) && ( +

{`This configuration means that this workflow will run ${ + currentWorkflow["cron-schedule"] + ? `as per {${currentWorkflow["cron-schedule"]}} schedule` + : "" + } for the next ${currentWorkflow.duration} ${ + currentWorkflow.interval + }`}

+ )} +
+ ); +} + +export default ScheduleDetails; diff --git a/web/src/components/Playbooks/workflows/triggers/AlertsTable.jsx b/web/src/components/Playbooks/workflows/triggers/AlertsTable.jsx new file mode 100644 index 000000000..d8003ed46 --- /dev/null +++ b/web/src/components/Playbooks/workflows/triggers/AlertsTable.jsx @@ -0,0 +1,81 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@mui/material"; +import NoExistingTrigger from "./NoExistingTrigger"; +import { renderTimestamp } from "../../../../utils/DateUtils"; +import { ExpandMore } from "@mui/icons-material"; + +const AlertsTable = ({ data }) => { + return ( + <> +

+ Alerts matching the search criteria +

+ + + + Title + Timestamp + Alert Tags + + + + {data?.map((item, index) => ( + + + {item.alert_title} + + + {renderTimestamp(item.alert_timestamp)} + + + + } + aria-controls="panel1a-content" + id="panel1a-header"> + Details + + +
+ + + Key + Value + + + + {item.alert_tags.map((tag, index) => ( + + {tag.key} + {tag.value} + + ))} + +
+ + + + + ))} + + + {!data?.length ? : null} + + ); +}; + +export default AlertsTable; diff --git a/web/src/components/Playbooks/workflows/triggers/NoExistingTrigger.jsx b/web/src/components/Playbooks/workflows/triggers/NoExistingTrigger.jsx new file mode 100644 index 000000000..226af33e5 --- /dev/null +++ b/web/src/components/Playbooks/workflows/triggers/NoExistingTrigger.jsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; + +const NoExistingTrigger = () => { + return ( + <> +
+ logo +
+ No Alerts found +
+
+ +
+ Check Documentation +
+ +
+
+ + ); +}; + +export default NoExistingTrigger; diff --git a/web/src/components/Playbooks/workflows/triggers/SlackTriggerForm.jsx b/web/src/components/Playbooks/workflows/triggers/SlackTriggerForm.jsx new file mode 100644 index 000000000..d3379a93f --- /dev/null +++ b/web/src/components/Playbooks/workflows/triggers/SlackTriggerForm.jsx @@ -0,0 +1,105 @@ +import React from "react"; +import SelectComponent from "../../../SelectComponent/index.jsx"; +import ValueComponent from "../../../ValueComponent/index.jsx"; +import { useSelector } from "react-redux"; +import { currentWorkflowSelector } from "../../../../store/features/workflow/workflowSlice.ts"; +import { useGetTriggerOptionsQuery } from "../../../../store/features/triggers/api/getTriggerOptionsApi.ts"; +import { + handleTriggerInput, + handleTriggerSelect, +} from "../utils/handleInputs.ts"; +import { useLazyGetSearchTriggersQuery } from "../../../../store/features/triggers/api/searchTriggerApi.ts"; +import AlertsTable from "./AlertsTable.jsx"; +import { CircularProgress } from "@mui/material"; + +function SlackTriggerForm() { + const { data: options } = useGetTriggerOptionsQuery(); + const currentWorkflow = useSelector(currentWorkflowSelector); + const [ + triggerSearchTrigger, + { data: searchTriggerResult, isFetching: searchLoading }, + ] = useLazyGetSearchTriggersQuery(); + + const handleSubmit = (e) => { + e?.preventDefault(); + triggerSearchTrigger({ + workspaceId: currentWorkflow?.trigger?.workspaceId, + channel_id: currentWorkflow?.trigger?.channel_id, + alert_type: currentWorkflow?.trigger?.alert_type, + filter_string: currentWorkflow?.trigger?.filterString, + }); + }; + + const sources = options?.alert_types?.filter( + (e) => e.channel_connector_key_id === currentWorkflow.channel?.id, + ); + const data = searchTriggerResult?.alerts ?? null; + + return ( +
+
+

Channel

+ { + return { + id: e.channel_id, + label: e.channel_name, + channel: e, + }; + })} + placeholder="Select Channel" + onSelectionChange={(_, val) => + handleTriggerSelect("channel", val.channel) + } + selected={currentWorkflow?.trigger?.channel?.channel_id ?? ""} + searchable={true} + /> +
+
+

Source

+ { + return { + id: e.alert_type, + label: e.alert_type, + source: e, + }; + })} + placeholder="Select Source" + onSelectionChange={(_, val) => + handleTriggerSelect("source", val.source) + } + selected={currentWorkflow?.trigger?.source?.alert_type ?? ""} + searchable={true} + /> +
+
+

Filter

+ { + handleTriggerInput("filterString", val); + }} + value={currentWorkflow?.trigger?.filterString} + placeHolder={"Enter filter string"} + length={300} + /> +
+ + + {searchLoading ? ( + + ) : data ? ( + + ) : ( + <> + )} + + ); +} + +export default SlackTriggerForm; diff --git a/web/src/components/Playbooks/workflows/utils/handleInputs.ts b/web/src/components/Playbooks/workflows/utils/handleInputs.ts new file mode 100644 index 000000000..f73957cfd --- /dev/null +++ b/web/src/components/Playbooks/workflows/utils/handleInputs.ts @@ -0,0 +1,42 @@ +import { store } from "../../../../store/index.ts"; +import { + setCurrentWorkflowKey, + setCurrentWorkflowTriggerKey, +} from "../../../../store/features/workflow/workflowSlice.ts"; + +export const handleSelect = (e, option) => { + const type = e.target?.getAttribute("data-type") ?? e; + store.dispatch( + setCurrentWorkflowKey({ + key: type, + value: option.id, + }), + ); +}; + +export const handleInput = (key, value) => { + store.dispatch( + setCurrentWorkflowKey({ + key, + value, + }), + ); +}; + +export const handleTriggerSelect = (key, value) => { + store.dispatch( + setCurrentWorkflowTriggerKey({ + key, + value, + }), + ); +}; + +export const handleTriggerInput = (key, value) => { + store.dispatch( + setCurrentWorkflowTriggerKey({ + key, + value, + }), + ); +}; diff --git a/web/src/store/features/triggers/api/getTriggerOptionsApi.ts b/web/src/store/features/triggers/api/getTriggerOptionsApi.ts new file mode 100644 index 000000000..d798ed528 --- /dev/null +++ b/web/src/store/features/triggers/api/getTriggerOptionsApi.ts @@ -0,0 +1,38 @@ +import { GET_TRIGGER_OPTIONS } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; +import { setCurrentWorkflowTriggerKey } from "../../workflow/workflowSlice.ts"; + +export const getTriggerOptionsApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + getTriggerOptions: builder.query({ + query: () => ({ + url: GET_TRIGGER_OPTIONS, + method: "POST", + body: { + connector_type_requests: [ + { + connector_type: "SLACK", + }, + ], + }, + }), + transformResponse: (response) => { + if (response?.alert_ops_options?.comm_options?.workspaces?.length > 0) + return response?.alert_ops_options?.comm_options?.workspaces[0]; + return []; + }, + onQueryStarted: async (_, { dispatch, queryFulfilled }) => { + try { + const { data } = await queryFulfilled; + dispatch( + setCurrentWorkflowTriggerKey({ key: "workflowId", value: data.id }), + ); + } catch (error) { + console.log(error); + } + }, + }), + }), +}); + +export const { useGetTriggerOptionsQuery } = getTriggerOptionsApi; diff --git a/web/src/store/features/triggers/api/index.ts b/web/src/store/features/triggers/api/index.ts new file mode 100644 index 000000000..63857eb1c --- /dev/null +++ b/web/src/store/features/triggers/api/index.ts @@ -0,0 +1 @@ +export * from "./getTriggerOptionsApi.ts"; diff --git a/web/src/store/features/triggers/api/searchTriggerApi.ts b/web/src/store/features/triggers/api/searchTriggerApi.ts new file mode 100644 index 000000000..c82fbb5f6 --- /dev/null +++ b/web/src/store/features/triggers/api/searchTriggerApi.ts @@ -0,0 +1,50 @@ +import { SEARCH_TRIGGER } from '../../../../constants/index.ts'; +import { apiSlice } from '../../../app/apiSlice.ts'; + +type SearchTriggerApiArgTypes = { + workspace_id: number; + channel_id: number; + alert_type: string; + filter_string: string; +}; + +const currentTimestamp = Math.floor(Date.now() / 1000); + +export const searchTriggerApi = apiSlice.injectEndpoints({ + endpoints: builder => ({ + getSearchTriggers: builder.query({ + query: ({ workspace_id, channel_id, alert_type, filter_string }) => ({ + url: SEARCH_TRIGGER, + method: 'POST', + body: { + meta: { + page: { + limit: 5, + offset: 0 + }, + time_range: { + time_geq: currentTimestamp - 259200, + time_lt: currentTimestamp + } + }, + workspace_id, + channel_id, + alert_type, + fuzzy_search_request: { + context: 'SLACK_ALERT', + pattern: filter_string + } + } + }), + transformResponse: (response: any) => { + const data = { + alerts: response?.slack_alerts ?? [], + total: response?.meta?.total_count ?? 0 + }; + return data; + } + }) + }) +}); + +export const { useLazyGetSearchTriggersQuery } = searchTriggerApi;