From f2fec4df1079fa596d4e5b8ba06ecadc4dd1229f Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Tue, 5 Aug 2025 10:09:00 +0530 Subject: [PATCH] Supported Contract Execution Chart Summary Card (#22735) * Supported the contract execution chart card in Contract Page * pending localization keys * minor improvenent around message * localization keys * decrease the chart size and fix some localizaion keys * added contract in persona tab and added beta lable in table contract tab and preference data asset rule tab * fix some styling around form (cherry picked from commit 98276fe8adca2da359e829022be3977fa8bbc345) --- .../ui/src/assets/svg/table-outline.svg | 4 + .../AddDataContract/AddDataContract.tsx | 7 +- .../AddDataContract/add-data-contract.less | 42 +++- .../ContractDetailFormTab.tsx | 11 +- .../ContractDetailTab/ContractDetail.tsx | 125 +++++++----- .../ContractDetailTab/contract-detail.less | 37 +++- .../ContractDetailTab/contract.interface.ts | 25 +++ .../ContractExecutionChart.component.tsx | 185 ++++++++++++++++++ .../contract-execution-chart.less | 19 ++ .../ContractScehmaFormTab.tsx | 5 +- .../ContractSemanticFormTab.tsx | 6 +- .../contract-semantic-form-tab.less | 46 +++++ .../ui/src/locale/languages/de-de.json | 3 + .../ui/src/locale/languages/en-us.json | 3 + .../ui/src/locale/languages/es-es.json | 3 + .../ui/src/locale/languages/fr-fr.json | 3 + .../ui/src/locale/languages/gl-es.json | 3 + .../ui/src/locale/languages/he-he.json | 3 + .../ui/src/locale/languages/ja-jp.json | 3 + .../ui/src/locale/languages/ko-kr.json | 3 + .../ui/src/locale/languages/mr-in.json | 3 + .../ui/src/locale/languages/nl-nl.json | 3 + .../ui/src/locale/languages/pr-pr.json | 3 + .../ui/src/locale/languages/pt-br.json | 3 + .../ui/src/locale/languages/pt-pt.json | 3 + .../ui/src/locale/languages/ru-ru.json | 3 + .../ui/src/locale/languages/th-th.json | 3 + .../ui/src/locale/languages/tr-tr.json | 3 + .../ui/src/locale/languages/zh-cn.json | 3 + .../main/resources/ui/src/rest/contractAPI.ts | 20 +- .../resources/ui/src/styles/variables.less | 2 + .../ui/src/utils/GlobalSettingsClassBase.ts | 1 + .../resources/ui/src/utils/TableClassBase.ts | 1 + .../resources/ui/src/utils/TableUtils.tsx | 1 + 34 files changed, 518 insertions(+), 70 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/table-outline.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/ContractExecutionChart.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/contract-execution-chart.less diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/table-outline.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/table-outline.svg new file mode 100644 index 00000000000..337e36f1433 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/table-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx index bbc726303a2..84c86b0c77d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx @@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as ContractIcon } from '../../../assets/svg/ic-contract.svg'; import { ReactComponent as QualityIcon } from '../../../assets/svg/policies.svg'; import { ReactComponent as SemanticsIcon } from '../../../assets/svg/semantics.svg'; -import { ReactComponent as TableIcon } from '../../../assets/svg/table-grey.svg'; +import { ReactComponent as TableIcon } from '../../../assets/svg/table-outline.svg'; import { DataContractMode, EDataContractTab, @@ -220,7 +220,10 @@ const AddDataContract: React.FC<{
-
-
+
+ +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx index 550e6251853..2ca3cfeaf20 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx @@ -28,6 +28,7 @@ import classNames from 'classnames'; import { isEmpty } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; import { Cell, Pie, PieChart } from 'recharts'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new-thick.svg'; import { ReactComponent as EmptyContractIcon } from '../../../assets/svg/empty-contract.svg'; @@ -67,6 +68,10 @@ import { } from '../../../utils/DataContract/DataContractUtils'; import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils'; import { getEntityName } from '../../../utils/EntityUtils'; +import { + getTestCaseDetailPagePath, + getTestSuitePath, +} from '../../../utils/RouterUtils'; import { pruneEmptyChildren } from '../../../utils/TableUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import AlertBar from '../../AlertBar/AlertBar'; @@ -78,6 +83,7 @@ import RichTextEditorPreviewerNew from '../../common/RichTextEditor/RichTextEdit import { StatusType } from '../../common/StatusBadge/StatusBadge.interface'; import StatusBadgeV2 from '../../common/StatusBadge/StatusBadgeV2.component'; import Table from '../../common/Table/Table'; +import ContractExecutionChart from '../ContractExecutionChart/ContractExecutionChart.component'; import ContractViewSwitchTab from '../ContractViewSwitchTab/ContractViewSwitchTab.component'; import ContractYaml from '../ContractYaml/ContractYaml.component'; import './contract-detail.less'; @@ -254,16 +260,18 @@ const ContractDetail: React.FC<{ downloadContractYamlFile(contract); }, [contract]); - const handleRunNow = () => { + const handleRunNow = async () => { if (contract?.id) { - setValidateLoading(true); - validateContractById(contract.id) - .then(() => - showSuccessToast('Contract validation trigger successfully.') - ) - .finally(() => { - setValidateLoading(false); - }); + try { + setValidateLoading(true); + await validateContractById(contract.id); + showSuccessToast(t('message.contract-validation-trigger-successfully')); + fetchLatestContractResults(); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setValidateLoading(false); + } } }; @@ -624,42 +632,51 @@ const ContractDetail: React.FC<{ ) : (
{showTestCaseSummaryChart && ( -
- {testCaseSummaryChartItems.map((item) => ( -
- - {item.label} - + +
+ {testCaseSummaryChartItems.map((item) => ( +
+ + {item.label} + - - - {item.chartData.map((entry, index) => ( - - ))} - - - {item.value} - - -
- ))} -
+ + + {item.chartData.map((entry, index) => ( + + ))} + + + {item.value} + + +
+ ))} +
{' '} + )} @@ -670,9 +687,16 @@ const ContractDetail: React.FC<{ key={item.id}> {getTestCaseStatusIcon(item)}
- - {item.name} - + + + {item.name} + + + )} + + {/* Contract Execution Chart */} + {contract.id && contract.latestResult?.resultId && ( + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less index f42a024a0cc..b7980af644f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less @@ -99,6 +99,14 @@ font-size: 14px; font-weight: 500; } + + &.success { + border-color: @green-16; + } + + &.version { + border-color: @purple-7; + } } } } @@ -214,6 +222,16 @@ } .data-quality-card-container { + .data-quality-chart-pie-chart { + cursor: pointer !important; + } + + .data-quality-chart-container-link { + text-decoration: none; + color: @grey-900; + cursor: pointer; + } + .chart-label { font-size: 16px; font-weight: 600; @@ -252,12 +270,21 @@ .data-quality-item-content { display: flex; flex-direction: column; - } - .data-quality-item-name { - font-size: 14px; - font-weight: 600; - color: @grey-900; + .data-quality-item-name-link { + text-decoration: none; + cursor: pointer; + + .data-quality-item-name { + font-size: 14px; + font-weight: 600; + color: @grey-900; + + &:hover { + color: @primary-color; + } + } + } } .data-quality-item-description { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract.interface.ts new file mode 100644 index 00000000000..22d95d8e6b2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract.interface.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 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 { DataContractResult } from '../../../generated/entity/datacontract/dataContractResult'; +import { Paging } from '../../../generated/type/paging'; + +export interface ContractAllResult { + data: DataContractResult[]; + paging: Paging; +} + +export interface ContractResultFilter { + startTs: number; + endTs: number; + limit?: number; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/ContractExecutionChart.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/ContractExecutionChart.component.tsx new file mode 100644 index 00000000000..05b33bcef65 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/ContractExecutionChart.component.tsx @@ -0,0 +1,185 @@ +/* + * Copyright 2025 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 { Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import { isEqual, pick } from 'lodash'; +import { DateRangeObject } from 'Models'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + Rectangle, + ResponsiveContainer, + XAxis, +} from 'recharts'; +import { GREEN_3, RED_3, YELLOW_2 } from '../../../constants/Color.constants'; +import { PROFILER_FILTER_RANGE } from '../../../constants/profiler.constant'; +import { DataContract } from '../../../generated/entity/data/dataContract'; +import { DataContractResult } from '../../../generated/entity/datacontract/dataContractResult'; +import { ContractExecutionStatus } from '../../../generated/type/contractExecutionStatus'; +import { getAllContractResults } from '../../../rest/contractAPI'; +import { + formatDateTime, + getCurrentMillis, + getEpochMillisForPastDays, +} from '../../../utils/date-time/DateTimeUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import DatePickerMenu from '../../common/DatePickerMenu/DatePickerMenu.component'; +import ExpandableCard from '../../common/ExpandableCard/ExpandableCard'; +import Loader from '../../common/Loader/Loader'; +import './contract-execution-chart.less'; + +const ContractExecutionChart = ({ contract }: { contract: DataContract }) => { + const { t } = useTranslation(); + const defaultRange = useMemo( + () => ({ + initialRange: { + startTs: getEpochMillisForPastDays( + PROFILER_FILTER_RANGE.last30days.days + ), + endTs: getCurrentMillis(), + }, + key: 'last30days', + title: PROFILER_FILTER_RANGE.last30days.title, + }), + [] + ); + + const [contractExecutionResultList, setContractExecutionResultList] = + useState([]); + const [isLoading, setIsLoading] = useState(true); + + const [dateRangeObject, setDateRangeObject] = useState( + defaultRange.initialRange + ); + + const fetchAllContractResults = async (dateRangeObj: DateRangeObject) => { + try { + setIsLoading(true); + const results = await getAllContractResults(contract.id, { + ...pick(dateRangeObj, ['startTs', 'endTs']), + }); + setContractExecutionResultList(results.data); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsLoading(false); + } + }; + + const processedChartData = useMemo(() => { + return contractExecutionResultList.map((item) => { + return { + name: item.timestamp, + failed: + item.contractExecutionStatus === ContractExecutionStatus.Failed + ? 1 + : 0, + success: + item.contractExecutionStatus === ContractExecutionStatus.Success + ? 1 + : 0, + aborted: + item.contractExecutionStatus === ContractExecutionStatus.Aborted + ? 1 + : 0, + }; + }); + }, [contractExecutionResultList]); + + const handleDateRangeChange = (value: DateRangeObject) => { + if (!isEqual(value, dateRangeObject)) { + setDateRangeObject(value); + } + }; + + useEffect(() => { + fetchAllContractResults(dateRangeObject); + }, [dateRangeObject]); + + return ( + + + {t('label.contract-execution-history')} + + + {t('message.contract-execution-history-description')} + +
+ ), + }}> +
+ {isLoading ? ( + + ) : ( + <> + + + + + + + + + } + dataKey="success" + fill={GREEN_3} + name={t('label.success')} + /> + } + dataKey="failed" + fill={RED_3} + name={t('label.failed')} + /> + } + dataKey="aborted" + fill={YELLOW_2} + name={t('label.aborted')} + /> + + + + )} +
+ + ); +}; + +export default ContractExecutionChart; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/contract-execution-chart.less b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/contract-execution-chart.less new file mode 100644 index 00000000000..cf58ccb6b07 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractExecutionChart/contract-execution-chart.less @@ -0,0 +1,19 @@ +/* + * Copyright 2025 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. + */ +.contract-execution-chart-container { + height: 360px; + display: flex; + flex-direction: column; + gap: 16px; + align-items: end; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx index be0c51da64d..a433a1a6f35 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx @@ -10,13 +10,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Icon from '@ant-design/icons'; import { Button, Card, Tag, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { isEmpty, pick } from 'lodash'; import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as LeftOutlined } from '../../../assets/svg/left-arrow.svg'; -import { ReactComponent as RightOutlined } from '../../../assets/svg/right-arrow.svg'; +import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { NO_DATA_PLACEHOLDER, @@ -313,7 +314,7 @@ export const ContractSchemaFormTab: React.FC<{ type="primary" onClick={onNext}> {nextLabel ?? t('label.next')} - +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx index 212bf3fb0ef..2f475d05f85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx @@ -23,7 +23,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg'; import { ReactComponent as LeftOutlined } from '../../../assets/svg/left-arrow.svg'; -import { ReactComponent as RightOutlined } from '../../../assets/svg/right-arrow.svg'; +import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg'; import { EntityType } from '../../../enums/entity.enum'; import { DataContract } from '../../../generated/entity/data/dataContract'; @@ -225,7 +225,7 @@ export const ContractSemanticFormTab: React.FC<{ {...field} label={t('label.description')} name={[field.name, 'description']}> -