feat(ui): alert details page (#9454)

* feat(ui): alert details page
fix edit alert flow broken

* incorporate backend changes

* address comments
This commit is contained in:
Chirag Madlani 2022-12-21 18:50:30 +05:30 committed by GitHub
parent 9c9c33f318
commit a190fae414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 572 additions and 226 deletions

View File

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.1843 2.8125H16.6687V2.10938C16.6687 0.946266 15.7224 0 14.5593 0H8.93433C7.77122 0 6.82495 0.946266 6.82495 2.10938V2.8125H3.30933C2.14622 2.8125 1.19995 3.75877 1.19995 4.92188C1.19995 5.856 1.8105 6.64973 2.65331 6.9263L3.90745 22.0658C3.99778 23.1504 4.92112 24 6.00951 24H17.4841C18.5726 24 19.4959 23.1504 19.5863 22.0654L20.8403 6.92625C21.6832 6.64973 22.2937 5.856 22.2937 4.92188C22.2937 3.75877 21.3474 2.8125 20.1843 2.8125ZM8.2312 2.10938C8.2312 1.72167 8.54662 1.40625 8.93433 1.40625H14.5593C14.947 1.40625 15.2625 1.72167 15.2625 2.10938V2.8125H8.2312V2.10938ZM18.1848 21.949C18.1547 22.3105 17.847 22.5938 17.4841 22.5938H6.00951C5.64675 22.5938 5.33897 22.3105 5.30892 21.9494L4.07311 7.03125H19.4205L18.1848 21.949ZM20.1843 5.625H3.30933C2.92162 5.625 2.6062 5.30958 2.6062 4.92188C2.6062 4.53417 2.92162 4.21875 3.30933 4.21875H20.1843C20.572 4.21875 20.8875 4.53417 20.8875 4.92188C20.8875 5.30958 20.572 5.625 20.1843 5.625Z" fill="#37352F"/>
<path d="M9.18619 20.4409L8.48307 9.09715C8.45902 8.70954 8.12354 8.41474 7.7378 8.43888C7.3502 8.46293 7.05549 8.79659 7.07949 9.18415L7.78262 20.5279C7.80573 20.9007 8.11529 21.1876 8.48373 21.1876C8.89093 21.1876 9.21118 20.8446 9.18619 20.4409Z" fill="#37352F"/>
<path d="M12 8.4375C11.6117 8.4375 11.2969 8.75231 11.2969 9.14062V20.4844C11.2969 20.8727 11.6117 21.1875 12 21.1875C12.3883 21.1875 12.7031 20.8727 12.7031 20.4844V9.14062C12.7031 8.75231 12.3883 8.4375 12 8.4375Z" fill="#37352F"/>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<path d="M13.0465 6.70756C12.7823 6.70756 12.568 6.92179 12.568 7.18605V13.8372C12.568 14.2402 12.2402 14.568 11.8372 14.568H2.16279C1.75984 14.568 1.43198 14.2402 1.43198 13.8372V4.16279C1.43198 3.75984 1.75984 3.43198 2.16279 3.43198H8.81395C9.07821 3.43198 9.29244 3.21774 9.29244 2.95349C9.29244 2.68923 9.07821 2.475 8.81395 2.475H2.16279C1.23212 2.475 0.475 3.23212 0.475 4.16279V13.8372C0.475 14.7679 1.23212 15.525 2.16279 15.525H11.8372C12.7679 15.525 13.525 14.7679 13.525 13.8372V7.18605C13.525 6.92179 13.3108 6.70756 13.0465 6.70756Z" fill="#37352F" stroke="#37352F" stroke-width="0.05"/>
<path d="M15.1814 1.70377L14.2975 0.819838C13.8711 0.393387 13.1772 0.393387 12.7507 0.819838L5.67941 7.89112C5.61397 7.95656 5.56937 8.03991 5.55118 8.13066L5.1092 10.3404C5.07848 10.4941 5.12658 10.653 5.23742 10.7638C5.32621 10.8526 5.4458 10.9012 5.56887 10.9012C5.59947 10.9012 5.63022 10.8982 5.66078 10.8921L7.87057 10.4501C7.96132 10.4319 8.04467 10.3873 8.11011 10.3219L15.1814 3.2506C15.1814 3.2506 15.1815 3.2506 15.1815 3.25057C15.6079 2.82415 15.6079 2.13025 15.1814 1.70377ZM7.54756 9.55858L6.16645 9.83484L6.44271 8.45373L12.1982 2.69815L13.3031 3.80306L7.54756 9.55858ZM14.5185 2.58767L13.966 3.14013L12.8611 2.03522L13.4136 1.4828C13.4745 1.42186 13.5736 1.42183 13.6346 1.48277L14.5185 2.3667C14.5794 2.42761 14.5794 2.52677 14.5185 2.58767Z" fill="#37352F"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -40,7 +40,20 @@ export const getAlertsFromId = async (
params: {
...params,
include: 'all',
fields: 'alertActions',
},
});
return response.data;
};
export const getAlertsFromName = async (
name: string,
params?: Pick<ListAlertsRequestParams, 'include'>
) => {
const response = await axiosClient.get<Alerts>(`${BASE_URL}/name/${name}`, {
params: {
...params,
include: 'all',
},
});
@ -51,7 +64,6 @@ export const getAllAlerts = async (params: ListAlertsRequestParams) => {
const response = await axiosClient.get<PagingResponse<Alerts[]>>(BASE_URL, {
params: {
...params,
fields: 'alertActions',
},
});

View File

@ -0,0 +1,234 @@
/*
* Copyright 2022 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 { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Card, Col, Divider, Row, Space, Tag, Typography } from 'antd';
import { isArray } from 'lodash';
import React, { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg';
import { ReactComponent as IconEdit } from '../../../assets/svg/ic-edit.svg';
import {
AlertAction,
AlertActionType,
} from '../../../generated/alerts/alertAction';
import {
Alerts,
AlertTriggerType,
Effect,
} from '../../../generated/alerts/alerts';
import {
EDIT_LINK_PATH,
getAlertActionTypeDisplayName,
getAlertsActionTypeIcon,
getDisplayNameForEntities,
getDisplayNameForTriggerType,
getFunctionDisplayName,
} from '../../../utils/Alerts/AlertsUtil';
import { getHostNameFromURL } from '../../../utils/CommonUtils';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../../common/title-breadcrumb/title-breadcrumb.interface';
interface AlertDetailsComponentProps {
alerts: Alerts;
alertActions: AlertAction[];
onDelete: () => void;
breadcrumb: TitleBreadcrumbProps['titleLinks'];
allowDelete?: boolean;
allowEdit?: boolean;
}
export const AlertDetailsComponent = ({
alerts,
alertActions,
onDelete,
breadcrumb,
allowDelete = true,
allowEdit = true,
}: AlertDetailsComponentProps) => {
const { t } = useTranslation();
return (
<Row align="middle" gutter={[16, 16]}>
<Col span={24}>
<div className="d-flex items-center justify-between">
<TitleBreadcrumb titleLinks={breadcrumb} />
<Space size={16}>
{allowEdit && (
<Link to={`${EDIT_LINK_PATH}/${alerts?.id}`}>
<Button icon={<Icon component={IconEdit} size={12} />}>
{t('label.edit')}
</Button>
</Link>
)}
{allowDelete && (
<Button
icon={<Icon component={IconDelete} size={12} />}
onClick={onDelete}>
{t('label.delete')}
</Button>
)}
</Space>
</div>
</Col>
<Col span={24}>
<Card>
{alerts.description && (
<>
<RichTextEditorPreviewer markdown={alerts?.description ?? ''} />
<Divider />
</>
)}
<Space direction="vertical" size={8}>
<Typography.Title className="m-0" level={5}>
{t('label.trigger')}
</Typography.Title>
<Typography.Text type="secondary">
{getDisplayNameForTriggerType(
alerts?.triggerConfig.type ?? AlertTriggerType.AllDataAssets
)}
:
</Typography.Text>
<Typography.Text>
{alerts?.triggerConfig.entities
?.map(getDisplayNameForEntities)
?.join(', ')}
</Typography.Text>
</Space>
<Divider />
<Typography.Title level={5}>{t('label.filter')}</Typography.Title>
<Typography.Paragraph>
{alerts?.filteringRules?.map((filter) => {
const conditions = isArray(filter.condition)
? filter.condition.join(', ')
: filter.condition;
const effect = filter.effect === Effect.Include ? '===' : '!==';
const conditionName = getFunctionDisplayName(
filter.fullyQualifiedName ?? ''
);
return (
<Fragment key={filter.name}>
<Typography.Text code>
{`${conditionName} ${effect} ${conditions}`}
</Typography.Text>
<br />
</Fragment>
);
})}
</Typography.Paragraph>
<Divider />
<Typography.Title level={5}>
{t('label.destination')}
</Typography.Title>
<Row gutter={[16, 16]}>
{alertActions.map((action) => (
<Col key={action.name} span={8}>
{action.alertActionType === AlertActionType.ActivityFeed ? (
<Space size={16}>
{getAlertsActionTypeIcon(action.alertActionType)}
{getAlertActionTypeDisplayName(
action.alertActionType ?? AlertActionType.GenericWebhook
)}
</Space>
) : (
<Card className="h-full" title={<Space size={8} />}>
<Space direction="vertical" size={8}>
{action.alertActionType === AlertActionType.Email && (
<>
<Typography.Text>
{t('label.send-to')}:{' '}
<div>
{action.alertActionConfig?.receivers?.map(
(rec) => (
<Tag key={rec}>{rec}</Tag>
)
)}
</div>
</Typography.Text>
<Typography.Text>
<Space size={16}>
<span>
{action.alertActionConfig.sendToAdmins ? (
<CheckCircleOutlined />
) : (
<CloseCircleOutlined />
)}{' '}
{t('label.admin-plural')}
</span>
<span>
{action.alertActionConfig.sendToOwners ? (
<CheckCircleOutlined />
) : (
<CloseCircleOutlined />
)}{' '}
{t('label.owner-plural')}
</span>
<span>
{action.alertActionConfig.sendToFollowers ? (
<CheckCircleOutlined />
) : (
<CloseCircleOutlined />
)}{' '}
{t('label.follower-plural')}
</span>
</Space>
</Typography.Text>
</>
)}
{action.alertActionType !== AlertActionType.Email && (
<>
<Typography.Text>
<Typography.Text type="secondary">
{t('label.webhook')}:{' '}
</Typography.Text>
{getHostNameFromURL(
action.alertActionConfig?.endpoint ?? '-'
)}
</Typography.Text>
<Typography.Text>
<Typography.Text type="secondary">
{t('label.batch-size')}:{' '}
</Typography.Text>
{action.batchSize}
</Typography.Text>
<Typography.Text>
<Typography.Text type="secondary">
{t('message.field-timeout-description')}:{' '}
</Typography.Text>
{action.timeout}
</Typography.Text>
<Typography.Text>
<Typography.Text type="secondary">
{t('label.secret-key')}:{' '}
</Typography.Text>
{action.alertActionConfig?.secretKey ? '****' : '-'}
</Typography.Text>
</>
)}
</Space>
</Card>
)}
</Col>
))}
</Row>
</Card>
</Col>
</Row>
);
};

View File

@ -15,7 +15,7 @@ import { ResourceEntity } from '../components/PermissionProvider/PermissionProvi
export enum GlobalSettingsMenuCategory {
ACCESS = 'access',
COLLABORATION = 'collaboration',
NOTIFICATIONS = 'notifications',
CUSTOM_ATTRIBUTES = 'customAttributes',
DATA_QUALITY = 'dataQuality',
EVENT_PUBLISHERS = 'eventPublishers',
@ -41,11 +41,12 @@ export enum GlobalSettingOptions {
BOTS = 'bots',
TABLES = 'tables',
MSTEAMS = 'msteams',
ACTIVITY_FEED = 'activityFeed',
ACTIVITY_FEED = 'activityFeeds',
ELASTIC_SEARCH = 'elasticSearch',
ALERTS = 'alerts',
ALERT = 'alert',
ADD_ALERTS = 'add-alerts',
EDIT_ALERTS = 'edit',
EDIT_ALERTS = 'edit-alert',
}
export const GLOBAL_SETTING_PERMISSION_RESOURCES = [

View File

@ -600,7 +600,8 @@
"follower-plural": "Followers",
"email-plural": "Emails",
"no-tier": "No Tier",
"dbt-run-result-http-path": "dbt Run Results HTTP Path"
"dbt-run-result-http-path": "dbt Run Results HTTP Path",
"field-change": "Field Change"
},
"message": {
"service-email-required": "Service account Email is required",

View File

@ -64,15 +64,13 @@ import {
ProviderType,
TriggerConfig,
} from '../../generated/alerts/alerts';
import {
AlertActionType,
CreateAlertAction,
} from '../../generated/alerts/api/createAlertAction';
import { AlertActionType } from '../../generated/alerts/api/createAlertAction';
import { EntitySpelFilters } from '../../generated/alerts/entitySpelFilters';
import { Function } from '../../generated/type/function';
import {
getAlertActionTypeDisplayName,
getAlertsActionTypeIcon,
getDisplayNameForEntities,
getDisplayNameForTriggerType,
getFunctionDisplayName,
listLengthValidator,
@ -80,7 +78,7 @@ import {
} from '../../utils/Alerts/AlertsUtil';
import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import './add-alerts-page.styles.less';
const AddAlertPage = () => {
@ -177,7 +175,6 @@ const AddAlertPage = () => {
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
@ -192,18 +189,19 @@ const AddAlertPage = () => {
// Else Create AlertActions and return new IDs
const promises =
alertActions?.map((action) =>
api(
pick(action, [
alertActions?.map((action) => {
const api = action.id ? updateAlertAction : createAlertAction;
const alertAction = pick(action, [
'alertActionConfig',
'alertActionType',
'name',
'displayName',
'timeout',
'batchSize',
]) as CreateAlertAction
)
) ?? [];
]) as AlertAction;
return api(alertAction);
}) ?? [];
const responses = await Promise.allSettled(promises);
@ -237,18 +235,9 @@ const AddAlertPage = () => {
)?.join(', ')})`,
}));
const modifiedAlertActions = alertActions?.map(
(action) =>
({
...action,
name: action.name ?? action.displayName,
displayName: action.displayName,
} as unknown as AlertAction)
);
try {
const requestAlertActions = await updateCreateAlertActions(
modifiedAlertActions
alertActions as unknown as AlertAction[]
);
try {
@ -258,14 +247,14 @@ const AddAlertPage = () => {
alertActions: requestAlertActions,
});
showErrorToast(
showSuccessToast(
t(`server.${isEditMode ? 'update' : 'create'}-entity-success`, {
entity: t('label.alert-plural'),
})
);
history.push(
getSettingPath(
GlobalSettingsMenuCategory.COLLABORATION,
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ALERTS
)
);
@ -445,17 +434,7 @@ const AddAlertPage = () => {
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')}
label={t('label.send-to')}
labelCol={{ span: 24 }}
name={[name, 'alertActionConfig', 'receivers']}>
<Select
@ -492,12 +471,6 @@ const AddAlertPage = () => {
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']}>
@ -621,7 +594,7 @@ const AddAlertPage = () => {
options={
selectedTrigger.entities?.map((entity) => ({
value: entity,
label: startCase(entity),
label: getDisplayNameForEntities(entity),
})) ?? []
}
placeholder={t('label.select-data-assets')}
@ -685,7 +658,7 @@ const AddAlertPage = () => {
)}
<Form.Item
initialValue={Effect.Allow}
initialValue={Effect.Include}
key={key}
name={[name, 'effect']}>
<Select

View File

@ -0,0 +1,128 @@
/*
* Copyright 2022 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 { Card } from 'antd';
import { trim } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import {
getAlertActionForAlerts,
getAlertsFromId,
} from '../../axiosAPIs/alertsAPI';
import { AlertDetailsComponent } from '../../components/Alerts/AlertsDetails/AlertDetails.component';
import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../constants/GlobalSettings.constants';
import { EntityType } from '../../enums/entity.enum';
import { AlertAction } from '../../generated/alerts/alertAction';
import { AlertFilterRule, Alerts } from '../../generated/alerts/alerts';
import { getEntityName } from '../../utils/CommonUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const AlertDetailsPage = () => {
const { t } = useTranslation();
const { fqn: id } = useParams<{ fqn: string }>();
const [loadingCount, setLoadingCount] = useState(0);
const [alerts, setAlerts] = useState<Alerts>();
const [alertActions, setAlertActions] = useState<AlertAction[]>([]);
const [showDeleteModel, setShowDeleteModel] = useState(false);
const fetchAlert = async () => {
try {
setLoadingCount((count) => count + 1);
const response: Alerts = await getAlertsFromId(id);
const alertActions = await getAlertActionForAlerts(response.id);
const requestFilteringRules =
response.filteringRules?.map((curr) => {
const [fullyQualifiedName, filterRule] =
curr.condition?.split('(') ?? [];
return {
...curr,
fullyQualifiedName,
condition: filterRule
.replaceAll("'", '')
.replace(new RegExp(`\\)`), '')
.split(',')
.map(trim),
} as unknown as AlertFilterRule;
}) ?? [];
setAlerts({ ...response, filteringRules: requestFilteringRules });
setAlertActions(alertActions);
} catch {
showErrorToast(
t('server.entity-fetch-error', { entity: t('label.alert') }),
id
);
} finally {
setLoadingCount((count) => count - 1);
}
};
useEffect(() => {
if (id) {
fetchAlert();
}
}, [id]);
const breadcrumb = useMemo(
() => [
{
name: t('label.alert-plural'),
url: getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ALERTS
),
},
{
name: getEntityName(alerts),
url: '',
},
],
[alerts]
);
return (
<>
{loadingCount > 0 && <Card loading={loadingCount > 0} />}
{alerts && (
<AlertDetailsComponent
alertActions={alertActions}
alerts={alerts}
breadcrumb={breadcrumb}
onDelete={() => setShowDeleteModel(true)}
/>
)}
<DeleteWidgetModal
afterDeleteAction={() => history.back()}
allowSoftDelete={false}
entityId={alerts?.id || ''}
entityName={alerts?.name || ''}
entityType={EntityType.ALERT}
visible={showDeleteModel}
onCancel={() => {
setShowDeleteModel(false);
}}
/>
</>
);
};
export default AlertDetailsPage;

View File

@ -0,0 +1,85 @@
import { Card } from 'antd';
import { noop, trim } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
getAlertActionForAlerts,
getAlertsFromName,
} from '../../axiosAPIs/alertsAPI';
import { AlertDetailsComponent } from '../../components/Alerts/AlertsDetails/AlertDetails.component';
import Loader from '../../components/Loader/Loader';
import { AlertAction } from '../../generated/alerts/alertAction';
import { AlertFilterRule, Alerts } from '../../generated/alerts/alerts';
import { getEntityName } from '../../utils/CommonUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const AlertsActivityFeedPage = () => {
const [loading, setLoading] = useState(false);
const [alert, setAlert] = useState<Alerts>();
const [alertActions, setAlertActions] = useState<AlertAction[]>();
const { t } = useTranslation();
const fetchActivityFeedAlert = useCallback(async () => {
try {
setLoading(true);
const response = await getAlertsFromName('ActivityFeedAlert');
const requestFilteringRules =
response.filteringRules?.map((curr) => {
const [fullyQualifiedName, filterRule] = curr.condition.split('(');
return {
...curr,
fullyQualifiedName,
condition: filterRule
.replaceAll("'", '')
.replace(new RegExp(`\\)`), '')
.split(',')
.map(trim),
} as unknown as AlertFilterRule;
}) ?? [];
const alertActions = await getAlertActionForAlerts(response.id);
setAlertActions(alertActions);
setAlert({ ...response, filteringRules: requestFilteringRules });
} catch (error) {
showErrorToast(
t('server.entity-fetch-error', { entity: t('label.activity-feeds') })
);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchActivityFeedAlert();
}, []);
const breadcrumb = useMemo(
() => [
{
name: getEntityName(alert),
url: '',
},
],
[alert]
);
if (loading) {
return <Card loading={loading} />;
}
return alert && alertActions ? (
<AlertDetailsComponent
alertActions={alertActions}
alerts={alert}
allowDelete={false}
breadcrumb={breadcrumb}
onDelete={noop}
/>
) : (
<Loader />
);
};
export default AlertsActivityFeedPage;

View File

@ -1,99 +0,0 @@
/*
* 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>
);
};

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Row, Table, Tag, Tooltip, Typography } from 'antd';
import { Button, Col, Row, Table, Tooltip, Typography } from 'antd';
import { isNil } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -25,15 +25,12 @@ import {
GlobalSettingsMenuCategory,
} from '../../constants/GlobalSettings.constants';
import { EntityType } from '../../enums/entity.enum';
import { AlertAction } from '../../generated/alerts/alertAction';
import { Alerts, ProviderType } from '../../generated/alerts/alerts';
import { Paging } from '../../generated/type/paging';
import { getDisplayNameForTriggerType } from '../../utils/Alerts/AlertsUtil';
import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { getTableExpandableConfig } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { AlertsExpanded } from './AlertRowExpanded';
const AlertsPage = () => {
const [loading, setLoading] = useState(true);
@ -50,7 +47,7 @@ const AlertsPage = () => {
try {
const { data, paging } = await getAllAlerts({ after });
setAlerts(data);
setAlerts(data.filter((d) => d.provider !== ProviderType.System));
setAlertsPaging(paging);
} catch (error) {
showErrorToast(
@ -83,6 +80,9 @@ const AlertsPage = () => {
dataIndex: 'name',
width: '200px',
key: 'name',
render: (name: string, record: Alerts) => {
return <Link to={`alert/${record.id}`}>{name}</Link>;
},
},
{
title: t('label.trigger'),
@ -91,14 +91,6 @@ const AlertsPage = () => {
key: 'triggerConfig.type',
render: getDisplayNameForTriggerType,
},
{
title: t('label.destination'),
dataIndex: 'alertActions',
width: '200px',
key: 'alertActions',
render: (actions: AlertAction[]) =>
actions.map((action) => <Tag key={action.name}>{action.name}</Tag>),
},
{
title: t('label.description'),
dataIndex: 'description',
@ -114,7 +106,7 @@ const AlertsPage = () => {
return (
<>
<Tooltip placement="bottom" title={t('label.edit')}>
<Link to={`edit/${id}`}>
<Link to={`edit-alert/${id}`}>
<Button
data-testid={`alert-edit-${record.name}`}
icon={<SVGIcons className="tw-w-4" icon={Icons.EDIT} />}
@ -154,7 +146,7 @@ const AlertsPage = () => {
</div>
<Link
to={getSettingPath(
GlobalSettingsMenuCategory.COLLABORATION,
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ADD_ALERTS
)}>
<Button type="primary">
@ -168,10 +160,6 @@ const AlertsPage = () => {
bordered
columns={columns}
dataSource={alerts}
expandable={{
...getTableExpandableConfig<Alerts>(),
expandedRowRender: (record) => <AlertsExpanded alert={record} />,
}}
loading={{ spinning: loading, indicator: <Loader /> }}
pagination={false}
rowKey="id"

View File

@ -11,15 +11,11 @@
* limitations under the License.
*/
import { AxiosError } from 'axios';
import { SlackChatConfig } from 'Models';
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { useAnalytics } from 'use-analytics';
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
import { fetchSlackConfig } from '../axiosAPIs/miscAPI';
import Loader from '../components/Loader/Loader';
import SlackChat from '../components/SlackChat/SlackChat';
import { ROUTES } from '../constants/constants';
import { AuthTypes } from '../enums/signin.enum';
import AccountActivationConfirmation from '../pages/signup/account-activation-confirmation.component';
@ -62,7 +58,6 @@ const AppRouter = () => {
getCallBackComponent,
} = useAuthContext();
const [slackConfig, setSlackConfig] = useState<SlackChatConfig | undefined>();
const callbackComponent = getCallBackComponent();
const oidcProviders = [
AuthTypes.GOOGLE,
@ -77,35 +72,6 @@ const AppRouter = () => {
(authConfig.provider === AuthTypes.BASIC ||
authConfig.provider === AuthTypes.LDAP);
const fetchSlackChatConfig = () => {
fetchSlackConfig()
.then((res) => {
if (res.data) {
const { slackUrl } = res.data;
const slackConfig = {
slackUrl,
};
setSlackConfig(slackConfig);
} else {
throw '';
}
})
.catch((err: AxiosError) => {
// eslint-disable-next-line no-console
console.error(err);
setSlackConfig(undefined);
});
};
useEffect(() => {
fetchSlackChatConfig();
}, []);
const slackChat =
slackConfig && slackConfig.slackUrl ? (
<SlackChat slackConfig={slackConfig} />
) : null;
useEffect(() => {
const { pathname } = location;
@ -128,17 +94,11 @@ const AppRouter = () => {
}
if (isOidcProvider || isAuthenticated) {
return (
<>
<AuthenticatedAppRouter />
{slackChat}
</>
);
return <AuthenticatedAppRouter />;
}
return (
<>
{slackChat}
<Switch>
<Route exact component={SigninPage} path={ROUTES.SIGNIN} />
{callbackComponent ? (

View File

@ -20,9 +20,6 @@ import {
GlobalSettingsMenuCategory,
} from '../constants/GlobalSettings.constants';
import { TeamType } from '../generated/entity/teams/team';
import AddAlertPage from '../pages/AddAlertPage/AddAlertPage';
import AlertsPage from '../pages/AlertsPage/AlertsPage';
import TeamsPage from '../pages/teams/TeamsPage';
import { userPermissions } from '../utils/PermissionsUtils';
import {
getSettingCategoryPath,
@ -32,6 +29,28 @@ import {
import AdminProtectedRoute from './AdminProtectedRoute';
import withSuspenseFallback from './withSuspenseFallback';
const AddAlertPage = withSuspenseFallback(
React.lazy(() => import('../pages/AddAlertPage/AddAlertPage'))
);
const AlertDetailsPage = withSuspenseFallback(
React.lazy(() => import('../pages/AlertDetailsPage/AlertDetailsPage'))
);
const AlertsActivityFeedPage = withSuspenseFallback(
React.lazy(
() => import('../pages/AlertsActivityFeedPage/AlertsActivityFeedPage')
)
);
const AlertsPage = withSuspenseFallback(
React.lazy(() => import('../pages/AlertsPage/AlertsPage'))
);
const TeamsPage = withSuspenseFallback(
React.lazy(() => import('../pages/teams/TeamsPage'))
);
const ServicesPage = withSuspenseFallback(
React.lazy(() => import('../pages/services/ServicesPage'))
);
@ -192,7 +211,7 @@ const GlobalSettingRouter = () => {
component={AlertsPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.COLLABORATION,
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ALERTS
)}
/>
@ -202,7 +221,7 @@ const GlobalSettingRouter = () => {
component={AddAlertPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.COLLABORATION,
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.EDIT_ALERTS,
true
)}
@ -212,11 +231,32 @@ const GlobalSettingRouter = () => {
component={AddAlertPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.COLLABORATION,
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ADD_ALERTS
)}
/>
<AdminProtectedRoute
exact
component={AlertDetailsPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ALERT,
true
)}
/>
<AdminProtectedRoute
exact
component={AlertsActivityFeedPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ACTIVITY_FEED
)}
/>
<AdminProtectedRoute
exact
component={CustomPropertiesPageV1}

View File

@ -14,6 +14,7 @@
import { Typography } from 'antd';
import { RuleObject } from 'antd/lib/form';
import i18next from 'i18next';
import { startCase } from 'lodash';
import React from 'react';
import { ReactComponent as AllActivityIcon } from '../../assets/svg/all-activity.svg';
import { ReactComponent as MailIcon } from '../../assets/svg/ic-mail.svg';
@ -51,6 +52,8 @@ export const getFunctionDisplayName = (func: string): string => {
return i18next.t('label.test-results');
case 'matchUpdatedBy':
return i18next.t('label.updated-by');
case 'matchAnyFieldChange':
return i18next.t('label.field-change');
case 'matchAnySource':
case 'matchAnyEntityId':
default:
@ -113,7 +116,7 @@ export const getAlertActionTypeDisplayName = (
) => {
switch (alertActionType) {
case AlertActionType.ActivityFeed:
return i18next.t('label.activity-feed');
return i18next.t('label.activity-feeds');
case AlertActionType.Email:
return i18next.t('label.email');
case AlertActionType.GenericWebhook:
@ -124,3 +127,16 @@ export const getAlertActionTypeDisplayName = (
return i18next.t('label.ms-teams');
}
};
export const getDisplayNameForEntities = (entity: string) => {
switch (entity) {
case 'kpi':
return i18next.t('label.kpi-uppercase');
case 'mlmodel':
return i18next.t('label.ml-model');
default:
return startCase(entity);
}
};
export const EDIT_LINK_PATH = `/settings/notifications/edit-alert`;

View File

@ -12,9 +12,11 @@
*/
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import i18next from 'i18next';
import { camelCase } from 'lodash';
import React, { ReactNode } from 'react';
import { ReactComponent as AdminIcon } from '../../src/assets/svg/admin.svg';
import { ReactComponent as AllActivityIcon } from '../../src/assets/svg/all-activity.svg';
import { ReactComponent as BotIcon } from '../../src/assets/svg/bot-profile.svg';
import { ReactComponent as DashboardIcon } from '../../src/assets/svg/dashboard-grey.svg';
import { ReactComponent as ElasticSearchIcon } from '../../src/assets/svg/elasticsearch.svg';
@ -154,10 +156,15 @@ export const getGlobalSettingsMenuWithPermission = (
],
},
{
category: 'Collaboration',
category: i18next.t('label.notification-plural'),
items: [
{
label: 'Alerts',
label: i18next.t('label.activity-feeds'),
isProtected: Boolean(isAdminUser),
icon: <AllActivityIcon className="side-panel-icons" />,
},
{
label: i18next.t('label.alert-plural'),
isProtected: Boolean(isAdminUser),
icon: <BellIcon className="side-panel-icons" />,
},