mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-28 17:23:28 +00:00
fix(ui): edit alert and misc feedback for alerts page (#9388)
* fix(ui): edit alert and misc feedback for alerts page * disable destination for system generated alerts * minor ui comments resolved * fix edit mode in alerts workflow * address comments
This commit is contained in:
parent
2398161653
commit
bac86cba69
@ -1,3 +1,3 @@
|
|||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.00002 8.65912C5.8954 8.65912 5.79067 8.61915 5.71079 8.53927L1.61989 4.44834C1.46004 4.28849 1.46004 4.02964 1.61989 3.86989C1.77974 3.71014 2.03859 3.71004 2.19834 3.86989L6.00002 7.6716L9.80167 3.86989C9.96157 3.71004 10.2204 3.71004 10.3802 3.86989C10.5399 4.02974 10.54 4.28859 10.3802 4.44834L6.28925 8.53927C6.20937 8.61915 6.10464 8.65912 6.00002 8.65912Z" fill="#37352F" stroke="#37352F" stroke-width="0.6"/>
|
<path d="M6.00002 8.65912C5.8954 8.65912 5.79067 8.61915 5.71079 8.53927L1.61989 4.44834C1.46004 4.28849 1.46004 4.02964 1.61989 3.86989C1.77974 3.71014 2.03859 3.71004 2.19834 3.86989L6.00002 7.6716L9.80167 3.86989C9.96157 3.71004 10.2204 3.71004 10.3802 3.86989C10.5399 4.02974 10.54 4.28859 10.3802 4.44834L6.28925 8.53927C6.20937 8.61915 6.10464 8.65912 6.00002 8.65912Z" fill="#37352F" stroke="#37352F" stroke-width="0.6"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 510 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.5455 7.99998C11.5455 8.13947 11.4922 8.27911 11.3857 8.38561L5.93112 13.8401C5.71799 14.0533 5.37285 14.0533 5.15985 13.8401C4.94685 13.627 4.94672 13.2819 5.15985 13.0689L10.2288 7.99998L5.15985 2.93107C4.94672 2.71794 4.94672 2.3728 5.15985 2.1598C5.37299 1.9468 5.71812 1.94667 5.93112 2.1598L11.3857 7.61434C11.4922 7.72084 11.5455 7.86048 11.5455 7.99998Z" fill="#37352F" stroke="#37352F" stroke-width="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 531 B |
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
import { PagingResponse } from 'Models';
|
import { PagingResponse } from 'Models';
|
||||||
import axiosClient from '.';
|
import axiosClient from '.';
|
||||||
import { AlertAction } from '../generated/alerts/alertAction';
|
import { AlertAction } from '../generated/alerts/alertAction';
|
||||||
@ -36,6 +37,21 @@ export const createAlertAction = async (alertAction: CreateAlertAction) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const patchAlertAction = async (id: string, operation: Operation) => {
|
||||||
|
const response = await axiosClient.patch<
|
||||||
|
Operation,
|
||||||
|
AxiosResponse<AlertAction>
|
||||||
|
>(`${BASE_URL}/${id}`, operation);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAlertAction = async (alertAction: AlertAction) => {
|
||||||
|
const response = await axiosClient.put<AlertAction>(BASE_URL, alertAction);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteAlertAction = async (id: string) => {
|
export const deleteAlertAction = async (id: string) => {
|
||||||
const response = await axiosClient.delete(`${BASE_URL}/${id}`);
|
const response = await axiosClient.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,11 @@
|
|||||||
|
|
||||||
import { PagingResponse } from 'Models';
|
import { PagingResponse } from 'Models';
|
||||||
import axiosClient from '.';
|
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 { Alerts, TriggerConfig } from '../generated/alerts/alerts';
|
||||||
import { EntitySpelFilters } from '../generated/alerts/entitySpelFilters';
|
import { EntitySpelFilters } from '../generated/alerts/entitySpelFilters';
|
||||||
import { Function } from '../generated/type/function';
|
import { Function } from '../generated/type/function';
|
||||||
@ -60,6 +64,12 @@ export const createAlert = async (alert: Alerts) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateAlert = async (alert: Alerts) => {
|
||||||
|
const response = await axiosClient.put<Alerts>(BASE_URL, alert);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteAlert = async (id: string) => {
|
export const deleteAlert = async (id: string) => {
|
||||||
const response = await axiosClient.delete(`${BASE_URL}/${id}`);
|
const response = await axiosClient.delete(`${BASE_URL}/${id}`);
|
||||||
|
|
||||||
@ -94,7 +104,9 @@ export const getDefaultTriggerConfigs = async (after?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAlertActionForAlerts = async (id: string) => {
|
export const getAlertActionForAlerts = async (id: string) => {
|
||||||
const response = await axiosClient.get(`${BASE_URL}/allAlertAction/${id}`);
|
const response = await axiosClient.get<AlertAction[]>(
|
||||||
|
`${BASE_URL}/allAlertAction/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -216,7 +216,7 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
<DropDown className="flex self-center" />
|
<DropDown className="flex self-center" height={12} width={12} />
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -17,10 +17,16 @@
|
|||||||
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.3);
|
||||||
height: 60px;
|
height: 60px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 40px;
|
right: 20px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
scale: 0.75;
|
||||||
|
transition: scale 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slack-chat .bubble:hover {
|
||||||
|
scale: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -408,7 +408,6 @@
|
|||||||
"edit-team-type": "Edit Team Type",
|
"edit-team-type": "Edit Team Type",
|
||||||
"configure-webhook-type": "Configure {{webhookType}} Webhooks",
|
"configure-webhook-type": "Configure {{webhookType}} Webhooks",
|
||||||
"ms-teams": "MS Teams",
|
"ms-teams": "MS Teams",
|
||||||
"ms-team": "MS Team",
|
|
||||||
"bot": "Bot",
|
"bot": "Bot",
|
||||||
"bot-plural": "Bots",
|
"bot-plural": "Bots",
|
||||||
"policy-name": "Policy name",
|
"policy-name": "Policy name",
|
||||||
@ -584,7 +583,15 @@
|
|||||||
"completed-description": "Completed Description",
|
"completed-description": "Completed Description",
|
||||||
"assigned-entity": "Assigned {{entity}}",
|
"assigned-entity": "Assigned {{entity}}",
|
||||||
"total-assets-view": "Total Assets View",
|
"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": {
|
"message": {
|
||||||
"service-email-required": "Service account Email is required",
|
"service-email-required": "Service account Email is required",
|
||||||
@ -709,7 +716,8 @@
|
|||||||
"alerts-description": "Stay current with timely alerts using webhooks.",
|
"alerts-description": "Stay current with timely alerts using webhooks.",
|
||||||
"alerts-destination-description": "Send notifications to Slack, MS Teams, Email, or and use 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-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": {
|
"server": {
|
||||||
"you-have-not-action-anything-yet": "You have not {{action}} anything yet.",
|
"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}}",
|
"ingestion-workflow-operation-error": "Error while {{operation}} ingestion workflow {{displayName}}",
|
||||||
"team-moved-error": "Error while moving team",
|
"team-moved-error": "Error while moving team",
|
||||||
"entity-details-fetch-error": "Error while fetching details for {{entityType}} {{entityName}}",
|
"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": {}
|
"url": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -404,7 +404,6 @@
|
|||||||
"edit-team-type": "Mettre à jour le type d'équipe",
|
"edit-team-type": "Mettre à jour le type d'équipe",
|
||||||
"configure-webhook-type": "Configurer {{webhookType}} Webhooks",
|
"configure-webhook-type": "Configurer {{webhookType}} Webhooks",
|
||||||
"ms-teams": "MS Teams",
|
"ms-teams": "MS Teams",
|
||||||
"ms-team": "MS Team",
|
|
||||||
"policy-name": "Nom de Police",
|
"policy-name": "Nom de Police",
|
||||||
"add-new-policy": "Ajouter une nouvelle police",
|
"add-new-policy": "Ajouter une nouvelle police",
|
||||||
"policies": "Polices",
|
"policies": "Polices",
|
||||||
|
|||||||
@ -14,30 +14,35 @@ import { PlusOutlined } from '@ant-design/icons';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
|
Collapse,
|
||||||
Divider,
|
Divider,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useForm } from 'antd/lib/form/Form';
|
import { useForm } from 'antd/lib/form/Form';
|
||||||
import { DefaultOptionType } from 'antd/lib/select';
|
import { DefaultOptionType } from 'antd/lib/select';
|
||||||
import { AxiosError } from 'axios';
|
import { get, intersection, isEmpty, map, pick, startCase, trim } from 'lodash';
|
||||||
import { get, intersection, isEmpty, map, startCase } from 'lodash';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { createAlertAction } from '../../axiosAPIs/alertActionAPI';
|
import {
|
||||||
|
createAlertAction,
|
||||||
|
updateAlertAction,
|
||||||
|
} from '../../axiosAPIs/alertActionAPI';
|
||||||
import {
|
import {
|
||||||
createAlert,
|
createAlert,
|
||||||
|
getAlertActionForAlerts,
|
||||||
getAlertsFromId,
|
getAlertsFromId,
|
||||||
getDefaultTriggerConfigs,
|
getDefaultTriggerConfigs,
|
||||||
getEntityFilterFunctions,
|
getEntityFilterFunctions,
|
||||||
getFilterFunctions,
|
getFilterFunctions,
|
||||||
|
updateAlert,
|
||||||
} from '../../axiosAPIs/alertsAPI';
|
} from '../../axiosAPIs/alertsAPI';
|
||||||
import {
|
import {
|
||||||
getSearchedUsersAndTeams,
|
getSearchedUsersAndTeams,
|
||||||
@ -51,19 +56,26 @@ import {
|
|||||||
import { PROMISE_STATE } from '../../enums/common.enum';
|
import { PROMISE_STATE } from '../../enums/common.enum';
|
||||||
import { AlertAction } from '../../generated/alerts/alertAction';
|
import { AlertAction } from '../../generated/alerts/alertAction';
|
||||||
import {
|
import {
|
||||||
|
AlertFilterRule,
|
||||||
Alerts,
|
Alerts,
|
||||||
AlertTriggerType,
|
AlertTriggerType,
|
||||||
Effect,
|
Effect,
|
||||||
EntityReference,
|
EntityReference,
|
||||||
|
ProviderType,
|
||||||
TriggerConfig,
|
TriggerConfig,
|
||||||
} from '../../generated/alerts/alerts';
|
} 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 { EntitySpelFilters } from '../../generated/alerts/entitySpelFilters';
|
||||||
import { Function } from '../../generated/type/function';
|
import { Function } from '../../generated/type/function';
|
||||||
import {
|
import {
|
||||||
|
getAlertActionTypeDisplayName,
|
||||||
getAlertsActionTypeIcon,
|
getAlertsActionTypeIcon,
|
||||||
getDisplayNameForTriggerType,
|
getDisplayNameForTriggerType,
|
||||||
getFunctionDisplayName,
|
getFunctionDisplayName,
|
||||||
|
listLengthValidator,
|
||||||
StyledCard,
|
StyledCard,
|
||||||
} from '../../utils/Alerts/AlertsUtil';
|
} from '../../utils/Alerts/AlertsUtil';
|
||||||
import { getSettingPath } from '../../utils/RouterUtils';
|
import { getSettingPath } from '../../utils/RouterUtils';
|
||||||
@ -76,6 +88,8 @@ const AddAlertPage = () => {
|
|||||||
const [form] = useForm<Alerts>();
|
const [form] = useForm<Alerts>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { fqn } = useParams<{ fqn: string }>();
|
const { fqn } = useParams<{ fqn: string }>();
|
||||||
|
// To block certain action based on provider of the Alert e.g. System / User
|
||||||
|
const [provider, setProvider] = useState<ProviderType>(ProviderType.User);
|
||||||
|
|
||||||
const [filterFunctions, setFilterFunctions] = useState<Function[]>();
|
const [filterFunctions, setFilterFunctions] = useState<Function[]>();
|
||||||
const [defaultTriggers, setDefaultTriggers] = useState<Array<TriggerConfig>>(
|
const [defaultTriggers, setDefaultTriggers] = useState<Array<TriggerConfig>>(
|
||||||
@ -88,23 +102,34 @@ const AddAlertPage = () => {
|
|||||||
const fetchAlert = async () => {
|
const fetchAlert = async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingCount((count) => count + 1);
|
setLoadingCount((count) => count + 1);
|
||||||
|
|
||||||
const response: Alerts = await getAlertsFromId(fqn);
|
const response: Alerts = await getAlertsFromId(fqn);
|
||||||
|
const alertActions = await getAlertActionForAlerts(response.id);
|
||||||
|
|
||||||
const requestFilteringRules =
|
const requestFilteringRules =
|
||||||
response.filteringRules?.map((curr) => ({
|
response.filteringRules?.map(
|
||||||
|
(curr) =>
|
||||||
|
({
|
||||||
...curr,
|
...curr,
|
||||||
condition: curr.condition
|
condition: curr.condition
|
||||||
.replace(new RegExp(`${curr.name}\\('`), '')
|
.replace(new RegExp(`${curr.name}\\('`), '')
|
||||||
.replace(new RegExp(`'\\)`), ''),
|
.replaceAll("'", '')
|
||||||
})) ?? [];
|
.replace(new RegExp(`\\)`), '')
|
||||||
|
.split(',')
|
||||||
|
.map(trim),
|
||||||
|
} as unknown as AlertFilterRule)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
setProvider(response.provider ?? ProviderType.User);
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...response,
|
...response,
|
||||||
filteringRules: requestFilteringRules,
|
filteringRules: requestFilteringRules,
|
||||||
|
alertActions: alertActions as unknown as EntityReference[],
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
t('message.entity-fetch-error', { entity: t('label.alert') }),
|
t('server.entity-fetch-error', { entity: t('label.alert') }),
|
||||||
fqn
|
fqn
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@ -149,29 +174,37 @@ const AddAlertPage = () => {
|
|||||||
fetchDefaultTriggerConfig();
|
fetchDefaultTriggerConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = async (data: Alerts) => {
|
const isEditMode = useMemo(() => !isEmpty(fqn), [fqn]);
|
||||||
const { filteringRules, alertActions } = data;
|
|
||||||
|
|
||||||
const requestFilteringRules = filteringRules?.map((curr) => ({
|
const updateCreateAlertActions = async (alertActions: AlertAction[]) => {
|
||||||
...curr,
|
const api = isEditMode ? updateAlertAction : createAlertAction;
|
||||||
condition: `${curr.name}(${map(
|
if (isEditMode) {
|
||||||
curr.condition,
|
if (!form.isFieldTouched(['alertActions'])) {
|
||||||
(v: string) => `'${v}'`
|
// If destination is not changed return given alertAction as it is
|
||||||
)?.join(', ')})`,
|
return Promise.resolve(
|
||||||
}));
|
alertActions.map((action) => ({
|
||||||
|
id: action.id ?? '',
|
||||||
|
type: 'alertAction',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Else Create AlertActions and return new IDs
|
||||||
const actions: AlertAction[] =
|
const promises =
|
||||||
alertActions?.map(
|
alertActions?.map((action) =>
|
||||||
(action) =>
|
api(
|
||||||
({
|
pick(action, [
|
||||||
...action,
|
'alertActionConfig',
|
||||||
enabled: true,
|
'alertActionType',
|
||||||
} as unknown as AlertAction)
|
'name',
|
||||||
|
'displayName',
|
||||||
|
'timeout',
|
||||||
|
'batchSize',
|
||||||
|
]) as CreateAlertAction
|
||||||
|
)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const promises = actions.map((action) => createAlertAction(action));
|
|
||||||
|
|
||||||
const responses = await Promise.allSettled(promises);
|
const responses = await Promise.allSettled(promises);
|
||||||
|
|
||||||
const requestAlertActions: EntityReference[] = responses.map((res) => {
|
const requestAlertActions: EntityReference[] = responses.map((res) => {
|
||||||
@ -185,15 +218,50 @@ const AddAlertPage = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
condition: `${curr.name}(${map(
|
||||||
|
curr.condition,
|
||||||
|
(v: string) => `'${v}'`
|
||||||
|
)?.join(', ')})`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modifiedAlertActions = alertActions?.map(
|
||||||
|
(action) =>
|
||||||
|
({
|
||||||
|
...action,
|
||||||
|
name: action.name ?? action.displayName,
|
||||||
|
displayName: action.displayName,
|
||||||
|
} as unknown as AlertAction)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createAlert({
|
const requestAlertActions = await updateCreateAlertActions(
|
||||||
|
modifiedAlertActions
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api({
|
||||||
...data,
|
...data,
|
||||||
filteringRules: requestFilteringRules,
|
filteringRules: requestFilteringRules,
|
||||||
alertActions: requestAlertActions,
|
alertActions: requestAlertActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
t('server.create-entity-success', { entity: t('alert-plural') })
|
t(`server.${isEditMode ? 'update' : 'create'}-entity-success`, {
|
||||||
|
entity: t('label.alert-plural'),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
history.push(
|
history.push(
|
||||||
getSettingPath(
|
getSettingPath(
|
||||||
@ -203,15 +271,24 @@ const AddAlertPage = () => {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
t('server.entity-creation-error', {
|
t(
|
||||||
|
`server.${
|
||||||
|
isEditMode ? 'entity-updating-error' : 'entity-creation-error'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
entity: t('label.alert-plural'),
|
entity: t('label.alert-plural'),
|
||||||
}),
|
}
|
||||||
(error as AxiosError).message
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(
|
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) => ({
|
return response.hits.hits.map((d) => ({
|
||||||
label: d._source.displayName ?? d._source.name,
|
label: d._source.displayName ?? d._source.name,
|
||||||
value: d._source.id,
|
value: d._source.fullyQualifiedName,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
@ -313,37 +390,171 @@ const AddAlertPage = () => {
|
|||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
const filters = Form.useWatch(['filteringRules'], form);
|
const filters = Form.useWatch(['filteringRules'], form);
|
||||||
const alertActions = Form.useWatch(['alertActions'], form);
|
|
||||||
const entitySelected = Form.useWatch(['triggerConfig', 'entities'], form);
|
const entitySelected = Form.useWatch(['triggerConfig', 'entities'], form);
|
||||||
const trigger = Form.useWatch(['triggerConfig', 'type'], form);
|
const trigger = Form.useWatch(['triggerConfig', 'type'], form);
|
||||||
|
const alertActions = Form.useWatch(['alertActions'], form);
|
||||||
|
|
||||||
// Run time values needed for conditional rendering
|
// Run time values needed for conditional rendering
|
||||||
const functions = useMemo(() => {
|
const functions = useMemo(() => {
|
||||||
if (entityFunctions) {
|
if (entityFunctions) {
|
||||||
if (!trigger || trigger === AlertTriggerType.AllDataAssets) {
|
const exitingFunctions = filters?.map((f) => f.name) ?? [];
|
||||||
return entityFunctions['all'].supportedFunctions.sort();
|
let supportedFunctions: string[][] = [];
|
||||||
}
|
|
||||||
|
|
||||||
const arrFunctions = entitySelected?.map(
|
if (!trigger || trigger === AlertTriggerType.AllDataAssets) {
|
||||||
|
supportedFunctions = [entityFunctions['all'].supportedFunctions];
|
||||||
|
} else {
|
||||||
|
supportedFunctions =
|
||||||
|
entitySelected?.map(
|
||||||
(entity) =>
|
(entity) =>
|
||||||
entityFunctions[entity as unknown as string].supportedFunctions
|
entityFunctions[entity as unknown as string].supportedFunctions
|
||||||
);
|
) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
const functions = arrFunctions
|
const functions = intersection(...supportedFunctions)
|
||||||
? intersection(...arrFunctions).sort()
|
.sort()
|
||||||
: [];
|
.map((func) => ({
|
||||||
|
label: getFunctionDisplayName(func),
|
||||||
|
value: func,
|
||||||
|
disabled: exitingFunctions.includes(func),
|
||||||
|
}));
|
||||||
|
|
||||||
return functions as string[];
|
return functions as DefaultOptionType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}, [entitySelected, entityFunctions]);
|
}, [entitySelected, entityFunctions, filters]);
|
||||||
|
|
||||||
const selectedTrigger = useMemo(
|
const selectedTrigger = useMemo(
|
||||||
() => defaultTriggers.find(({ type }) => trigger === type),
|
() => defaultTriggers.find(({ type }) => trigger === type),
|
||||||
[defaultTriggers, trigger]
|
[defaultTriggers, trigger]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChange = (changedValues: Partial<Alerts>) => {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
required
|
||||||
|
label={t('label.name')}
|
||||||
|
labelCol={{ span: 24 }}
|
||||||
|
name={[name, 'displayName']}>
|
||||||
|
<Input
|
||||||
|
disabled={provider === ProviderType.System}
|
||||||
|
placeholder={t('label.name')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('label.receiver-plural')}
|
||||||
|
labelCol={{ span: 24 }}
|
||||||
|
name={[name, 'alertActionConfig', 'receivers']}>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
mode="tags"
|
||||||
|
open={false}
|
||||||
|
placeholder={t('label.enter-entity', {
|
||||||
|
entity: t('label.email-plural'),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Space align="baseline">
|
||||||
|
<label>{t('label.send-to')}:</label>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'alertActionConfig', 'sendToAdmins']}
|
||||||
|
valuePropName="checked">
|
||||||
|
<Checkbox>{t('label.admin-plural')}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'alertActionConfig', 'sendToOwners']}
|
||||||
|
valuePropName="checked">
|
||||||
|
<Checkbox>{t('label.owner-plural')}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'alertActionConfig', 'sendToFollowers']}
|
||||||
|
valuePropName="checked">
|
||||||
|
<Checkbox>{t('label.follower-plural')}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case AlertActionType.GenericWebhook:
|
||||||
|
case AlertActionType.SlackWebhook:
|
||||||
|
case AlertActionType.MSTeamsWebhook:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item required name={[name, 'displayName']}>
|
||||||
|
<Input
|
||||||
|
disabled={provider === ProviderType.System}
|
||||||
|
placeholder={t('label.name')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
required
|
||||||
|
name={[name, 'alertActionConfig', 'endpoint']}>
|
||||||
|
<Input
|
||||||
|
disabled={provider === ProviderType.System}
|
||||||
|
placeholder={
|
||||||
|
t('label.endpoint-url') +
|
||||||
|
': ' +
|
||||||
|
'http(s)://www.example.com'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Collapse ghost>
|
||||||
|
<Collapse.Panel
|
||||||
|
header={`${t('label.advanced-config')}:`}
|
||||||
|
key="1">
|
||||||
|
<Space>
|
||||||
|
<Form.Item
|
||||||
|
initialValue={10}
|
||||||
|
label="Batch Size"
|
||||||
|
labelCol={{ span: 24 }}
|
||||||
|
name={[name, 'batchSize']}>
|
||||||
|
<Input disabled={provider === ProviderType.System} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
colon
|
||||||
|
initialValue={10}
|
||||||
|
label={`${t(
|
||||||
|
'label.connection-timeout-plural-optional'
|
||||||
|
)}`}
|
||||||
|
labelCol={{ span: 24 }}
|
||||||
|
name={[name, 'timeout']}>
|
||||||
|
<Input disabled={provider === ProviderType.System} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Form.Item
|
||||||
|
label={t('label.secret-key')}
|
||||||
|
labelCol={{ span: 24 }}
|
||||||
|
name={[name, 'alertActionConfig', 'secretKey']}>
|
||||||
|
<Input
|
||||||
|
disabled={provider === ProviderType.System}
|
||||||
|
placeholder={t('label.secret-key')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
},
|
||||||
|
[alertActions]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
@ -359,14 +570,15 @@ const AddAlertPage = () => {
|
|||||||
<Form<Alerts>
|
<Form<Alerts>
|
||||||
className="alerts-notification-form"
|
className="alerts-notification-form"
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={handleSave}>
|
onFinish={handleSave}
|
||||||
|
onValuesChange={handleChange}>
|
||||||
<Card loading={loadingCount > 0}>
|
<Card loading={loadingCount > 0}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('label.name')}
|
label={t('label.name')}
|
||||||
labelCol={{ span: 24 }}
|
labelCol={{ span: 24 }}
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true }]}>
|
rules={[{ required: true }]}>
|
||||||
<Input />
|
<Input disabled={isEditMode} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('label.description')}
|
label={t('label.description')}
|
||||||
@ -375,7 +587,7 @@ const AddAlertPage = () => {
|
|||||||
rules={[{ required: true }]}>
|
rules={[{ required: true }]}>
|
||||||
<Input.TextArea />
|
<Input.TextArea />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Space className="w-full" direction="vertical" size={16}>
|
<Space className="w-full" direction="vertical" size={16}>
|
||||||
@ -396,7 +608,12 @@ const AddAlertPage = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
{selectedTrigger?.type ===
|
{selectedTrigger?.type ===
|
||||||
AlertTriggerType.SpecificDataAsset && (
|
AlertTriggerType.SpecificDataAsset && (
|
||||||
<Form.Item name={['triggerConfig', 'entities']}>
|
<Form.Item
|
||||||
|
required
|
||||||
|
messageVariables={{
|
||||||
|
fieldName: t('label.data-assets'),
|
||||||
|
}}
|
||||||
|
name={['triggerConfig', 'entities']}>
|
||||||
<Select
|
<Select
|
||||||
showArrow
|
showArrow
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -421,21 +638,40 @@ const AddAlertPage = () => {
|
|||||||
subHeading={t('message.alerts-filter-description')}
|
subHeading={t('message.alerts-filter-description')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.List name="filteringRules">
|
<Form.List
|
||||||
{(fields, { add, remove }) => (
|
name="filteringRules"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: listLengthValidator(
|
||||||
|
t('label.filter-plural')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{(fields, { add, remove }, { errors }) => (
|
||||||
<>
|
<>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="default"
|
||||||
|
onClick={() => add({}, 0)}>
|
||||||
|
{t('label.add-entity', {
|
||||||
|
entity: t('label.filter-plural'),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
{fields.map(({ key, name }) => (
|
{fields.map(({ key, name }) => (
|
||||||
<div key={`filteringRules-${key}`}>
|
<div key={`filteringRules-${key}`}>
|
||||||
|
{name > 0 && (
|
||||||
|
<Divider
|
||||||
|
style={{ margin: 0, marginBottom: '16px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="d-flex gap-1">
|
<div className="d-flex gap-1">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Form.Item key={key} name={[name, 'name']}>
|
<Form.Item key={key} name={[name, 'name']}>
|
||||||
<Select
|
<Select
|
||||||
options={functions?.map(
|
options={functions}
|
||||||
(func: string) => ({
|
|
||||||
label: getFunctionDisplayName(func),
|
|
||||||
value: func,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
placeholder={t('label.select-field', {
|
placeholder={t('label.select-field', {
|
||||||
field: t('label.condition'),
|
field: t('label.condition'),
|
||||||
})}
|
})}
|
||||||
@ -453,10 +689,13 @@ const AddAlertPage = () => {
|
|||||||
key={key}
|
key={key}
|
||||||
name={[name, 'effect']}>
|
name={[name, 'effect']}>
|
||||||
<Select
|
<Select
|
||||||
options={map(Effect, (func: string) => ({
|
options={map(
|
||||||
|
Effect,
|
||||||
|
(func: string) => ({
|
||||||
label: startCase(func),
|
label: startCase(func),
|
||||||
value: func,
|
value: func,
|
||||||
}))}
|
})
|
||||||
|
)}
|
||||||
placeholder={t('label.select-field', {
|
placeholder={t('label.select-field', {
|
||||||
field: t('label.effect'),
|
field: t('label.effect'),
|
||||||
})}
|
})}
|
||||||
@ -476,22 +715,9 @@ const AddAlertPage = () => {
|
|||||||
onClick={() => remove(name)}
|
onClick={() => remove(name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{fields.length - 1 !== key && (
|
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Form.Item>
|
<Form.ErrorList errors={errors} />
|
||||||
<Button
|
|
||||||
block
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => add()}>
|
|
||||||
{t('label.add-entity', {
|
|
||||||
entity: t('label.filter-plural'),
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
@ -504,11 +730,36 @@ const AddAlertPage = () => {
|
|||||||
subHeading={t('message.alerts-destination-description')}
|
subHeading={t('message.alerts-destination-description')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.List name="alertActions">
|
<Form.List
|
||||||
{(fields, { add, remove }) => (
|
name="alertActions"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: listLengthValidator(
|
||||||
|
t('label.destination')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{(fields, { add, remove }, { errors }) => (
|
||||||
<>
|
<>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
disabled={provider === ProviderType.System}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="default"
|
||||||
|
onClick={() => add({}, 0)}>
|
||||||
|
{t('label.add-entity', {
|
||||||
|
entity: t('label.destination'),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
{fields.map(({ key, name }) => (
|
{fields.map(({ key, name }) => (
|
||||||
<div key={`alertActions-${key}`}>
|
<div key={`alertActions-${key}`}>
|
||||||
|
{name > 0 && (
|
||||||
|
<Divider
|
||||||
|
style={{ margin: 0, marginBottom: '16px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="d-flex" style={{ gap: '10px' }}>
|
<div className="d-flex" style={{ gap: '10px' }}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -516,12 +767,16 @@ const AddAlertPage = () => {
|
|||||||
key={key}
|
key={key}
|
||||||
name={[name, 'alertActionType']}>
|
name={[name, 'alertActionType']}>
|
||||||
<Select
|
<Select
|
||||||
|
disabled={
|
||||||
|
provider === ProviderType.System
|
||||||
|
}
|
||||||
placeholder={t('label.select-field', {
|
placeholder={t('label.select-field', {
|
||||||
field: t('label.source'),
|
field: t('label.source'),
|
||||||
})}
|
})}
|
||||||
showSearch={false}>
|
showSearch={false}>
|
||||||
{map(AlertActionType, (value) => {
|
{map(AlertActionType, (value) => {
|
||||||
return (
|
return value ===
|
||||||
|
AlertActionType.ActivityFeed ? null : (
|
||||||
<Select.Option
|
<Select.Option
|
||||||
key={value}
|
key={value}
|
||||||
value={value}>
|
value={value}>
|
||||||
@ -529,77 +784,16 @@ const AddAlertPage = () => {
|
|||||||
{getAlertsActionTypeIcon(
|
{getAlertsActionTypeIcon(
|
||||||
value as AlertActionType
|
value as AlertActionType
|
||||||
)}
|
)}
|
||||||
{value}
|
{getAlertActionTypeDisplayName(
|
||||||
|
value
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required name={[name, 'name']}>
|
{getDestinationConfigFields(name)}
|
||||||
<Input placeholder={t('label.name')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
required
|
|
||||||
name={[
|
|
||||||
name,
|
|
||||||
'alertActionConfig',
|
|
||||||
'endpoint',
|
|
||||||
]}>
|
|
||||||
<Input
|
|
||||||
placeholder={
|
|
||||||
t('label.endpoint-url') +
|
|
||||||
': ' +
|
|
||||||
'http(s)://www.example.com'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('label.advanced-config')}
|
|
||||||
name={[name, 'enabled']}
|
|
||||||
valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
{get(
|
|
||||||
alertActions,
|
|
||||||
`${name}.enabled`,
|
|
||||||
false
|
|
||||||
) && (
|
|
||||||
<>
|
|
||||||
<Space className="w-full" size={16}>
|
|
||||||
<Form.Item
|
|
||||||
initialValue={10}
|
|
||||||
label="Batch Size"
|
|
||||||
labelCol={{ span: 24 }}
|
|
||||||
name={[name, 'batchSize']}>
|
|
||||||
<Input defaultValue={10} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
colon
|
|
||||||
initialValue={10}
|
|
||||||
label={`${t(
|
|
||||||
'label.connection-timeout-plural-optional'
|
|
||||||
)}`}
|
|
||||||
labelCol={{ span: 24 }}
|
|
||||||
name={[name, 'timeout']}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
<Form.Item
|
|
||||||
label={t('label.secret-key')}
|
|
||||||
labelCol={{ span: 24 }}
|
|
||||||
name={[
|
|
||||||
name,
|
|
||||||
'alertActionConfig',
|
|
||||||
'secretKey',
|
|
||||||
]}>
|
|
||||||
<Input
|
|
||||||
placeholder={t('label.secret-key')}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-testid={`remove-filter-rule-${name}`}
|
data-testid={`remove-filter-rule-${name}`}
|
||||||
@ -614,21 +808,9 @@ const AddAlertPage = () => {
|
|||||||
onClick={() => remove(name)}
|
onClick={() => remove(name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fields.length - 1 !== key && <Divider />}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Form.Item>
|
<Form.ErrorList errors={errors} />
|
||||||
<Button
|
|
||||||
block
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => add()}>
|
|
||||||
{t('label.add-entity', {
|
|
||||||
entity: t('label.destination'),
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
@ -643,9 +825,12 @@ const AddAlertPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</Form.Item>
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={24} />
|
||||||
|
<Col span={24} />
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
.alerts-notification-form {
|
.alerts-notification-form {
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|||||||
@ -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<AlertAction[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="p-x-sm">
|
||||||
|
<Typography.Title level={5}>{t('label.alert-actions')}</Typography.Title>
|
||||||
|
<Table
|
||||||
|
bordered={false}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={alertActions}
|
||||||
|
loading={{ spinning: loading, indicator: <Loader size="small" /> }}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="name"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -10,11 +10,14 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||||
import { Button, Col, Row, Table, Tag, Tooltip, Typography } from 'antd';
|
import { Button, Col, Row, Table, Tag, Tooltip, Typography } from 'antd';
|
||||||
import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
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 { getAllAlerts } from '../../axiosAPIs/alertsAPI';
|
||||||
import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal';
|
import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal';
|
||||||
import NextPrevious from '../../components/common/next-previous/NextPrevious';
|
import NextPrevious from '../../components/common/next-previous/NextPrevious';
|
||||||
@ -32,6 +35,7 @@ import { getDisplayNameForTriggerType } from '../../utils/Alerts/AlertsUtil';
|
|||||||
import { getSettingPath } from '../../utils/RouterUtils';
|
import { getSettingPath } from '../../utils/RouterUtils';
|
||||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||||
import { showErrorToast } from '../../utils/ToastUtils';
|
import { showErrorToast } from '../../utils/ToastUtils';
|
||||||
|
import { AlertsExpanded } from './AlertRowExpanded';
|
||||||
|
|
||||||
const AlertsPage = () => {
|
const AlertsPage = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -166,6 +170,17 @@ const AlertsPage = () => {
|
|||||||
bordered
|
bordered
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={alerts}
|
dataSource={alerts}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: (record) => <AlertsExpanded alert={record} />,
|
||||||
|
expandIcon: ({ expanded, onExpand, expandable, record }) =>
|
||||||
|
expandable && (
|
||||||
|
<Icon
|
||||||
|
component={expanded ? DropDownIcon : RightArrowIcon}
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => onExpand(record, e)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
loading={{ spinning: loading, indicator: <Loader /> }}
|
loading={{ spinning: loading, indicator: <Loader /> }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
@ -187,6 +202,7 @@ const AlertsPage = () => {
|
|||||||
|
|
||||||
<DeleteWidgetModal
|
<DeleteWidgetModal
|
||||||
afterDeleteAction={handleAlertDelete}
|
afterDeleteAction={handleAlertDelete}
|
||||||
|
allowSoftDelete={false}
|
||||||
entityId={selectedAlert?.id || ''}
|
entityId={selectedAlert?.id || ''}
|
||||||
entityName={selectedAlert?.name || ''}
|
entityName={selectedAlert?.name || ''}
|
||||||
entityType={EntityType.ALERT}
|
entityType={EntityType.ALERT}
|
||||||
|
|||||||
@ -324,3 +324,16 @@
|
|||||||
.quick-filter-dropdown-trigger-btn:focus {
|
.quick-filter-dropdown-trigger-btn:focus {
|
||||||
background-color: @trigger-btn-hover-bg;
|
background-color: @trigger-btn-hover-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate
|
||||||
|
.rotate-inverse {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate--90 {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|||||||
@ -362,10 +362,6 @@
|
|||||||
animation: highlight 3000ms ease-out;
|
animation: highlight 3000ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate-inverse {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rjsf.no-header #root__title,
|
.rjsf.no-header #root__title,
|
||||||
.rjsf.no-header #root__description,
|
.rjsf.no-header #root__description,
|
||||||
.rjsf .form-additional .control-label,
|
.rjsf .form-additional .control-label,
|
||||||
|
|||||||
@ -11,14 +11,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Card, Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import { RuleObject } from 'antd/lib/form';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactComponent as AllActivityIcon } from '../../assets/svg/all-activity.svg';
|
import { ReactComponent as AllActivityIcon } from '../../assets/svg/all-activity.svg';
|
||||||
import { ReactComponent as MailIcon } from '../../assets/svg/ic-mail.svg';
|
import { ReactComponent as MailIcon } from '../../assets/svg/ic-mail.svg';
|
||||||
import { ReactComponent as MSTeamsIcon } from '../../assets/svg/ms-teams-grey.svg';
|
import { ReactComponent as MSTeamsIcon } from '../../assets/svg/ms-teams.svg';
|
||||||
import { ReactComponent as SlackIcon } from '../../assets/svg/slack-grey.svg';
|
import { ReactComponent as SlackIcon } from '../../assets/svg/slack.svg';
|
||||||
import { ReactComponent as WebhookIcon } from '../../assets/svg/webhook-grey.svg';
|
import { ReactComponent as WebhookIcon } from '../../assets/svg/webhook.svg';
|
||||||
import { AlertActionType } from '../../generated/alerts/alertAction';
|
import { AlertActionType } from '../../generated/alerts/alertAction';
|
||||||
import { AlertTriggerType } from '../../generated/alerts/alerts';
|
import { AlertTriggerType } from '../../generated/alerts/alerts';
|
||||||
|
|
||||||
@ -65,13 +66,13 @@ export const StyledCard = ({
|
|||||||
subHeading: string;
|
subHeading: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card bordered={false} className="bg-grey">
|
<div className="bg-grey p-sm rounded-4">
|
||||||
<Typography.Text>{heading}</Typography.Text>
|
<Typography.Text>{heading}</Typography.Text>
|
||||||
<br />
|
<br />
|
||||||
<Typography.Text className="text-xs text-grey-muted">
|
<Typography.Text className="text-xs text-grey-muted">
|
||||||
{subHeading}
|
{subHeading}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,3 +84,43 @@ export const getDisplayNameForTriggerType = (type: AlertTriggerType) => {
|
|||||||
return i18next.t('label.specific-data-assets');
|
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 =
|
||||||
|
<T,>(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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -11,8 +11,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ExpandableConfig } from 'antd/lib/table/interface';
|
import { ExpandableConfig } from 'antd/lib/table/interface';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -21,6 +19,7 @@ import { isEmpty, upperCase } from 'lodash';
|
|||||||
import { EntityTags } from 'Models';
|
import { EntityTags } from 'Models';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactComponent as DashboardIcon } from '../assets/svg/dashboard-grey.svg';
|
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 MlModelIcon } from '../assets/svg/mlmodal.svg';
|
||||||
import { ReactComponent as PipelineIcon } from '../assets/svg/pipeline-grey.svg';
|
import { ReactComponent as PipelineIcon } from '../assets/svg/pipeline-grey.svg';
|
||||||
import { ReactComponent as TableIcon } from '../assets/svg/table-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"
|
className="m-r-xs cursor-pointer"
|
||||||
data-testid="expand-icon"
|
data-testid="expand-icon"
|
||||||
onClick={(e) => onExpand(record, e)}>
|
onClick={(e) => onExpand(record, e)}>
|
||||||
<FontAwesomeIcon icon={expanded ? faCaretDown : faCaretRight} />
|
{expanded ? <DropDownIcon /> : <DropDownIcon />}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user