diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/DropDown.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/DropDown.svg index 05f32f42411..aabcc0f8320 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/DropDown.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/DropDown.svg @@ -1,3 +1,3 @@ - + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-right-arrow.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-right-arrow.svg new file mode 100644 index 00000000000..0c3bf65467e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertActionAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertActionAPI.ts index be52364b66a..97af3c3b3ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertActionAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertActionAPI.ts @@ -12,6 +12,7 @@ */ import { AxiosResponse } from 'axios'; +import { Operation } from 'fast-json-patch'; import { PagingResponse } from 'Models'; import axiosClient from '.'; import { AlertAction } from '../generated/alerts/alertAction'; @@ -36,6 +37,21 @@ export const createAlertAction = async (alertAction: CreateAlertAction) => { return response.data; }; +export const patchAlertAction = async (id: string, operation: Operation) => { + const response = await axiosClient.patch< + Operation, + AxiosResponse + >(`${BASE_URL}/${id}`, operation); + + return response.data; +}; + +export const updateAlertAction = async (alertAction: AlertAction) => { + const response = await axiosClient.put(BASE_URL, alertAction); + + return response.data; +}; + export const deleteAlertAction = async (id: string) => { const response = await axiosClient.delete(`${BASE_URL}/${id}`); diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertsAPI.ts index ab8e35e44c9..8e7a794809a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/alertsAPI.ts @@ -13,7 +13,11 @@ import { PagingResponse } from 'Models'; import axiosClient from '.'; -import { AlertActionType, Status } from '../generated/alerts/alertAction'; +import { + AlertAction, + AlertActionType, + Status, +} from '../generated/alerts/alertAction'; import { Alerts, TriggerConfig } from '../generated/alerts/alerts'; import { EntitySpelFilters } from '../generated/alerts/entitySpelFilters'; import { Function } from '../generated/type/function'; @@ -60,6 +64,12 @@ export const createAlert = async (alert: Alerts) => { return response.data; }; +export const updateAlert = async (alert: Alerts) => { + const response = await axiosClient.put(BASE_URL, alert); + + return response.data; +}; + export const deleteAlert = async (id: string) => { const response = await axiosClient.delete(`${BASE_URL}/${id}`); @@ -94,7 +104,9 @@ export const getDefaultTriggerConfigs = async (after?: string) => { }; export const getAlertActionForAlerts = async (id: string) => { - const response = await axiosClient.get(`${BASE_URL}/allAlertAction/${id}`); + const response = await axiosClient.get( + `${BASE_URL}/allAlertAction/${id}` + ); return response.data; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx index 496491d767b..ef232c03692 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx @@ -216,7 +216,7 @@ const SearchDropdown: FC = ({ )} - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SlackChat/SlackChat.css b/openmetadata-ui/src/main/resources/ui/src/components/SlackChat/SlackChat.css index 852ff388614..08ef3abe958 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SlackChat/SlackChat.css +++ b/openmetadata-ui/src/main/resources/ui/src/components/SlackChat/SlackChat.css @@ -17,10 +17,16 @@ box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.3); height: 60px; position: fixed; - right: 40px; + right: 20px; bottom: 20px; width: 60px; border-radius: 50%; display: inline-block; cursor: pointer; + scale: 0.75; + transition: scale 0.5s ease-in-out; +} + +.slack-chat .bubble:hover { + scale: 1; } diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 2d26e646a0f..c0209263d21 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -408,7 +408,6 @@ "edit-team-type": "Edit Team Type", "configure-webhook-type": "Configure {{webhookType}} Webhooks", "ms-teams": "MS Teams", - "ms-team": "MS Team", "bot": "Bot", "bot-plural": "Bots", "policy-name": "Policy name", @@ -584,7 +583,15 @@ "completed-description": "Completed Description", "assigned-entity": "Assigned {{entity}}", "total-assets-view": "Total Assets View", - "total-active-user": "Total Active User" + "total-active-user": "Total Active User", + "alert-actions": "Alert Actions", + "test-results": "Test Results", + "send-to": "Send to", + "receiver-plural": "Receivers", + "admin-plural": "Admins", + "owner-plural": "Owners", + "follower-plural": "Followers", + "email-plural": "Emails" }, "message": { "service-email-required": "Service account Email is required", @@ -709,7 +716,8 @@ "alerts-description": "Stay current with timely alerts using webhooks.", "alerts-destination-description": "Send notifications to Slack, MS Teams, Email, or and use Webhooks.", "alerts-trigger-description": "Trigger for all data assets or a specific entity.", - "alerts-filter-description": "Specify the change events to narrow the scope of your alerts." + "alerts-filter-description": "Specify the change events to narrow the scope of your alerts.", + "length-validator-error": "At least {{length}} {{field}} required" }, "server": { "you-have-not-action-anything-yet": "You have not {{action}} anything yet.", @@ -729,7 +737,8 @@ "ingestion-workflow-operation-error": "Error while {{operation}} ingestion workflow {{displayName}}", "team-moved-error": "Error while moving team", "entity-details-fetch-error": "Error while fetching details for {{entityType}} {{entityName}}", - "create-entity-success": "{{entity}} created successfully." + "create-entity-success": "{{entity}} created successfully.", + "update-entity-success": "{{entity}} updated successfully." }, "url": {} } diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index db84a363c08..db5e1813095 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -404,7 +404,6 @@ "edit-team-type": "Mettre à jour le type d'équipe", "configure-webhook-type": "Configurer {{webhookType}} Webhooks", "ms-teams": "MS Teams", - "ms-team": "MS Team", "policy-name": "Nom de Police", "add-new-policy": "Ajouter une nouvelle police", "policies": "Polices", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx index 9215d616d7f..b7db46273cb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/AddAlertPage.tsx @@ -14,30 +14,35 @@ import { PlusOutlined } from '@ant-design/icons'; import { Button, Card, + Checkbox, Col, + Collapse, Divider, Form, Input, Row, Select, Space, - Switch, Typography, } from 'antd'; import { useForm } from 'antd/lib/form/Form'; import { DefaultOptionType } from 'antd/lib/select'; -import { AxiosError } from 'axios'; -import { get, intersection, isEmpty, map, startCase } from 'lodash'; +import { get, intersection, isEmpty, map, pick, startCase, trim } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { createAlertAction } from '../../axiosAPIs/alertActionAPI'; +import { + createAlertAction, + updateAlertAction, +} from '../../axiosAPIs/alertActionAPI'; import { createAlert, + getAlertActionForAlerts, getAlertsFromId, getDefaultTriggerConfigs, getEntityFilterFunctions, getFilterFunctions, + updateAlert, } from '../../axiosAPIs/alertsAPI'; import { getSearchedUsersAndTeams, @@ -51,19 +56,26 @@ import { import { PROMISE_STATE } from '../../enums/common.enum'; import { AlertAction } from '../../generated/alerts/alertAction'; import { + AlertFilterRule, Alerts, AlertTriggerType, Effect, EntityReference, + ProviderType, TriggerConfig, } from '../../generated/alerts/alerts'; -import { AlertActionType } from '../../generated/alerts/api/createAlertAction'; +import { + AlertActionType, + CreateAlertAction, +} from '../../generated/alerts/api/createAlertAction'; import { EntitySpelFilters } from '../../generated/alerts/entitySpelFilters'; import { Function } from '../../generated/type/function'; import { + getAlertActionTypeDisplayName, getAlertsActionTypeIcon, getDisplayNameForTriggerType, getFunctionDisplayName, + listLengthValidator, StyledCard, } from '../../utils/Alerts/AlertsUtil'; import { getSettingPath } from '../../utils/RouterUtils'; @@ -76,6 +88,8 @@ const AddAlertPage = () => { const [form] = useForm(); const history = useHistory(); const { fqn } = useParams<{ fqn: string }>(); + // To block certain action based on provider of the Alert e.g. System / User + const [provider, setProvider] = useState(ProviderType.User); const [filterFunctions, setFilterFunctions] = useState(); const [defaultTriggers, setDefaultTriggers] = useState>( @@ -88,23 +102,34 @@ const AddAlertPage = () => { const fetchAlert = async () => { try { setLoadingCount((count) => count + 1); + const response: Alerts = await getAlertsFromId(fqn); + const alertActions = await getAlertActionForAlerts(response.id); const requestFilteringRules = - response.filteringRules?.map((curr) => ({ - ...curr, - condition: curr.condition - .replace(new RegExp(`${curr.name}\\('`), '') - .replace(new RegExp(`'\\)`), ''), - })) ?? []; + response.filteringRules?.map( + (curr) => + ({ + ...curr, + condition: curr.condition + .replace(new RegExp(`${curr.name}\\('`), '') + .replaceAll("'", '') + .replace(new RegExp(`\\)`), '') + .split(',') + .map(trim), + } as unknown as AlertFilterRule) + ) ?? []; + + setProvider(response.provider ?? ProviderType.User); form.setFieldsValue({ ...response, filteringRules: requestFilteringRules, + alertActions: alertActions as unknown as EntityReference[], }); } catch { showErrorToast( - t('message.entity-fetch-error', { entity: t('label.alert') }), + t('server.entity-fetch-error', { entity: t('label.alert') }), fqn ); } finally { @@ -149,8 +174,60 @@ const AddAlertPage = () => { fetchDefaultTriggerConfig(); }, []); + const isEditMode = useMemo(() => !isEmpty(fqn), [fqn]); + + const updateCreateAlertActions = async (alertActions: AlertAction[]) => { + const api = isEditMode ? updateAlertAction : createAlertAction; + if (isEditMode) { + if (!form.isFieldTouched(['alertActions'])) { + // If destination is not changed return given alertAction as it is + return Promise.resolve( + alertActions.map((action) => ({ + id: action.id ?? '', + type: 'alertAction', + })) + ); + } + } + + // Else Create AlertActions and return new IDs + const promises = + alertActions?.map((action) => + api( + pick(action, [ + 'alertActionConfig', + 'alertActionType', + 'name', + 'displayName', + 'timeout', + 'batchSize', + ]) as CreateAlertAction + ) + ) ?? []; + + const responses = await Promise.allSettled(promises); + + const requestAlertActions: EntityReference[] = responses.map((res) => { + if (res.status === PROMISE_STATE.REJECTED) { + throw res.reason; + } + + return { + id: res.status === PROMISE_STATE.FULFILLED ? res.value.id ?? '' : '', + type: 'alertAction', + }; + }); + + return Promise.resolve(requestAlertActions); + }; + const handleSave = async (data: Alerts) => { const { filteringRules, alertActions } = data; + if (!alertActions?.length) { + return; + } + + const api = isEditMode ? updateAlert : createAlert; const requestFilteringRules = filteringRules?.map((curr) => ({ ...curr, @@ -160,40 +237,31 @@ const AddAlertPage = () => { )?.join(', ')})`, })); + const modifiedAlertActions = alertActions?.map( + (action) => + ({ + ...action, + name: action.name ?? action.displayName, + displayName: action.displayName, + } as unknown as AlertAction) + ); + try { - const actions: AlertAction[] = - alertActions?.map( - (action) => - ({ - ...action, - enabled: true, - } as unknown as AlertAction) - ) ?? []; - - const promises = actions.map((action) => createAlertAction(action)); - - const responses = await Promise.allSettled(promises); - - const requestAlertActions: EntityReference[] = responses.map((res) => { - if (res.status === PROMISE_STATE.REJECTED) { - throw res.reason; - } - - return { - id: res.status === PROMISE_STATE.FULFILLED ? res.value.id ?? '' : '', - type: 'alertAction', - }; - }); + const requestAlertActions = await updateCreateAlertActions( + modifiedAlertActions + ); try { - await createAlert({ + await api({ ...data, filteringRules: requestFilteringRules, alertActions: requestAlertActions, }); showErrorToast( - t('server.create-entity-success', { entity: t('alert-plural') }) + t(`server.${isEditMode ? 'update' : 'create'}-entity-success`, { + entity: t('label.alert-plural'), + }) ); history.push( getSettingPath( @@ -203,15 +271,24 @@ const AddAlertPage = () => { ); } catch (error) { showErrorToast( - t('server.entity-creation-error', { - entity: t('label.alert-plural'), - }), - (error as AxiosError).message + t( + `server.${ + isEditMode ? 'entity-updating-error' : 'entity-creation-error' + }`, + { + entity: t('label.alert-plural'), + } + ) ); } } catch (error) { showErrorToast( - t('server.entity-creation-error', { entity: t('label.alert-plural') }) + t( + `server.${ + isEditMode ? 'entity-updating-error' : 'entity-creation-error' + }`, + { entity: t('label.alert-plural') } + ) ); } }; @@ -222,7 +299,7 @@ const AddAlertPage = () => { return response.hits.hits.map((d) => ({ label: d._source.displayName ?? d._source.name, - value: d._source.id, + value: d._source.fullyQualifiedName, })); } catch (error) { return []; @@ -313,37 +390,171 @@ const AddAlertPage = () => { // Watchers const filters = Form.useWatch(['filteringRules'], form); - const alertActions = Form.useWatch(['alertActions'], form); const entitySelected = Form.useWatch(['triggerConfig', 'entities'], form); const trigger = Form.useWatch(['triggerConfig', 'type'], form); + const alertActions = Form.useWatch(['alertActions'], form); // Run time values needed for conditional rendering const functions = useMemo(() => { if (entityFunctions) { + const exitingFunctions = filters?.map((f) => f.name) ?? []; + let supportedFunctions: string[][] = []; + if (!trigger || trigger === AlertTriggerType.AllDataAssets) { - return entityFunctions['all'].supportedFunctions.sort(); + supportedFunctions = [entityFunctions['all'].supportedFunctions]; + } else { + supportedFunctions = + entitySelected?.map( + (entity) => + entityFunctions[entity as unknown as string].supportedFunctions + ) ?? []; } - const arrFunctions = entitySelected?.map( - (entity) => - entityFunctions[entity as unknown as string].supportedFunctions - ); + const functions = intersection(...supportedFunctions) + .sort() + .map((func) => ({ + label: getFunctionDisplayName(func), + value: func, + disabled: exitingFunctions.includes(func), + })); - const functions = arrFunctions - ? intersection(...arrFunctions).sort() - : []; - - return functions as string[]; + return functions as DefaultOptionType[]; } return []; - }, [entitySelected, entityFunctions]); + }, [entitySelected, entityFunctions, filters]); const selectedTrigger = useMemo( () => defaultTriggers.find(({ type }) => trigger === type), [defaultTriggers, trigger] ); + const handleChange = (changedValues: Partial) => { + const { triggerConfig } = changedValues; + if (triggerConfig?.entities || triggerConfig?.type) { + form.resetFields(['filteringRules', 'condition']); + } + }; + + const getDestinationConfigFields = useCallback( + (name: number) => { + const alertActionType = get(alertActions, [name, 'alertActionType']); + if (alertActions && alertActions[name]) { + switch (alertActionType) { + case AlertActionType.Email: + return ( + <> + + + + + + + + + + {t('label.admin-plural')} + + + {t('label.owner-plural')} + + + {t('label.follower-plural')} + + + + ); + case AlertActionType.GenericWebhook: + case AlertActionType.SlackWebhook: + case AlertActionType.MSTeamsWebhook: + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ); + } + } + + return <>; + }, + [alertActions] + ); + return ( <> @@ -359,14 +570,15 @@ const AddAlertPage = () => { className="alerts-notification-form" form={form} - onFinish={handleSave}> + onFinish={handleSave} + onValuesChange={handleChange}> 0}> - + { rules={[{ required: true }]}> - - - - - -
- - ({ - value: entity, - label: startCase(entity), - })) ?? [] - } - placeholder={t('label.select-data-assets')} + options={defaultTriggers.map((trigger) => ({ + label: getDisplayNameForTriggerType(trigger.type), + value: trigger.type, + }))} /> - )} -
- - - - - + {selectedTrigger?.type === + AlertTriggerType.SpecificDataAsset && ( + + ({ - label: getFunctionDisplayName(func), - value: func, - }) + + {(fields, { add, remove }, { errors }) => ( + <> + + + + {fields.map(({ key, name }) => ( +
+ {name > 0 && ( + + )} +
+
+ + ({ - label: startCase(func), - value: func, - }))} - placeholder={t('label.select-field', { - field: t('label.effect'), - })} - /> - + + - {map(AlertActionType, (value) => { - return ( - - - {getAlertsActionTypeIcon( - value as AlertActionType - )} - {value} - - - ); - })} - - - - - - - - - - - - - {get( - alertActions, - `${name}.enabled`, - false - ) && ( - <> - - - - - - - - - - - - - )} + + {(fields, { add, remove }, { errors }) => ( + <> + + + + {fields.map(({ key, name }) => ( +
+ {name > 0 && ( + + )} +
+
+ + + + {getDestinationConfigFields(name)} +
+
-
- - {fields.length - 1 !== key && } -
- ))} - - - - - )} - - - - - - - - + ))} + + + )} + + + + + + + + + + + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/add-alerts-page.styles.less b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/add-alerts-page.styles.less index c9fd2b493db..88e6104d395 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/add-alerts-page.styles.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddAlertPage/add-alerts-page.styles.less @@ -13,7 +13,7 @@ .alerts-notification-form { .ant-form-item { - margin-bottom: 12px; + margin-bottom: 16px; } .footer { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertRowExpanded.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertRowExpanded.tsx new file mode 100644 index 00000000000..4cdcd44ca32 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertRowExpanded.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Table, Typography } from 'antd'; +import { startCase } from 'lodash'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getAlertActionForAlerts } from '../../axiosAPIs/alertsAPI'; +import Loader from '../../components/Loader/Loader'; +import { AlertAction } from '../../generated/alerts/alertAction'; +import { Alerts } from '../../generated/alerts/alerts'; +import { showErrorToast } from '../../utils/ToastUtils'; + +export const AlertsExpanded = ({ alert }: { alert: Alerts }) => { + const [alertActions, setAlertActions] = useState([]); + const [loading, setLoading] = useState(true); + const { t } = useTranslation(); + const fetchAlertActions = useCallback(async () => { + try { + setLoading(true); + const response = await getAlertActionForAlerts(alert.id); + setAlertActions(response); + } catch (error) { + showErrorToast( + t('server.entity-fetch-error', { entity: t('label.alert-actions') }) + ); + } finally { + setLoading(false); + } + }, [alert.id]); + + useEffect(() => { + if (alert.id) { + fetchAlertActions(); + } + }, [alert.id]); + + const columns = useMemo( + () => [ + { + title: t('label.name'), + dataIndex: 'displayName', + width: '200px', + key: 'displayName', + }, + { + title: t('label.destination'), + dataIndex: 'alertActionType', + width: '200px', + key: 'alertActionType', + render: startCase, + }, + { + title: t('label.batch-size'), + dataIndex: 'batchSize', + width: '200px', + key: 'batchSize', + }, + { + title: t('label.timeout'), + dataIndex: 'timeout', + width: '200px', + key: 'timeout', + }, + { + title: t('label.endpoint'), + dataIndex: ['alertActionConfig', 'endpoint'], + width: '200px', + key: 'endpoint', + }, + ], + [] + ); + + return ( +
+ {t('label.alert-actions')} + }} + pagination={false} + rowKey="name" + size="small" + /> + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertsPage.tsx index ed2c28d9c5c..a6774b0914c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertsPage/AlertsPage.tsx @@ -10,11 +10,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Icon from '@ant-design/icons/lib/components/Icon'; import { Button, Col, Row, Table, Tag, Tooltip, Typography } from 'antd'; import { isNil } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { ReactComponent as DropDownIcon } from '../../assets/svg/DropDown.svg'; +import { ReactComponent as RightArrowIcon } from '../../assets/svg/ic-right-arrow.svg'; import { getAllAlerts } from '../../axiosAPIs/alertsAPI'; import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal'; import NextPrevious from '../../components/common/next-previous/NextPrevious'; @@ -32,6 +35,7 @@ import { getDisplayNameForTriggerType } from '../../utils/Alerts/AlertsUtil'; import { getSettingPath } from '../../utils/RouterUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { AlertsExpanded } from './AlertRowExpanded'; const AlertsPage = () => { const [loading, setLoading] = useState(true); @@ -166,6 +170,17 @@ const AlertsPage = () => { bordered columns={columns} dataSource={alerts} + expandable={{ + expandedRowRender: (record) => , + expandIcon: ({ expanded, onExpand, expandable, record }) => + expandable && ( + onExpand(record, e)} + /> + ), + }} loading={{ spinning: loading, indicator: }} pagination={false} rowKey="id" @@ -187,6 +202,7 @@ const AlertsPage = () => { { return ( - +
{heading}
{subHeading} - +
); }; @@ -83,3 +84,43 @@ export const getDisplayNameForTriggerType = (type: AlertTriggerType) => { return i18next.t('label.specific-data-assets'); } }; + +/** + * + * @param name Field name used to identify which field has error + * @param minLengthRequired how many item should be there in the list + * @returns If validation failed throws an error else resolve + */ +export const listLengthValidator = + (name: string, minLengthRequired = 1) => + async (_: RuleObject, list: T[]) => { + if (!list || list.length < minLengthRequired) { + return Promise.reject( + new Error( + i18next.t('message.length-validator-error', { + length: minLengthRequired, + field: name, + }) + ) + ); + } + + return Promise.resolve(); + }; + +export const getAlertActionTypeDisplayName = ( + alertActionType: AlertActionType +) => { + switch (alertActionType) { + case AlertActionType.ActivityFeed: + return i18next.t('label.activity-feed'); + case AlertActionType.Email: + return i18next.t('label.email'); + case AlertActionType.GenericWebhook: + return i18next.t('label.webhook'); + case AlertActionType.SlackWebhook: + return i18next.t('label.slack'); + case AlertActionType.MSTeamsWebhook: + return i18next.t('label.ms-teams'); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 49c680acbe6..bfe686ff28e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -11,8 +11,6 @@ * limitations under the License. */ -import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Typography } from 'antd'; import { ExpandableConfig } from 'antd/lib/table/interface'; import classNames from 'classnames'; @@ -21,6 +19,7 @@ import { isEmpty, upperCase } from 'lodash'; import { EntityTags } from 'Models'; import React from 'react'; import { ReactComponent as DashboardIcon } from '../assets/svg/dashboard-grey.svg'; +import { ReactComponent as DropDownIcon } from '../assets/svg/DropDown.svg'; import { ReactComponent as MlModelIcon } from '../assets/svg/mlmodal.svg'; import { ReactComponent as PipelineIcon } from '../assets/svg/pipeline-grey.svg'; import { ReactComponent as TableIcon } from '../assets/svg/table-grey.svg'; @@ -373,7 +372,7 @@ export function getTableExpandableConfig< className="m-r-xs cursor-pointer" data-testid="expand-icon" onClick={(e) => onExpand(record, e)}> - + {expanded ? : } ), };