mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-08 05:26:19 +00:00
* Centralize EventType * Format better for readability * Add Handling for Thread as Change Event * Centralize logic for Entity Message Creation * Add Thread Parent Message in Conversation * Add Task and Announcement * Support for multiple destinations * Delete existing alerts for schema changes * Delete all tables data for event subscription * Centralize logic for Notifications and Observability * Change Field Name and Remove redundant code * Test Fixes * Add more fields * Add Optional * Logical Test Case Addition Chnage Event * Add Filter By Owner * Fix Email Entity Url * added data observability settings page * localization changes for other languages * Handler closure and cleanup * complete create observibility flow * fix searchIndex method error --------- Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com> Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
parent
72fc0cf685
commit
f3f73a3f01
@ -11,9 +11,8 @@
|
||||
* 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 { Button, Card, Col, Divider, Row, Space, Typography } from 'antd';
|
||||
import { isArray } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -23,16 +22,12 @@ import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg'
|
||||
import {
|
||||
Effect,
|
||||
EventSubscription,
|
||||
SubscriptionType,
|
||||
} from '../../../generated/events/eventSubscription';
|
||||
import {
|
||||
EDIT_LINK_PATH,
|
||||
getAlertActionTypeDisplayName,
|
||||
getAlertsActionTypeIcon,
|
||||
getDisplayNameForEntities,
|
||||
getFunctionDisplayName,
|
||||
} from '../../../utils/Alerts/AlertsUtil';
|
||||
import { getHostNameFromURL } from '../../../utils/CommonUtils';
|
||||
import TitleBreadcrumb from '../../common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
||||
import PageHeader from '../../PageHeader/PageHeader.component';
|
||||
@ -122,108 +117,7 @@ export const AlertDetailsComponent = ({
|
||||
<Typography.Title level={5}>
|
||||
{t('label.destination')}
|
||||
</Typography.Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col key={alerts.name} span={8}>
|
||||
{alerts.subscriptionType === SubscriptionType.ActivityFeed ? (
|
||||
<Space size={16}>
|
||||
{getAlertsActionTypeIcon(alerts.subscriptionType)}
|
||||
|
||||
{getAlertActionTypeDisplayName(
|
||||
alerts.subscriptionType ?? SubscriptionType.GenericWebhook
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Card
|
||||
className="h-full"
|
||||
title={
|
||||
<Space size={16}>
|
||||
{getAlertsActionTypeIcon(alerts.subscriptionType)}
|
||||
|
||||
{getAlertActionTypeDisplayName(
|
||||
alerts.subscriptionType ??
|
||||
SubscriptionType.GenericWebhook
|
||||
)}
|
||||
</Space>
|
||||
}>
|
||||
<Space direction="vertical" size={8}>
|
||||
{alerts.subscriptionType === SubscriptionType.Email && (
|
||||
<>
|
||||
<Typography.Text>
|
||||
{t('label.send-to')}:{' '}
|
||||
<div>
|
||||
{alerts.subscriptionConfig?.receivers?.map(
|
||||
(rec) => (
|
||||
<Tag key={rec}>{rec}</Tag>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Space size={16}>
|
||||
<span>
|
||||
{alerts?.subscriptionConfig?.sendToAdmins ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)}{' '}
|
||||
{t('label.admin-plural')}
|
||||
</span>
|
||||
<span>
|
||||
{alerts?.subscriptionConfig?.sendToOwners ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)}{' '}
|
||||
{t('label.owner-plural')}
|
||||
</span>
|
||||
<span>
|
||||
{alerts?.subscriptionConfig?.sendToFollowers ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)}{' '}
|
||||
{t('label.follower-plural')}
|
||||
</span>
|
||||
</Space>
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
{alerts.subscriptionType !== SubscriptionType.Email && (
|
||||
<>
|
||||
<Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t('label.webhook')}:{' '}
|
||||
</Typography.Text>
|
||||
{getHostNameFromURL(
|
||||
alerts.subscriptionConfig?.endpoint ?? '-'
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t('label.batch-size')}:{' '}
|
||||
</Typography.Text>
|
||||
{alerts.batchSize}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t('message.field-timeout-description')}:{' '}
|
||||
</Typography.Text>
|
||||
{alerts.timeout}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t('label.secret-key')}:{' '}
|
||||
</Typography.Text>
|
||||
|
||||
{alerts.subscriptionConfig?.secretKey ? '****' : '-'}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@ -36,6 +36,12 @@ const AddAlertPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../../pages/AddAlertPage/AddAlertPage'))
|
||||
);
|
||||
|
||||
const AddObservabilityPage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() => import('../../pages/AddObservabilityPage/AddObservabilityPage')
|
||||
)
|
||||
);
|
||||
|
||||
const ImportTeamsPage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() => import('../../pages/TeamsPage/ImportTeamsPage/ImportTeamsPage')
|
||||
@ -51,19 +57,15 @@ const AlertsActivityFeedPage = withSuspenseFallback(
|
||||
() => import('../../pages/AlertsActivityFeedPage/AlertsActivityFeedPage')
|
||||
)
|
||||
);
|
||||
const AlertDataInsightReportPage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() =>
|
||||
import(
|
||||
'../../pages/AlertDataInsightReportPage/AlertDataInsightReportPage'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const AlertsPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../../pages/AlertsPage/AlertsPage'))
|
||||
);
|
||||
|
||||
const ObservabilityPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../../pages/ObservabilityPage/ObservabilityPage'))
|
||||
);
|
||||
|
||||
const TeamsPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../../pages/TeamsPage/TeamsPage'))
|
||||
);
|
||||
@ -315,6 +317,16 @@ const GlobalSettingRouter = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={ObservabilityPage}
|
||||
hasPermission={false}
|
||||
path={getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.OBSERVABILITY
|
||||
)}
|
||||
/>
|
||||
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={AddAlertPage}
|
||||
@ -334,6 +346,15 @@ const GlobalSettingRouter = () => {
|
||||
GlobalSettingOptions.ADD_ALERTS
|
||||
)}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={AddObservabilityPage}
|
||||
hasPermission={false}
|
||||
path={getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.ADD_OBSERVABILITY
|
||||
)}
|
||||
/>
|
||||
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
@ -356,16 +377,6 @@ const GlobalSettingRouter = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={AlertDataInsightReportPage}
|
||||
hasPermission={false}
|
||||
path={getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.DATA_INSIGHT_REPORT_ALERT
|
||||
)}
|
||||
/>
|
||||
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={CustomPropertiesPageV1}
|
||||
|
||||
@ -50,9 +50,11 @@ export enum GlobalSettingOptions {
|
||||
EMAIL = 'email',
|
||||
ALERTS = 'alerts',
|
||||
ALERT = 'alert',
|
||||
OBSERVABILITY = 'observability',
|
||||
GLOSSARY_TERM = 'glossaryTerm',
|
||||
ADD_ALERTS = 'add-alerts',
|
||||
EDIT_ALERTS = 'edit-alert',
|
||||
ADD_OBSERVABILITY = 'add-observability',
|
||||
STORAGES = 'storages',
|
||||
DATA_INSIGHT_REPORT_ALERT = 'dataInsightReport',
|
||||
ADD_DATA_INSIGHT_REPORT_ALERT = 'add-data-insight-report',
|
||||
|
||||
@ -26,6 +26,7 @@ export enum SearchIndex {
|
||||
CONTAINER = 'container_search_index',
|
||||
QUERY = 'query_search_index',
|
||||
TEST_CASE = 'test_case_search_index',
|
||||
TEST_SUITE = 'test_suite_search_index',
|
||||
DATABASE_SCHEMA = 'database_schema_search_index',
|
||||
DATABASE = 'database_search_index',
|
||||
DATABASE_SERVICE = 'database_service_search_index',
|
||||
|
||||
@ -120,6 +120,7 @@ export interface TestCaseSearchSource
|
||||
Exclude<TestCase, 'testSuite'> {
|
||||
testSuites: TestSuite[];
|
||||
} // extends EntityInterface
|
||||
export interface TestSuiteSearchSource extends SearchSourceBase, TestSuite {}
|
||||
|
||||
export interface DatabaseServiceSearchSource
|
||||
extends SearchSourceBase,
|
||||
@ -201,6 +202,7 @@ export type SearchIndexSearchSourceMapping = {
|
||||
[SearchIndex.STORED_PROCEDURE]: StoredProcedureSearchSource;
|
||||
[SearchIndex.DASHBOARD_DATA_MODEL]: DashboardDataModelSearchSource;
|
||||
[SearchIndex.DATA_PRODUCT]: DataProductSearchSource;
|
||||
[SearchIndex.TEST_SUITE]: TestSuiteSearchSource;
|
||||
};
|
||||
|
||||
export type SearchRequest<
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Konto-E-Mail",
|
||||
"account-name": "Kontoname",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Aktionen",
|
||||
"active": "Aktiv",
|
||||
"active-user": "Aktiver Benutzer",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Anzahl der Wiederholungen",
|
||||
"number-of-rows": "Anzahl der Zeilen",
|
||||
"object-plural": "Objekte",
|
||||
"observability": "Observability",
|
||||
"october": "Oktober",
|
||||
"of-lowercase": "von",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Account email",
|
||||
"account-name": "Account Name",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Actions",
|
||||
"active": "Active",
|
||||
"active-user": "Active User",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Number of Retries",
|
||||
"number-of-rows": "Number of rows",
|
||||
"object-plural": "Objects",
|
||||
"observability": "Observability",
|
||||
"october": "October",
|
||||
"of-lowercase": "of",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Correo electrónico de la cuenta",
|
||||
"account-name": "Account Name",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Acciones",
|
||||
"active": "Activo",
|
||||
"active-user": "Usuario activo",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Number of Retries",
|
||||
"number-of-rows": "Número de filas",
|
||||
"object-plural": "Objects",
|
||||
"observability": "Observability",
|
||||
"october": "Octubre",
|
||||
"of-lowercase": "de",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Compte email",
|
||||
"account-name": "Nom du Compte",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Actions",
|
||||
"active": "Actif",
|
||||
"active-user": "Utilisateur Actif",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Nombre de Tentatives",
|
||||
"number-of-rows": "Nombre de lignes",
|
||||
"object-plural": "Objets",
|
||||
"observability": "Observability",
|
||||
"october": "Octobre",
|
||||
"of-lowercase": "de",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "אימייל חשבון",
|
||||
"account-name": "שם החשבון",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "פעולות",
|
||||
"active": "פעיל",
|
||||
"active-user": "משתמש פעיל",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "מספר ניסיונות מחדש",
|
||||
"number-of-rows": "מספר השורות",
|
||||
"object-plural": "אובייקטים",
|
||||
"observability": "Observability",
|
||||
"october": "אוקטובר",
|
||||
"of-lowercase": "של",
|
||||
"ok": "אישור",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "アカウントのEmail",
|
||||
"account-name": "Account Name",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "アクション",
|
||||
"active": "アクティブ",
|
||||
"active-user": "アクティブなユーザ",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Number of Retries",
|
||||
"number-of-rows": "行数",
|
||||
"object-plural": "Objects",
|
||||
"observability": "Observability",
|
||||
"october": "10月",
|
||||
"of-lowercase": "の",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Account e-mail",
|
||||
"account-name": "Accountnaam",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Acties",
|
||||
"active": "Actief",
|
||||
"active-user": "Actieve gebruiker",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Aantal herhalingen",
|
||||
"number-of-rows": "Aantal rijen",
|
||||
"object-plural": "Objecten",
|
||||
"observability": "Observability",
|
||||
"october": "Oktober",
|
||||
"of-lowercase": "van",
|
||||
"ok": "Oké",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "E-mail da conta",
|
||||
"account-name": "Nome da Conta",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Ações",
|
||||
"active": "Ativo",
|
||||
"active-user": "Usuário Ativo",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Número de Tentativas",
|
||||
"number-of-rows": "Número de linhas",
|
||||
"object-plural": "Objetos",
|
||||
"observability": "Observability",
|
||||
"october": "Outubro",
|
||||
"of-lowercase": "de",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "Адрес электронной почты",
|
||||
"account-name": "Наименование аккаунта",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "Действия",
|
||||
"active": "Активный",
|
||||
"active-user": "Активный пользователь",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "Number of Retries",
|
||||
"number-of-rows": "Количество строк",
|
||||
"object-plural": "Объекты",
|
||||
"observability": "Observability",
|
||||
"october": "Октябрь",
|
||||
"of-lowercase": "из",
|
||||
"ok": "Ок",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"account-email": "帐号邮箱",
|
||||
"account-name": "帐号名称",
|
||||
"acknowledged": "Acknowledged",
|
||||
"action": "Action",
|
||||
"action-plural": "操作",
|
||||
"active": "活跃的",
|
||||
"active-user": "活跃用户",
|
||||
@ -706,6 +707,7 @@
|
||||
"number-of-retries": "重试次数",
|
||||
"number-of-rows": "行数",
|
||||
"object-plural": "对象",
|
||||
"observability": "Observability",
|
||||
"october": "十月",
|
||||
"of-lowercase": "of",
|
||||
"ok": "Ok",
|
||||
|
||||
@ -66,6 +66,7 @@ import {
|
||||
getAlertActionTypeDisplayName,
|
||||
getAlertsActionTypeIcon,
|
||||
getFunctionDisplayName,
|
||||
listLengthValidator,
|
||||
StyledCard,
|
||||
} from '../../utils/Alerts/AlertsUtil';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
@ -196,7 +197,7 @@ const AddAlertPage = () => {
|
||||
...(filteringRules as FilteringRules),
|
||||
rules: requestFilteringRules,
|
||||
},
|
||||
alertType: AlertType.ChangeEvent,
|
||||
alertType: AlertType.Notification,
|
||||
provider,
|
||||
});
|
||||
|
||||
@ -423,10 +424,10 @@ const AddAlertPage = () => {
|
||||
{sendToCommonFields}
|
||||
</>
|
||||
);
|
||||
case SubscriptionType.GenericWebhook:
|
||||
case SubscriptionType.SlackWebhook:
|
||||
case SubscriptionType.MSTeamsWebhook:
|
||||
case SubscriptionType.GChatWebhook:
|
||||
case SubscriptionType.Generic:
|
||||
case SubscriptionType.Slack:
|
||||
case SubscriptionType.MSTeams:
|
||||
case SubscriptionType.GChat:
|
||||
return (
|
||||
<>
|
||||
<Form.Item required name={['subscriptionConfig', 'endpoint']}>
|
||||
@ -481,237 +482,240 @@ const AddAlertPage = () => {
|
||||
}, [subscriptionType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Typography.Title level={5}>
|
||||
{!isEmpty(fqn)
|
||||
? t('label.edit-entity', { entity: t('label.alert-plural') })
|
||||
: t('label.create-entity', { entity: t('label.alert-plural') })}
|
||||
</Typography.Title>
|
||||
<Typography.Text>{t('message.alerts-description')}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form<EventSubscription>
|
||||
className="alerts-notification-form"
|
||||
form={form}
|
||||
onFinish={handleSave}
|
||||
onValuesChange={handleChange}>
|
||||
{loadingCount > 0 ? (
|
||||
<Skeleton title paragraph={{ rows: 8 }} />
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('label.name')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
pattern: ENTITY_NAME_REGEX,
|
||||
message: t('message.entity-name-validation'),
|
||||
},
|
||||
]}>
|
||||
<Input disabled={isEditMode} placeholder={t('label.name')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('label.description')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="description"
|
||||
trigger="onTextChange"
|
||||
valuePropName="initialValue">
|
||||
<RichTextEditor
|
||||
data-testid="description"
|
||||
height="200px"
|
||||
initialValue=""
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.trigger')}
|
||||
subHeading={t('message.alerts-trigger-description')}
|
||||
/>
|
||||
<div>
|
||||
<Form.Item
|
||||
required
|
||||
initialValue={['all']}
|
||||
messageVariables={{
|
||||
fieldName: t('label.data-asset-plural'),
|
||||
}}
|
||||
name={['filteringRules', 'resources']}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="w-full"
|
||||
data-testid="triggerConfig-type"
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.data-asset-plural'),
|
||||
})}
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={resourcesOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.filter-plural')}
|
||||
subHeading={t('message.alerts-filter-description')}
|
||||
/>
|
||||
|
||||
<Form.List name={['filteringRules', 'rules']}>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Button
|
||||
block
|
||||
data-testid="add-filters"
|
||||
icon={<PlusOutlined />}
|
||||
type="default"
|
||||
onClick={() => add({}, 0)}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.filter-plural'),
|
||||
})}
|
||||
</Button>
|
||||
{fields.map(({ key, name }) => (
|
||||
<div key={`filteringRules-${key}`}>
|
||||
{name > 0 && (
|
||||
<Divider
|
||||
style={{
|
||||
margin: 0,
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<div className="flex-1">
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={[name, 'name']}>
|
||||
<Select
|
||||
options={functions}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.condition'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{filters &&
|
||||
filters[name] &&
|
||||
getConditionField(
|
||||
filters[name].name ?? '',
|
||||
name
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
initialValue={Effect.Include}
|
||||
key={key}
|
||||
name={[name, 'effect']}>
|
||||
<Select
|
||||
options={map(
|
||||
Effect,
|
||||
(func: string) => ({
|
||||
label: startCase(func),
|
||||
value: func,
|
||||
})
|
||||
)}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.effect'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
data-testid={`remove-filter-rule-${name}`}
|
||||
icon={
|
||||
<SVGIcons
|
||||
alt={t('label.delete')}
|
||||
className="w-4"
|
||||
icon={Icons.DELETE}
|
||||
/>
|
||||
}
|
||||
type="text"
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<Form.ErrorList errors={errors} />
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.destination')}
|
||||
subHeading={t('message.alerts-destination-description')}
|
||||
/>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Typography.Title level={5}>
|
||||
{!isEmpty(fqn)
|
||||
? t('label.edit-entity', { entity: t('label.alert-plural') })
|
||||
: t('label.create-entity', { entity: t('label.alert-plural') })}
|
||||
</Typography.Title>
|
||||
<Typography.Text>{t('message.alerts-description')}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form<EventSubscription>
|
||||
className="alerts-notification-form"
|
||||
form={form}
|
||||
onFinish={handleSave}
|
||||
onValuesChange={handleChange}>
|
||||
{loadingCount > 0 ? (
|
||||
<Skeleton title paragraph={{ rows: 8 }} />
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('label.name')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
pattern: ENTITY_NAME_REGEX,
|
||||
message: t('message.entity-name-validation'),
|
||||
},
|
||||
]}>
|
||||
<Input disabled={isEditMode} placeholder={t('label.name')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('label.description')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="description"
|
||||
trigger="onTextChange"
|
||||
valuePropName="initialValue">
|
||||
<RichTextEditor
|
||||
data-testid="description"
|
||||
height="200px"
|
||||
initialValue=""
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.trigger')}
|
||||
subHeading={t('message.alerts-trigger-description')}
|
||||
/>
|
||||
<div>
|
||||
<Form.Item
|
||||
required
|
||||
name="subscriptionType"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.destination'),
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
data-testid="alert-action-type"
|
||||
disabled={provider === ProviderType.System}
|
||||
initialValue={['all']}
|
||||
messageVariables={{
|
||||
fieldName: t('label.data-asset-plural'),
|
||||
}}
|
||||
name={['filteringRules', 'resources']}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="w-full"
|
||||
data-testid="triggerConfig-type"
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.source'),
|
||||
field: t('label.data-asset-plural'),
|
||||
})}
|
||||
showSearch={false}>
|
||||
{map(SubscriptionType, (value) => {
|
||||
return [
|
||||
SubscriptionType.ActivityFeed,
|
||||
SubscriptionType.DataInsight,
|
||||
].includes(value) ? null : (
|
||||
<Select.Option key={value} value={value}>
|
||||
<Space size={16}>
|
||||
{getAlertsActionTypeIcon(
|
||||
value as SubscriptionType
|
||||
)}
|
||||
{getAlertActionTypeDisplayName(value)}
|
||||
</Space>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={resourcesOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
{getDestinationConfigFields()}
|
||||
</Space>
|
||||
</Col>
|
||||
<Col className="footer" span={24}>
|
||||
<Button onClick={() => history.goBack()}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="save"
|
||||
htmlType="submit"
|
||||
loading={isButtonLoading}
|
||||
type="primary">
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
<Col span={24} />
|
||||
<Col span={24} />
|
||||
</Row>
|
||||
</>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.filter-plural')}
|
||||
subHeading={t('message.alerts-filter-description')}
|
||||
/>
|
||||
|
||||
<Form.List
|
||||
name={['filteringRules', 'rules']}
|
||||
rules={[
|
||||
{
|
||||
validator: listLengthValidator(
|
||||
t('label.filter-plural')
|
||||
),
|
||||
},
|
||||
]}>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Button
|
||||
block
|
||||
data-testid="add-filters"
|
||||
icon={<PlusOutlined />}
|
||||
type="default"
|
||||
onClick={() => add({}, 0)}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.filter-plural'),
|
||||
})}
|
||||
</Button>
|
||||
{fields.map(({ key, name }) => (
|
||||
<div key={`filteringRules-${key}`}>
|
||||
{name > 0 && (
|
||||
<Divider
|
||||
style={{
|
||||
margin: 0,
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<div className="flex-1">
|
||||
<Form.Item key={key} name={[name, 'name']}>
|
||||
<Select
|
||||
options={functions}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.condition'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{filters &&
|
||||
filters[name] &&
|
||||
getConditionField(
|
||||
filters[name].name ?? '',
|
||||
name
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
initialValue={Effect.Include}
|
||||
key={key}
|
||||
name={[name, 'effect']}>
|
||||
<Select
|
||||
options={map(
|
||||
Effect,
|
||||
(func: string) => ({
|
||||
label: startCase(func),
|
||||
value: func,
|
||||
})
|
||||
)}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.effect'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
data-testid={`remove-filter-rule-${name}`}
|
||||
icon={
|
||||
<SVGIcons
|
||||
alt={t('label.delete')}
|
||||
className="w-4"
|
||||
icon={Icons.DELETE}
|
||||
/>
|
||||
}
|
||||
type="text"
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<Form.ErrorList errors={errors} />
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space className="w-full" direction="vertical" size={16}>
|
||||
<StyledCard
|
||||
heading={t('label.destination')}
|
||||
subHeading={t('message.alerts-destination-description')}
|
||||
/>
|
||||
<Form.Item
|
||||
required
|
||||
name="subscriptionType"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.destination'),
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
data-testid="alert-action-type"
|
||||
disabled={provider === ProviderType.System}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.source'),
|
||||
})}
|
||||
showSearch={false}>
|
||||
{map(SubscriptionType, (value) => {
|
||||
return [SubscriptionType.ActivityFeed].includes(
|
||||
value
|
||||
) ? null : (
|
||||
<Select.Option key={value} value={value}>
|
||||
<Space size={16}>
|
||||
{getAlertsActionTypeIcon(
|
||||
value as SubscriptionType
|
||||
)}
|
||||
{getAlertActionTypeDisplayName(value)}
|
||||
</Space>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{getDestinationConfigFields()}
|
||||
</Space>
|
||||
</Col>
|
||||
<Col className="footer" span={24}>
|
||||
<Button onClick={() => history.goBack()}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="save"
|
||||
htmlType="submit"
|
||||
loading={isButtonLoading}
|
||||
type="primary">
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
<Col span={24} />
|
||||
<Col span={24} />
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Button, Col, Form, Input, Row, Typography } from 'antd';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import { AxiosError } from 'axios';
|
||||
import { map, startCase } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor';
|
||||
import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
import { HTTP_STATUS_CODE } from '../../constants/Auth.constants';
|
||||
import {
|
||||
GlobalSettingOptions,
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../../constants/GlobalSettings.constants';
|
||||
import { ENTITY_NAME_REGEX } from '../../constants/regex.constants';
|
||||
import { CreateEventSubscription } from '../../generated/events/api/createEventSubscription';
|
||||
import {
|
||||
AlertType,
|
||||
ProviderType,
|
||||
SubscriptionType,
|
||||
} from '../../generated/events/eventSubscription';
|
||||
import { FilterResourceDescriptor } from '../../generated/events/filterResourceDescriptor';
|
||||
import {
|
||||
createObservabilityAlert,
|
||||
getResourceFunctions,
|
||||
} from '../../rest/observabilityAPI';
|
||||
import { getSettingPath } from '../../utils/RouterUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import './add-observability-page.less';
|
||||
import DestinationFormItem from './DestinationFormItem/DestinationFormItem.component';
|
||||
import ObservabilityFormActionItem from './ObservabilityFormActionItem/ObservabilityFormActionItem';
|
||||
import ObservabilityFormFiltersItem from './ObservabilityFormFiltersItem/ObservabilityFormFiltersItem';
|
||||
import { default as ObservabilityFormTriggerItem } from './ObservabilityFormTriggerItem/ObservabilityFormTriggerItem';
|
||||
|
||||
function AddObservabilityPage() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [form] = useForm<CreateEventSubscription>();
|
||||
|
||||
const [filterResources, setFilterResources] = useState<
|
||||
FilterResourceDescriptor[]
|
||||
>([]);
|
||||
|
||||
const notificationsPath = getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.ALERT
|
||||
);
|
||||
|
||||
const breadcrumb = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: t('label.setting-plural'),
|
||||
url: notificationsPath,
|
||||
},
|
||||
{
|
||||
name: t('label.observability'),
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
[notificationsPath]
|
||||
);
|
||||
|
||||
const fetchFunctions = async () => {
|
||||
try {
|
||||
const filterResources = await getResourceFunctions();
|
||||
|
||||
setFilterResources(filterResources.data);
|
||||
} catch (error) {
|
||||
// TODO: Handle error
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFunctions();
|
||||
}, []);
|
||||
|
||||
const handleSave = async (data: CreateEventSubscription) => {
|
||||
try {
|
||||
const resources = [data.resources as unknown as string];
|
||||
await createObservabilityAlert({
|
||||
...data,
|
||||
resources,
|
||||
});
|
||||
|
||||
showSuccessToast(
|
||||
t(`server.${'create'}-entity-success`, {
|
||||
entity: t('label.alert-plural'),
|
||||
})
|
||||
);
|
||||
history.push(
|
||||
getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
// We need this check to have correct redirection after updating the subscription
|
||||
alert?.name === 'ActivityFeedAlert'
|
||||
? GlobalSettingOptions.ACTIVITY_FEED
|
||||
: GlobalSettingOptions.ALERTS
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT
|
||||
) {
|
||||
showErrorToast(
|
||||
t('server.entity-already-exist', {
|
||||
entity: t('label.alert'),
|
||||
entityPlural: t('label.alert-lowercase-plural'),
|
||||
name: data.name,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t(`server.${'entity-creation-error'}`, {
|
||||
entity: t('label.alert-lowercase'),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row className="add-notification-container" gutter={[24, 24]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumb} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Typography.Title level={5}>
|
||||
{t('label.create-entity', { entity: t('label.observability') })}
|
||||
</Typography.Title>
|
||||
<Typography.Text>{t('message.alerts-description')}</Typography.Text>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form<CreateEventSubscription> form={form} onFinish={handleSave}>
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
label={t('label.name')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
pattern: ENTITY_NAME_REGEX,
|
||||
message: t('message.entity-name-validation'),
|
||||
},
|
||||
]}>
|
||||
<Input placeholder={t('label.name')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
label={t('label.description')}
|
||||
labelCol={{ span: 24 }}
|
||||
name="description"
|
||||
trigger="onTextChange"
|
||||
valuePropName="initialValue">
|
||||
<RichTextEditor
|
||||
data-testid="description"
|
||||
height="200px"
|
||||
initialValue=""
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ObservabilityFormTriggerItem
|
||||
buttonLabel={t('label.add-entity', {
|
||||
entity: t('label.trigger'),
|
||||
})}
|
||||
filterResources={filterResources}
|
||||
heading={t('label.trigger')}
|
||||
subHeading={t('message.alerts-trigger-description')}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ObservabilityFormFiltersItem
|
||||
filterResources={filterResources}
|
||||
heading={t('label.filter-plural')}
|
||||
subHeading={t('message.alerts-filter-description')}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ObservabilityFormActionItem
|
||||
filterResources={filterResources}
|
||||
heading={t('label.action-plural')}
|
||||
subHeading={t('message.alerts-filter-description')}
|
||||
/>
|
||||
</Col>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue={AlertType.Observability}
|
||||
name="alertType"
|
||||
/>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue={ProviderType.User}
|
||||
name="provider"
|
||||
/>
|
||||
<Col span={24}>
|
||||
<DestinationFormItem
|
||||
buttonLabel={t('label.add-entity', {
|
||||
entity: t('label.destination'),
|
||||
})}
|
||||
filterResources={map(SubscriptionType, (type) => ({
|
||||
label: startCase(type),
|
||||
value: type,
|
||||
}))}
|
||||
heading={t('label.destination')}
|
||||
subHeading={t('message.alerts-destination-description')}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button className="m-r-sm" htmlType="submit">
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
<Button onClick={() => history.goBack()}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddObservabilityPage;
|
||||
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2024 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 { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Form, Row, Select, Typography } from 'antd';
|
||||
import Input from 'antd/lib/input/Input';
|
||||
import { DefaultOptionType } from 'antd/lib/select';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubscriptionCategory } from '../../../generated/events/eventSubscription';
|
||||
|
||||
function DestinationFormItem({
|
||||
heading,
|
||||
subHeading,
|
||||
buttonLabel,
|
||||
filterResources,
|
||||
}: Readonly<{
|
||||
heading: string;
|
||||
subHeading: string;
|
||||
buttonLabel: string;
|
||||
filterResources: DefaultOptionType[];
|
||||
}>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="trigger-item-container">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={24}>
|
||||
<Typography.Text>{heading}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Typography.Text className="text-xs text-grey-muted">
|
||||
{subHeading}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.List name="destinations">
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
{fields.map((field, index) => (
|
||||
<>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
required
|
||||
messageVariables={{
|
||||
fieldName: t('label.data-asset-plural'),
|
||||
}}
|
||||
name={[index, 'type']}>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="triggerConfig-type"
|
||||
options={filterResources}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.data-asset-plural'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue={SubscriptionCategory.External}
|
||||
name={[index, 'category']}
|
||||
/>
|
||||
<Form.Item
|
||||
label=""
|
||||
name={[index, 'config', 'receivers']}>
|
||||
<Input placeholder="EndPoint URL" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
data-testid={`remove-action-rule-${name}`}
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
</>
|
||||
))}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={add}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default DestinationFormItem;
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 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 { FilterResourceDescriptor } from '../../../generated/events/filterResourceDescriptor';
|
||||
|
||||
export interface ObservabilityFormActionItemProps {
|
||||
heading: string;
|
||||
subHeading: string;
|
||||
filterResources: FilterResourceDescriptor[];
|
||||
}
|
||||
@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2024 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 { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Form, Row, Select, Switch, Typography } from 'antd';
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AsyncSelect } from '../../../components/AsyncSelect/AsyncSelect';
|
||||
import { PAGE_SIZE_LARGE } from '../../../constants/constants';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { Effect } from '../../../generated/events/eventSubscription';
|
||||
import { InputType } from '../../../generated/events/filterResourceDescriptor';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { listLengthValidator } from '../../../utils/Alerts/AlertsUtil';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getSearchIndexFromEntityType } from '../ObservabilityFormFiltersItem/ObservabilityFormFiltersItem';
|
||||
import { ObservabilityFormActionItemProps } from './ObservabilityFormActionItem.interface';
|
||||
|
||||
function ObservabilityFormActionItem({
|
||||
heading,
|
||||
subHeading,
|
||||
filterResources,
|
||||
}: Readonly<ObservabilityFormActionItemProps>) {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// Watchers
|
||||
const filters = Form.useWatch(['input', 'actions'], form);
|
||||
const triggerValue = Form.useWatch(['resources'], form);
|
||||
|
||||
const selectedTrigger = useMemo(
|
||||
() => form.getFieldValue(['resources']),
|
||||
[triggerValue, form]
|
||||
);
|
||||
const selectedDescriptor = useMemo(
|
||||
() => filterResources.find((resource) => resource.name === selectedTrigger),
|
||||
[filterResources, selectedTrigger]
|
||||
);
|
||||
|
||||
const supportedActions = useMemo(
|
||||
() => selectedDescriptor?.supportedActions,
|
||||
[selectedDescriptor]
|
||||
);
|
||||
|
||||
const searchEntity = useCallback(
|
||||
async (search: string, searchIndex: SearchIndex) => {
|
||||
try {
|
||||
const response = await searchData(
|
||||
search,
|
||||
1,
|
||||
PAGE_SIZE_LARGE,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
searchIndex
|
||||
);
|
||||
|
||||
return response.data.hits.hits.map((d) => ({
|
||||
label: getEntityName(d._source),
|
||||
value: d._source.fullyQualifiedName,
|
||||
}));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getEntityByFQN = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
return searchEntity(
|
||||
searchText,
|
||||
getSearchIndexFromEntityType(selectedTrigger)
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[searchEntity, selectedTrigger]
|
||||
);
|
||||
|
||||
const getTableSuggestions = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
return searchEntity(searchText, SearchIndex.TABLE);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[searchEntity, selectedTrigger]
|
||||
);
|
||||
|
||||
const getDomainOptions = useCallback(
|
||||
async (searchText: string) => {
|
||||
return searchEntity(searchText, SearchIndex.DOMAIN);
|
||||
},
|
||||
[searchEntity]
|
||||
);
|
||||
|
||||
// Run time values needed for conditional rendering
|
||||
const functions = useMemo(
|
||||
() =>
|
||||
selectedDescriptor?.supportedActions?.map((func) => ({
|
||||
label: getEntityName(func),
|
||||
value: func.name,
|
||||
})),
|
||||
[selectedDescriptor]
|
||||
);
|
||||
|
||||
const getFieldByArgumentType = useCallback(
|
||||
(fieldName: number, argument: string) => {
|
||||
switch (argument) {
|
||||
case 'fqnList':
|
||||
return (
|
||||
<>
|
||||
<Col key="fqn-list-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getEntityByFQN}
|
||||
data-testid="fqn-list-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.fqn-uppercase'),
|
||||
})}
|
||||
showArrow={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
className="d-none"
|
||||
initialValue="fqnList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'domainList':
|
||||
return (
|
||||
<>
|
||||
<Col key="domain-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getDomainOptions}
|
||||
data-testid="domain-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.domain-lowercase'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
className="d-none"
|
||||
initialValue="domainList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'tableNameList':
|
||||
return (
|
||||
<>
|
||||
<Col key="domain-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getTableSuggestions}
|
||||
data-testid="table-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.table-lowercase'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
className="d-none"
|
||||
initialValue="tableNameList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
},
|
||||
[getEntityByFQN, getDomainOptions]
|
||||
);
|
||||
|
||||
// Render condition field based on function selected
|
||||
const getConditionField = (condition: string, name: number) => {
|
||||
const selectedAction = supportedActions?.find(
|
||||
(action) => action.name === condition
|
||||
);
|
||||
const requireInput = selectedAction?.inputType === InputType.Runtime;
|
||||
const requiredArguments = selectedAction?.arguments;
|
||||
|
||||
if (!requireInput) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{requiredArguments?.map((argument) => {
|
||||
return getFieldByArgumentType(name, argument);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="trigger-item-container">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={24}>
|
||||
<Typography.Text>{heading}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Typography.Text className="text-xs text-grey-muted">
|
||||
{subHeading}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.List
|
||||
name={['input', 'actions']}
|
||||
rules={[
|
||||
{
|
||||
validator: listLengthValidator(t('label.action-plural')),
|
||||
},
|
||||
]}>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<Row gutter={[16, 16]}>
|
||||
{fields.map(({ key, name }) => (
|
||||
<Col key={`observability-${key}`} span={24}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={11}>
|
||||
<Form.Item key={`action-${key}`} name={[name, 'name']}>
|
||||
<Select
|
||||
options={functions}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.action'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!isNil(selectedDescriptor) &&
|
||||
!isEmpty(filters) &&
|
||||
filters[name] &&
|
||||
getConditionField(filters[name].name ?? '', name)}
|
||||
<Col span={2}>
|
||||
<Button
|
||||
data-testid={`remove-action-rule-${name}`}
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
getValueFromEvent={(value) =>
|
||||
value ? 'include' : 'exclude'
|
||||
}
|
||||
initialValue={Effect.Include}
|
||||
key={`effect-${key}`}
|
||||
label={
|
||||
<Typography.Text>{t('label.include')}</Typography.Text>
|
||||
}
|
||||
name={[name, 'effect']}>
|
||||
<Switch defaultChecked />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
{fields.length < (supportedActions?.length ?? 1) && (
|
||||
<Col span={24}>
|
||||
<Button
|
||||
data-testid="add-action"
|
||||
type="primary"
|
||||
onClick={() => add({})}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.action'),
|
||||
})}
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Row>
|
||||
)}
|
||||
</Form.List>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ObservabilityFormActionItem;
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 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 { FilterResourceDescriptor } from '../../../generated/events/filterResourceDescriptor';
|
||||
|
||||
export interface ObservabilityFormFiltersItemProps {
|
||||
heading: string;
|
||||
subHeading: string;
|
||||
filterResources: FilterResourceDescriptor[];
|
||||
}
|
||||
@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2024 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 { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Form, Row, Select, Switch, Typography } from 'antd';
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AsyncSelect } from '../../../components/AsyncSelect/AsyncSelect';
|
||||
import { PAGE_SIZE_LARGE } from '../../../constants/constants';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { Effect } from '../../../generated/events/eventSubscription';
|
||||
import { InputType } from '../../../generated/events/filterResourceDescriptor';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { listLengthValidator } from '../../../utils/Alerts/AlertsUtil';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { ObservabilityFormFiltersItemProps } from './ObservabilityFormFiltersItem.interface';
|
||||
|
||||
export const getSearchIndexFromEntityType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'table':
|
||||
return SearchIndex.TABLE;
|
||||
case 'topic':
|
||||
return SearchIndex.TOPIC;
|
||||
case 'pipeline':
|
||||
return SearchIndex.PIPELINE;
|
||||
case 'container':
|
||||
return SearchIndex.CONTAINER;
|
||||
case 'testCase':
|
||||
return SearchIndex.TEST_CASE;
|
||||
case 'testSuite':
|
||||
return SearchIndex.TEST_SUITE;
|
||||
default:
|
||||
return SearchIndex.TABLE;
|
||||
}
|
||||
};
|
||||
|
||||
function ObservabilityFormFiltersItem({
|
||||
heading,
|
||||
subHeading,
|
||||
filterResources,
|
||||
}: Readonly<ObservabilityFormFiltersItemProps>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// Watchers
|
||||
const filters = Form.useWatch(['input', 'filters'], form);
|
||||
const triggerValue = Form.useWatch(['resources'], form);
|
||||
|
||||
const selectedTrigger = useMemo(
|
||||
() => form.getFieldValue(['resources']),
|
||||
[triggerValue, form]
|
||||
);
|
||||
const supportedFilters = useMemo(
|
||||
() =>
|
||||
filterResources.find((resource) => resource.name === selectedTrigger)
|
||||
?.supportedFilters,
|
||||
[filterResources, selectedTrigger]
|
||||
);
|
||||
|
||||
const searchEntity = useCallback(
|
||||
async (search: string, searchIndex: SearchIndex) => {
|
||||
try {
|
||||
const response = await searchData(
|
||||
search,
|
||||
1,
|
||||
PAGE_SIZE_LARGE,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
searchIndex
|
||||
);
|
||||
|
||||
return response.data.hits.hits.map((d) => ({
|
||||
label: getEntityName(d._source),
|
||||
value: d._source.fullyQualifiedName,
|
||||
}));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getEntityByFQN = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
return searchEntity(
|
||||
searchText,
|
||||
getSearchIndexFromEntityType(selectedTrigger)
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[searchEntity, selectedTrigger]
|
||||
);
|
||||
|
||||
const getTableSuggestions = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
return searchEntity(searchText, SearchIndex.TABLE);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[searchEntity, selectedTrigger]
|
||||
);
|
||||
|
||||
const getDomainOptions = useCallback(
|
||||
async (searchText: string) => {
|
||||
return searchEntity(searchText, SearchIndex.DOMAIN);
|
||||
},
|
||||
[searchEntity]
|
||||
);
|
||||
|
||||
const getOwnerOptions = useCallback(
|
||||
async (searchText: string) => {
|
||||
return searchEntity(searchText, SearchIndex.USER);
|
||||
},
|
||||
[searchEntity]
|
||||
);
|
||||
|
||||
// Run time values needed for conditional rendering
|
||||
const functions = useMemo(
|
||||
() =>
|
||||
supportedFilters?.map((func) => ({
|
||||
label: getEntityName(func),
|
||||
value: func.name,
|
||||
})),
|
||||
[supportedFilters]
|
||||
);
|
||||
|
||||
const getFieldByArgumentType = useCallback(
|
||||
(fieldName: number, argument: string, index: number) => {
|
||||
switch (argument) {
|
||||
case 'fqnList':
|
||||
return (
|
||||
<>
|
||||
<Col key="fqn-list-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', index, 'input']}>
|
||||
<AsyncSelect
|
||||
api={getEntityByFQN}
|
||||
data-testid="fqn-list-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.fqn-uppercase'),
|
||||
})}
|
||||
showArrow={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue="fqnList"
|
||||
name={[fieldName, 'arguments', index, 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'domainList':
|
||||
return (
|
||||
<>
|
||||
<Col key="domain-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getDomainOptions}
|
||||
data-testid="domain-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.domain-lowercase'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue="domainList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'tableNameList':
|
||||
return (
|
||||
<>
|
||||
<Col key="domain-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getTableSuggestions}
|
||||
data-testid="table-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.table-lowercase'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue="tableNameList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'ownerNameList':
|
||||
return (
|
||||
<>
|
||||
<Col key="owner-select" span={11}>
|
||||
<Form.Item
|
||||
className="w-full"
|
||||
name={[fieldName, 'arguments', 'input']}>
|
||||
<AsyncSelect
|
||||
api={getOwnerOptions}
|
||||
data-testid="owner-select"
|
||||
mode="multiple"
|
||||
placeholder={t('label.search-by-type', {
|
||||
type: t('label.owner-lowercase'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Form.Item
|
||||
hidden
|
||||
initialValue="ownerNameList"
|
||||
name={[fieldName, 'arguments', 'name']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
},
|
||||
[getEntityByFQN, getDomainOptions]
|
||||
);
|
||||
|
||||
// Render condition field based on function selected
|
||||
const getConditionField = (condition: string, name: number) => {
|
||||
const selectedFilter = supportedFilters?.find(
|
||||
(filter) => filter.name === condition
|
||||
);
|
||||
const requireInput = selectedFilter?.inputType === InputType.Runtime;
|
||||
const requiredArguments = selectedFilter?.arguments;
|
||||
|
||||
if (!requireInput) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{requiredArguments?.map((argument, index) => {
|
||||
return getFieldByArgumentType(name, argument, index);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="trigger-item-container">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={24}>
|
||||
<Typography.Text>{heading}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Typography.Text className="text-xs text-grey-muted">
|
||||
{subHeading}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.List
|
||||
name={['input', 'filters']}
|
||||
rules={[
|
||||
{
|
||||
validator: listLengthValidator(t('label.filter-plural')),
|
||||
},
|
||||
]}>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<Row gutter={[16, 16]}>
|
||||
{fields.map(({ key, name }) => (
|
||||
<Col key={`observability-${key}`} span={24}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={11}>
|
||||
<Form.Item key={`filter-${key}`} name={[name, 'name']}>
|
||||
<Select
|
||||
options={functions}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.filter'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!isNil(supportedFilters) &&
|
||||
!isEmpty(filters) &&
|
||||
filters[name] &&
|
||||
getConditionField(filters[name].name ?? '', name)}
|
||||
<Col span={2}>
|
||||
<Button
|
||||
data-testid={`remove-filter-rule-${name}`}
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
getValueFromEvent={(value) =>
|
||||
value ? 'include' : 'exclude'
|
||||
}
|
||||
initialValue={Effect.Include}
|
||||
key={`effect-${key}`}
|
||||
label={
|
||||
<Typography.Text>{t('label.include')}</Typography.Text>
|
||||
}
|
||||
name={[name, 'effect']}
|
||||
valuePropName="checked">
|
||||
<Switch defaultChecked />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
<Col span={24}>
|
||||
<Button
|
||||
data-testid="add-filters"
|
||||
type="primary"
|
||||
onClick={() => add({})}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.filter'),
|
||||
})}
|
||||
</Button>
|
||||
</Col>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Row>
|
||||
)}
|
||||
</Form.List>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ObservabilityFormFiltersItem;
|
||||
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2024 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 { FilterResourceDescriptor } from '../../../generated/events/filterResourceDescriptor';
|
||||
|
||||
export interface ObservabilityFormTriggerItemProps {
|
||||
heading: string;
|
||||
subHeading: string;
|
||||
buttonLabel: string;
|
||||
filterResources: FilterResourceDescriptor[];
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Button, Card, Col, Form, Row, Select, Space, Typography } from 'antd';
|
||||
import { startCase } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconTestSuite } from '../../../assets/svg/icon-test-suite.svg';
|
||||
import { getEntityIcon } from '../../../utils/TableUtils';
|
||||
import './observability-form-trigger-item.less';
|
||||
import { ObservabilityFormTriggerItemProps } from './ObservabilityFormTriggerItem.interface';
|
||||
|
||||
function ObservabilityFormTriggerItem({
|
||||
heading,
|
||||
subHeading,
|
||||
buttonLabel,
|
||||
filterResources,
|
||||
}: Readonly<ObservabilityFormTriggerItemProps>) {
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getIconForEntity = (type: string) => {
|
||||
switch (type) {
|
||||
case 'container':
|
||||
case 'pipeline':
|
||||
case 'topic':
|
||||
case 'table':
|
||||
return getEntityIcon(type);
|
||||
case 'testCase':
|
||||
case 'testSuite':
|
||||
return <IconTestSuite height={16} width={16} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleAddTriggerClick = useCallback(() => {
|
||||
setEditMode(true);
|
||||
}, []);
|
||||
|
||||
const resourcesOptions = useMemo(
|
||||
() =>
|
||||
filterResources.map((resource) => ({
|
||||
label: (
|
||||
<Space align="center" size={4}>
|
||||
{getIconForEntity(resource.name ?? '')}
|
||||
{startCase(resource.name)}
|
||||
</Space>
|
||||
),
|
||||
value: resource.name,
|
||||
})),
|
||||
[filterResources]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="trigger-item-container">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={24}>
|
||||
<Typography.Text>{heading}</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Typography.Text className="text-xs text-grey-muted">
|
||||
{subHeading}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{editMode ? (
|
||||
<Form.Item
|
||||
required
|
||||
messageVariables={{
|
||||
fieldName: t('label.data-asset-plural'),
|
||||
}}
|
||||
name={['resources']}>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="triggerConfig-type"
|
||||
options={resourcesOptions}
|
||||
placeholder={t('label.select-field', {
|
||||
field: t('label.data-asset-plural'),
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddTriggerClick}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ObservabilityFormTriggerItem;
|
||||
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
.trigger-item-container {
|
||||
background-color: #f9f9f9;
|
||||
.ant-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
.add-notification-container {
|
||||
.ant-form-item {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { getAlertsFromName, triggerEventById } from '../../rest/alertsAPI';
|
||||
import AlertDataInsightReportPage from './AlertDataInsightReportPage';
|
||||
|
||||
const MOCK_DATA_INSIGHTS_ALERT_DATA = {
|
||||
id: '6bb36fa4-55eb-448c-a96d-f635cce913fd',
|
||||
name: 'DataInsightReport',
|
||||
fullyQualifiedName: 'DataInsightReport',
|
||||
description:
|
||||
'Data Insight Report send to the admin (organization level) and teams (team level) at given interval.',
|
||||
href: 'http://localhost:8585/api/v1/events/subscriptions/6bb36fa4-55eb-448c-a96d-f635cce913fd',
|
||||
version: 0.1,
|
||||
updatedAt: 1683187040175,
|
||||
updatedBy: 'admin',
|
||||
alertType: 'DataInsightReport',
|
||||
trigger: {
|
||||
triggerType: 'Scheduled',
|
||||
scheduleInfo: 'Weekly',
|
||||
},
|
||||
subscriptionType: 'DataInsight',
|
||||
subscriptionConfig: {
|
||||
sendToTeams: true,
|
||||
sendToAdmins: true,
|
||||
},
|
||||
enabled: true,
|
||||
batchSize: 10,
|
||||
timeout: 10,
|
||||
readTimeout: 12,
|
||||
deleted: false,
|
||||
provider: 'system',
|
||||
};
|
||||
|
||||
jest.mock('../../rest/alertsAPI', () => ({
|
||||
getAlertsFromName: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(MOCK_DATA_INSIGHTS_ALERT_DATA)),
|
||||
triggerEventById: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/authHooks', () => ({
|
||||
useAuth: jest.fn().mockImplementation(() => ({ isAdminUser: true })),
|
||||
}));
|
||||
|
||||
describe('Test Alert Data Insights Report Page', () => {
|
||||
it('Should render the child elements', async () => {
|
||||
render(<AlertDataInsightReportPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
|
||||
|
||||
expect(screen.getByText('label.data-insight-report')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Data Insight Report send to the admin (organization level) and teams (team level) at given interval.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('edit-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('trigger')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('schedule-info')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('destination')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render data', async () => {
|
||||
render(<AlertDataInsightReportPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
|
||||
|
||||
const editButton = screen.getByTestId('edit-button');
|
||||
|
||||
expect(editButton).toHaveAttribute(
|
||||
'href',
|
||||
`/settings/notifications/edit-data-insight-report/${MOCK_DATA_INSIGHTS_ALERT_DATA.id}`
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('trigger-type')).toHaveTextContent('Scheduled');
|
||||
expect(screen.getByTestId('schedule-info-type')).toHaveTextContent(
|
||||
'Weekly'
|
||||
);
|
||||
expect(screen.getByTestId('sendToAdmins')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sendToTeams')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render the error placeholder if api fails or no data', async () => {
|
||||
(getAlertsFromName as jest.Mock).mockRejectedValueOnce(() =>
|
||||
Promise.reject()
|
||||
);
|
||||
render(<AlertDataInsightReportPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
|
||||
|
||||
expect(screen.getByTestId('no-data-image')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('add-placeholder-button')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByTestId('create-error-placeholder-label.data-insight-report')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Send Now button should work', async () => {
|
||||
const mockTrigger = triggerEventById as jest.Mock;
|
||||
render(<AlertDataInsightReportPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
|
||||
|
||||
const sendNowButton = screen.getByTestId('send-now-button');
|
||||
|
||||
expect(sendNowButton).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(sendNowButton);
|
||||
});
|
||||
|
||||
expect(mockTrigger).toHaveBeenCalledWith(MOCK_DATA_INSIGHTS_ALERT_DATA.id);
|
||||
});
|
||||
});
|
||||
@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Popover,
|
||||
Row,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import formateCron from 'cronstrue';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as IconSend } from '../../assets/svg/paper-plane.svg';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import PageHeader from '../../components/PageHeader/PageHeader.component';
|
||||
import { ALERTS_DOCS } from '../../constants/docs.constants';
|
||||
import {
|
||||
GlobalSettingOptions,
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../../constants/GlobalSettings.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
|
||||
import {
|
||||
EventSubscription,
|
||||
ScheduleInfo,
|
||||
} from '../../generated/events/eventSubscription';
|
||||
import { useAuth } from '../../hooks/authHooks';
|
||||
import { getAlertsFromName, triggerEventById } from '../../rest/alertsAPI';
|
||||
import { EDIT_DATA_INSIGHT_REPORT_PATH } from '../../utils/Alerts/AlertsUtil';
|
||||
import { getSettingPath } from '../../utils/RouterUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const AlertDataInsightReportPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const { isAdminUser } = useAuth();
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [dataInsightAlert, setDataInsightAlert] = useState<EventSubscription>();
|
||||
const [isSendingReport, setIsSendingReport] = useState<boolean>(false);
|
||||
|
||||
const fetchDataInsightsAlert = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getAlertsFromName('DataInsightReport');
|
||||
setDataInsightAlert(response);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSendDataInsightReport = async () => {
|
||||
if (isUndefined(dataInsightAlert)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSendingReport(true);
|
||||
await triggerEventById(dataInsightAlert.id);
|
||||
showSuccessToast(t('message.data-insight-report-send-success-message'));
|
||||
} catch (error) {
|
||||
showErrorToast(t('message.data-insight-report-send-failed-message'));
|
||||
} finally {
|
||||
setIsSendingReport(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataInsightsAlert();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (!isAdminUser) {
|
||||
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
|
||||
}
|
||||
|
||||
if (isUndefined(dataInsightAlert)) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
permission
|
||||
doc={ALERTS_DOCS}
|
||||
heading={t('label.data-insight-report')}
|
||||
type={ERROR_PLACEHOLDER_TYPE.CREATE}
|
||||
onClick={() =>
|
||||
history.push(
|
||||
getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.ADD_DATA_INSIGHT_REPORT_ALERT
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isEnabled = Boolean(dataInsightAlert?.enabled);
|
||||
|
||||
return (
|
||||
<Row align="middle" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Space className="w-full justify-between">
|
||||
<PageHeader
|
||||
data={{
|
||||
header: (
|
||||
<Space align="center" size={4}>
|
||||
{t('label.data-insight-report')}{' '}
|
||||
{!isEnabled ? (
|
||||
<Badge
|
||||
className="badge-grey"
|
||||
count={t('label.disabled')}
|
||||
data-testid="disabled"
|
||||
/>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
subHeader: dataInsightAlert?.description ?? '',
|
||||
}}
|
||||
/>
|
||||
<Space size={16}>
|
||||
<Link
|
||||
data-testid="edit-button"
|
||||
to={`${EDIT_DATA_INSIGHT_REPORT_PATH}/${dataInsightAlert?.id}`}>
|
||||
<Button icon={<Icon component={IconEdit} size={12} />}>
|
||||
{t('label.edit')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
data-testid="send-now-button"
|
||||
icon={<Icon component={IconSend} size={12} />}
|
||||
loading={isSendingReport}
|
||||
onClick={handleSendDataInsightReport}>
|
||||
{t('label.send')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
{/* Trigger section */}
|
||||
<>
|
||||
<Typography.Title data-testid="trigger" level={5}>
|
||||
{t('label.trigger')}
|
||||
</Typography.Title>
|
||||
<Typography.Text data-testid="trigger-type">
|
||||
{dataInsightAlert?.trigger?.triggerType}
|
||||
</Typography.Text>
|
||||
</>
|
||||
<Divider />
|
||||
|
||||
{/* Schedule Info Section */}
|
||||
<>
|
||||
<Typography.Title data-testid="schedule-info" level={5}>
|
||||
{t('label.schedule-info')}
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Typography.Text data-testid="schedule-info-type">
|
||||
{dataInsightAlert?.trigger?.scheduleInfo}
|
||||
</Typography.Text>
|
||||
|
||||
{dataInsightAlert?.trigger?.scheduleInfo ===
|
||||
ScheduleInfo.Custom &&
|
||||
dataInsightAlert?.trigger?.cronExpression ? (
|
||||
<Popover
|
||||
content={
|
||||
<div>
|
||||
{formateCron.toString(
|
||||
dataInsightAlert.trigger.cronExpression,
|
||||
{
|
||||
use24HourTimeFormat: true,
|
||||
verbose: true,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
trigger="hover">
|
||||
<Typography.Text code>
|
||||
{dataInsightAlert.trigger.cronExpression}
|
||||
</Typography.Text>
|
||||
</Popover>
|
||||
) : null}
|
||||
</Space>
|
||||
</>
|
||||
<Divider />
|
||||
|
||||
{/* Destination section */}
|
||||
<>
|
||||
<Typography.Title data-testid="destination" level={5}>
|
||||
{t('label.destination')}
|
||||
</Typography.Title>
|
||||
<Space size={8}>
|
||||
<Typography.Text>{t('label.send-to')}</Typography.Text>
|
||||
<Typography.Text>
|
||||
{dataInsightAlert?.subscriptionConfig?.sendToAdmins ? (
|
||||
<CheckCircleOutlined data-testid="sendToAdmins" />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)}{' '}
|
||||
{t('label.admin-plural')}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{dataInsightAlert?.subscriptionConfig?.sendToTeams ? (
|
||||
<CheckCircleOutlined data-testid="sendToTeams" />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)}{' '}
|
||||
{t('label.team-plural')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertDataInsightReportPage;
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import ObservabilityPage from './ObservabilityPage';
|
||||
|
||||
const MOCK_DATA = [
|
||||
{
|
||||
id: '971a21b3-eeaf-4765-bda7-4e2cdb9788de',
|
||||
name: 'alert-test',
|
||||
fullyQualifiedName: 'alert-test',
|
||||
href: 'http://localhost:8585/api/v1/events/subscriptions/971a21b3-eeaf-4765-bda7-4e2cdb9788de',
|
||||
version: 0.1,
|
||||
updatedAt: 1682366749021,
|
||||
updatedBy: 'admin',
|
||||
filteringRules: {
|
||||
resources: ['all'],
|
||||
rules: [
|
||||
{
|
||||
name: 'matchIngestionPipelineState',
|
||||
effect: 'include',
|
||||
condition: "matchIngestionPipelineState('partialSuccess')",
|
||||
},
|
||||
],
|
||||
},
|
||||
subscriptionType: 'Email',
|
||||
subscriptionConfig: {
|
||||
receivers: ['test@gmail.com'],
|
||||
},
|
||||
enabled: true,
|
||||
batchSize: 10,
|
||||
timeout: 10,
|
||||
readTimeout: 12,
|
||||
deleted: false,
|
||||
provider: 'user',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../../rest/alertsAPI', () => ({
|
||||
getAllAlerts: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: MOCK_DATA,
|
||||
paging: { total: 1 },
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
describe.skip('Alerts Page Tests', () => {
|
||||
it('Title should be rendered', async () => {
|
||||
const { findByText } = render(<ObservabilityPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
expect(await findByText('label.alert-plural')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('SubTitle should be rendered', async () => {
|
||||
const { findByText } = render(<ObservabilityPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
expect(await findByText(/message.alerts-description/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Add alert button should be rendered', async () => {
|
||||
const { findByText } = render(<ObservabilityPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
expect(await findByText(/label.create-entity/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* 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 { Button, Col, Row, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg';
|
||||
import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import NextPrevious from '../../components/common/NextPrevious/NextPrevious';
|
||||
import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface';
|
||||
import Table from '../../components/common/Table/Table';
|
||||
import PageHeader from '../../components/PageHeader/PageHeader.component';
|
||||
import { ALERTS_DOCS } from '../../constants/docs.constants';
|
||||
import {
|
||||
GlobalSettingOptions,
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../../constants/GlobalSettings.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import {
|
||||
EventSubscription,
|
||||
ProviderType,
|
||||
} from '../../generated/events/eventSubscription';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
import { usePaging } from '../../hooks/paging/usePaging';
|
||||
import { getAllAlerts } from '../../rest/alertsAPI';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { getSettingPath } from '../../utils/RouterUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
|
||||
const ObservabilityPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [alerts, setAlerts] = useState<EventSubscription[]>([]);
|
||||
const [selectedAlert, setSelectedAlert] = useState<EventSubscription>();
|
||||
const {
|
||||
pageSize,
|
||||
currentPage,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
handlePagingChange,
|
||||
showPagination,
|
||||
paging,
|
||||
} = usePaging();
|
||||
|
||||
const fetchAlerts = useCallback(
|
||||
async (params?: Partial<Paging>) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, paging } = await getAllAlerts({
|
||||
after: params?.after,
|
||||
before: params?.before,
|
||||
limit: pageSize,
|
||||
});
|
||||
|
||||
setAlerts(data.filter((d) => d.provider !== ProviderType.System));
|
||||
handlePagingChange(paging);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
t('server.entity-fetch-error', { entity: t('label.alert-plural') })
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[pageSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlerts();
|
||||
}, [pageSize]);
|
||||
|
||||
const handleAlertDelete = useCallback(async () => {
|
||||
try {
|
||||
setSelectedAlert(undefined);
|
||||
fetchAlerts();
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
}, [fetchAlerts]);
|
||||
|
||||
const onPageChange = useCallback(
|
||||
({ cursorType, currentPage }: PagingHandlerParams) => {
|
||||
if (cursorType) {
|
||||
fetchAlerts({ [cursorType]: paging[cursorType] });
|
||||
handlePageChange(currentPage);
|
||||
}
|
||||
},
|
||||
[paging]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name',
|
||||
width: '200px',
|
||||
key: 'name',
|
||||
render: (name: string, record: EventSubscription) => {
|
||||
return <Link to={`alert/${record.id}`}>{name}</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.trigger'),
|
||||
dataIndex: ['filteringRules', 'resources'],
|
||||
width: '200px',
|
||||
key: 'FilteringRules.resources',
|
||||
render: (resources: string[]) => {
|
||||
return resources?.join(', ') || '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.description'),
|
||||
dataIndex: 'description',
|
||||
flex: true,
|
||||
key: 'description',
|
||||
render: (description: string) =>
|
||||
isEmpty(description) ? (
|
||||
<Typography.Text className="text-grey-muted">
|
||||
{t('label.no-entity', {
|
||||
entity: t('label.description'),
|
||||
})}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
description
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.action-plural'),
|
||||
dataIndex: 'id',
|
||||
width: 120,
|
||||
key: 'id',
|
||||
render: (id: string, record: EventSubscription) => {
|
||||
return (
|
||||
<div className="d-flex items-center">
|
||||
<Tooltip placement="bottom" title={t('label.edit')}>
|
||||
<Link to={`edit-alert/${id}`}>
|
||||
<Button
|
||||
className="d-inline-flex items-center justify-center"
|
||||
data-testid={`alert-edit-${record.name}`}
|
||||
icon={<EditIcon width={16} />}
|
||||
type="text"
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" title={t('label.delete')}>
|
||||
<Button
|
||||
data-testid={`alert-delete-${record.name}`}
|
||||
disabled={record.provider === ProviderType.System}
|
||||
icon={<SVGIcons className="w-4" icon={Icons.DELETE} />}
|
||||
type="text"
|
||||
onClick={() => setSelectedAlert(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[handleAlertDelete]
|
||||
);
|
||||
|
||||
const pageHeaderData = useMemo(
|
||||
() => ({
|
||||
header: t('label.observability'),
|
||||
subHeader: t('message.alerts-description'),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<div className="d-flex justify-between">
|
||||
<PageHeader data={pageHeaderData} />
|
||||
<Link
|
||||
to={getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.ADD_OBSERVABILITY
|
||||
)}>
|
||||
<Button data-testid="create-observability" type="primary">
|
||||
{t('label.create-entity', { entity: t('label.observability') })}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={alerts}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<ErrorPlaceHolder
|
||||
permission
|
||||
className="p-y-md"
|
||||
doc={ALERTS_DOCS}
|
||||
heading={t('label.alert')}
|
||||
type={ERROR_PLACEHOLDER_TYPE.CREATE}
|
||||
onClick={() =>
|
||||
history.push(
|
||||
getSettingPath(
|
||||
GlobalSettingsMenuCategory.NOTIFICATIONS,
|
||||
GlobalSettingOptions.ADD_OBSERVABILITY
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{showPagination && (
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={onPageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeleteWidgetModal
|
||||
afterDeleteAction={handleAlertDelete}
|
||||
allowSoftDelete={false}
|
||||
entityId={selectedAlert?.id ?? ''}
|
||||
entityName={getEntityName(selectedAlert)}
|
||||
entityType={EntityType.SUBSCRIPTION}
|
||||
visible={Boolean(selectedAlert)}
|
||||
onCancel={() => {
|
||||
setSelectedAlert(undefined);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObservabilityPage;
|
||||
@ -0,0 +1,127 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/*
|
||||
* 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 { PagingResponse } from 'Models';
|
||||
import axiosClient from '.';
|
||||
import { CreateEventSubscription } from '../generated/events/api/createEventSubscription';
|
||||
import {
|
||||
EventSubscription,
|
||||
Status,
|
||||
SubscriptionType,
|
||||
} from '../generated/events/eventSubscription';
|
||||
import { FilterResourceDescriptor } from '../generated/events/filterResourceDescriptor';
|
||||
import { Function } from '../generated/type/function';
|
||||
|
||||
const BASE_URL = '/events/subscriptions/observability';
|
||||
|
||||
interface ListAlertsRequestParams {
|
||||
status?: Status;
|
||||
alertType?: SubscriptionType;
|
||||
before?: string;
|
||||
after?: string;
|
||||
include?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export const getAlertsFromId = async (
|
||||
id: string,
|
||||
params?: Pick<ListAlertsRequestParams, 'include'>
|
||||
) => {
|
||||
const response = await axiosClient.get<EventSubscription>(
|
||||
`${BASE_URL}/${id}`,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
include: 'all',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAlertsFromName = async (
|
||||
name: string,
|
||||
params?: Pick<ListAlertsRequestParams, 'include'>
|
||||
) => {
|
||||
const response = await axiosClient.get<EventSubscription>(
|
||||
`${BASE_URL}/name/${name}`,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
include: 'all',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAllAlerts = async (params: ListAlertsRequestParams) => {
|
||||
const response = await axiosClient.get<PagingResponse<EventSubscription[]>>(
|
||||
BASE_URL,
|
||||
{
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createObservabilityAlert = async (
|
||||
alert: CreateEventSubscription
|
||||
) => {
|
||||
const response = await axiosClient.post<EventSubscription>(
|
||||
`/events/subscriptions`,
|
||||
alert
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateObservabilityAlert = async (alert: EventSubscription) => {
|
||||
const response = await axiosClient.put<EventSubscription>(BASE_URL, alert);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteObservabilityAlert = async (id: string) => {
|
||||
const response = await axiosClient.delete(`${BASE_URL}/${id}`);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getFilterFunctions = async () => {
|
||||
const response = await axiosClient.get<Function[]>(`${BASE_URL}/functions`);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getResourceFunctions = async () => {
|
||||
const response = await axiosClient.get<
|
||||
PagingResponse<FilterResourceDescriptor[]>
|
||||
>(`${BASE_URL}/resources`);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const triggerEventById = async (id: string) => {
|
||||
const response = await axiosClient.put<EventSubscription>(
|
||||
`${BASE_URL}/trigger/${id}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@ -25,15 +25,15 @@ import { SubscriptionType } from '../../generated/events/eventSubscription';
|
||||
|
||||
export const getAlertsActionTypeIcon = (type?: SubscriptionType) => {
|
||||
switch (type) {
|
||||
case SubscriptionType.SlackWebhook:
|
||||
case SubscriptionType.Slack:
|
||||
return <SlackIcon height={16} width={16} />;
|
||||
case SubscriptionType.MSTeamsWebhook:
|
||||
case SubscriptionType.MSTeams:
|
||||
return <MSTeamsIcon height={16} width={16} />;
|
||||
case SubscriptionType.Email:
|
||||
return <MailIcon height={16} width={16} />;
|
||||
case SubscriptionType.ActivityFeed:
|
||||
return <AllActivityIcon height={16} width={16} />;
|
||||
case SubscriptionType.GenericWebhook:
|
||||
case SubscriptionType.Generic:
|
||||
default:
|
||||
return <WebhookIcon height={16} width={16} />;
|
||||
}
|
||||
@ -115,13 +115,13 @@ export const getAlertActionTypeDisplayName = (
|
||||
return i18next.t('label.activity-feed-plural');
|
||||
case SubscriptionType.Email:
|
||||
return i18next.t('label.email');
|
||||
case SubscriptionType.GenericWebhook:
|
||||
case SubscriptionType.Generic:
|
||||
return i18next.t('label.webhook');
|
||||
case SubscriptionType.SlackWebhook:
|
||||
case SubscriptionType.Slack:
|
||||
return i18next.t('label.slack');
|
||||
case SubscriptionType.MSTeamsWebhook:
|
||||
case SubscriptionType.MSTeams:
|
||||
return i18next.t('label.ms-team-plural');
|
||||
case SubscriptionType.GChatWebhook:
|
||||
case SubscriptionType.GChat:
|
||||
return i18next.t('label.g-chat');
|
||||
default:
|
||||
return '';
|
||||
|
||||
@ -239,6 +239,12 @@ export const getGlobalSettingsMenuWithPermission = (
|
||||
key: 'notifications.alerts',
|
||||
icon: <BellIcon className="side-panel-icons" />,
|
||||
},
|
||||
{
|
||||
label: i18next.t('label.observability'),
|
||||
isProtected: Boolean(isAdminUser),
|
||||
key: 'notifications.observability',
|
||||
icon: <BellIcon className="side-panel-icons" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user