-
Data Insight
+
+ {t('label.data-insight-plural')}
+
{t('label.data-insight-subtitle')}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx
new file mode 100644
index 00000000000..ed35bab951d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { act } from 'react-test-renderer';
+import KPIList from './KPIList';
+import { KPI_DATA } from './mocks/KPIList';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn().mockImplementation(() => ({
+ push: mockPush,
+ })),
+ Link: jest
+ .fn()
+ .mockImplementation(({ children }: { children: React.ReactNode }) => (
+
{children}
+ )),
+}));
+
+jest.mock('../../components/common/DeleteWidget/DeleteWidgetModal', () =>
+ jest.fn().mockReturnValue(
Delete Modal
)
+);
+
+jest.mock('../../components/common/next-previous/NextPrevious', () =>
+ jest
+ .fn()
+ .mockReturnValue(
Next Previous
)
+);
+
+jest.mock(
+ '../../components/common/rich-text-editor/RichTextEditorPreviewer',
+ () => jest.fn().mockReturnValue(
Editor
)
+);
+
+jest.mock('../../components/Loader/Loader', () =>
+ jest.fn().mockReturnValue(
Loader
)
+);
+
+jest.mock('../../hooks/authHooks', () => ({
+ useAuth: jest.fn().mockReturnValue({ isAdminUser: true }),
+}));
+
+jest.mock('../../utils/TimeUtils', () => ({
+ formatDateTime: jest.fn().mockReturnValue('7 Dec 2022, 00:00'),
+}));
+
+jest.mock('../../axiosAPIs/KpiAPI', () => ({
+ getListKPIs: jest
+ .fn()
+ .mockImplementation(() => Promise.resolve({ data: KPI_DATA })),
+}));
+
+describe('KPI list component', () => {
+ it('Should render the kpi list', async () => {
+ render(
, { wrapper: MemoryRouter });
+
+ const container = await screen.findByTestId('kpi-table');
+ const descriptionKPI = await screen.findByText('Description KPI');
+ const ownerKPI = await screen.findByText('Owner KPI');
+
+ expect(container).toBeInTheDocument();
+
+ expect(descriptionKPI).toBeInTheDocument();
+ expect(ownerKPI).toBeInTheDocument();
+ });
+
+ it('Action button should work', async () => {
+ const KPI = KPI_DATA[0];
+
+ render(
, { wrapper: MemoryRouter });
+
+ const editButton = await screen.findByTestId(
+ `edit-action-${KPI.displayName}`
+ );
+ const deleteButton = await screen.findByTestId(
+ `delete-action-${KPI.displayName}`
+ );
+
+ expect(editButton).toBeInTheDocument();
+ expect(deleteButton).toBeInTheDocument();
+
+ await act(async () => {
+ fireEvent.click(editButton);
+ });
+
+ expect(mockPush).toBeCalled();
+
+ await act(async () => {
+ fireEvent.click(deleteButton);
+ });
+
+ expect(await screen.findByTestId('delete-modal')).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx
index 1ddf9a4f3a1..fe732a717bb 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx
@@ -11,12 +11,12 @@
* limitations under the License.
*/
-import { Button, Col, Table, Tooltip, Typography } from 'antd';
+import { Button, Col, Space, Table, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Link } from 'react-router-dom';
+import { Link, useHistory } from 'react-router-dom';
import { getListKPIs } from '../../axiosAPIs/KpiAPI';
import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal';
import NextPrevious from '../../components/common/next-previous/NextPrevious';
@@ -28,7 +28,6 @@ import {
PAGE_SIZE_MEDIUM,
pagingObject,
} from '../../constants/constants';
-import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { EntityType } from '../../enums/entity.enum';
import { Kpi, KpiTargetType } from '../../generated/dataInsight/kpi/kpi';
@@ -39,6 +38,7 @@ import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { formatDateTime } from '../../utils/TimeUtils';
const KPIList = () => {
+ const history = useHistory();
const { isAdminUser } = useAuth();
const { t } = useTranslation();
const [kpiList, setKpiList] = useState
>([]);
@@ -122,21 +122,46 @@ const KPIList = () => {
key: 'actions',
render: (_, record) => {
return (
-
-
- }
- type="text"
- onClick={() => setSelectedKpi(record)}
- />
-
+
+
+
+ }
+ type="text"
+ onClick={() => history.push(getKpiPath(record.name))}
+ />
+
+
+
+ }
+ type="text"
+ onClick={() => setSelectedKpi(record)}
+ />
+
+
);
},
},
@@ -165,6 +190,7 @@ const KPIList = () => {
}}
pagination={false}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/mocks/KPIList.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/mocks/KPIList.ts
new file mode 100644
index 00000000000..bf6a759928a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/mocks/KPIList.ts
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+export const KPI_DATA = [
+ {
+ id: 'dabd01bb-095d-448e-af21-0427859b99b5',
+ name: 'description-kpi',
+ displayName: 'Description KPI',
+ fullyQualifiedName: 'description-kpi',
+ description: '',
+ metricType: 'PERCENTAGE',
+ targetDefinition: [
+ {
+ name: 'completedDescriptionFraction',
+ value: '0.65',
+ },
+ ],
+ startDate: 1670351400000,
+ endDate: 1672165800000,
+ version: 0.2,
+ updatedAt: 1670414685805,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/kpi/dabd01bb-095d-448e-af21-0427859b99b5',
+ changeDescription: {
+ fieldsAdded: [],
+ fieldsUpdated: [
+ {
+ name: 'startDate',
+ oldValue: 1670395661000,
+ newValue: 1670351400000,
+ },
+ {
+ name: 'endDate',
+ oldValue: 1672210072000,
+ newValue: 1672165800000,
+ },
+ ],
+ fieldsDeleted: [],
+ previousVersion: 0.1,
+ },
+ deleted: false,
+ },
+ {
+ id: 'bb71e000-f837-4e3c-8e03-0a0f1fd35667',
+ name: 'owner-kpi-has-owner-fraction',
+ displayName: 'Owner KPI',
+ fullyQualifiedName: 'owner-kpi-has-owner-fraction',
+ description: '',
+ metricType: 'PERCENTAGE',
+ targetDefinition: [
+ {
+ name: 'hasOwnerFraction',
+ value: '0.64',
+ },
+ ],
+ startDate: 1670351400000,
+ endDate: 1672511340000,
+ version: 0.2,
+ updatedAt: 1670415195507,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/kpi/bb71e000-f837-4e3c-8e03-0a0f1fd35667',
+ changeDescription: {
+ fieldsAdded: [],
+ fieldsUpdated: [
+ {
+ name: 'endDate',
+ oldValue: 1672252140000,
+ newValue: 1672511340000,
+ },
+ ],
+ fieldsDeleted: [],
+ previousVersion: 0.1,
+ },
+ deleted: false,
+ },
+];
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx
new file mode 100644
index 00000000000..8eae79f4392
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.test.tsx
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ act,
+ findByRole,
+ fireEvent,
+ render,
+ screen,
+ waitForElement,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+
+import AddKPIPage from './AddKPIPage';
+
+import { KPI_CHARTS, KPI_DATA, KPI_LIST } from './KPIMock.mock';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn().mockReturnValue({
+ push: mockPush,
+ }),
+}));
+
+jest.mock('../../axiosAPIs/DataInsightAPI', () => ({
+ getListDataInsightCharts: jest
+ .fn()
+ .mockImplementation(() => Promise.resolve({ data: KPI_CHARTS })),
+}));
+
+jest.mock('../../components/common/rich-text-editor/RichTextEditor', () =>
+ jest.fn().mockReturnValue(Editor
)
+);
+
+jest.mock(
+ '../../components/common/title-breadcrumb/title-breadcrumb.component',
+ () =>
+ jest.fn().mockReturnValue(BreadCrumb
)
+);
+
+jest.mock('../../axiosAPIs/KpiAPI', () => ({
+ getListKPIs: jest
+ .fn()
+ .mockImplementation(() => Promise.resolve({ data: KPI_LIST })),
+ postKPI: jest.fn().mockImplementation(() => Promise.resolve(KPI_DATA)),
+}));
+
+jest.mock('../../utils/CommonUtils', () => ({
+ isUrlFriendlyName: jest.fn().mockReturnValue(true),
+}));
+
+jest.mock('../../utils/DataInsightUtils', () => ({
+ getKpiTargetValueByMetricType: jest.fn().mockReturnValue(10),
+ getKPIFormattedDates: jest.fn().mockReturnValue({
+ startDate: `2022-12-08 00:00`,
+ endDate: `2022-12-28 23:59`,
+ }),
+ getDisabledDates: jest.fn().mockReturnValue(true),
+}));
+
+describe('Add KPI page', () => {
+ it('Should render all the components', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const container = await screen.findByTestId('add-kpi-container');
+ const breadCrumb = await screen.findByTestId('breadcrumb');
+ const formTitle = await screen.findByTestId('form-title');
+ const rightPanel = await screen.findByTestId('right-panel');
+
+ expect(container).toBeInTheDocument();
+
+ expect(breadCrumb).toBeInTheDocument();
+
+ expect(formTitle).toBeInTheDocument();
+
+ expect(formTitle.textContent).toContain('label.add-new-kpi');
+
+ const formContainer = await screen.findByTestId('kpi-form');
+
+ expect(formContainer).toBeInTheDocument();
+
+ expect(rightPanel).toBeInTheDocument();
+ });
+
+ it('Should render all the form fields', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const formContainer = await screen.findByTestId('kpi-form');
+
+ const chart = await screen.findByTestId('dataInsightChart');
+ const displayName = await screen.findByTestId('displayName');
+ const metricType = await screen.findByTestId('metricType');
+ const startDate = await screen.findByTestId('start-date');
+ const endDate = await screen.findByTestId('end-date');
+ const editor = await screen.findByTestId('editor');
+ const cancelButton = await screen.findByTestId('cancel-btn');
+ const submitButton = await screen.findByTestId('submit-btn');
+
+ expect(formContainer).toBeInTheDocument();
+ expect(chart).toBeInTheDocument();
+ expect(displayName).toBeInTheDocument();
+ expect(metricType).toBeInTheDocument();
+ expect(startDate).toBeInTheDocument();
+ expect(endDate).toBeInTheDocument();
+ expect(editor).toBeInTheDocument();
+ expect(cancelButton).toBeInTheDocument();
+ expect(submitButton).toBeInTheDocument();
+ });
+
+ it('Metric type input should be disable if chart is not selected', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const chart = await screen.findByTestId('dataInsightChart');
+
+ const metricType = await screen.findByTestId('metricType');
+
+ expect(chart).toBeInTheDocument();
+
+ expect(metricType).toHaveClass('ant-select-disabled');
+ });
+
+ it('Metric type input should not be disable if chart is selected', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const chart = await screen.findByTestId('dataInsightChart');
+
+ const chartInput = await findByRole(chart, 'combobox');
+
+ const metricType = await screen.findByTestId('metricType');
+
+ act(() => {
+ userEvent.click(chartInput);
+ });
+
+ await waitForElement(() =>
+ screen.getByText('Percentage of Entities With Owner')
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Percentage of Entities With Owner'));
+ });
+
+ expect(chart).toBeInTheDocument();
+ expect(metricType).not.toHaveClass('ant-select-disabled');
+ });
+
+ it('Should render the proper metric input based on metric type', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const chart = await screen.findByTestId('dataInsightChart');
+
+ const chartInput = await findByRole(chart, 'combobox');
+
+ const metricType = await screen.findByTestId('metricType');
+
+ const metricInput = await findByRole(metricType, 'combobox');
+
+ act(() => {
+ userEvent.click(chartInput);
+ });
+
+ await waitForElement(() =>
+ screen.getByText('Percentage of Entities With Owner')
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Percentage of Entities With Owner'));
+ });
+
+ act(() => {
+ userEvent.click(metricInput);
+ });
+
+ // check for percentage type
+ await waitForElement(() =>
+ screen.getByText('hasOwnerFraction (PERCENTAGE)')
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('hasOwnerFraction (PERCENTAGE)'));
+ });
+
+ expect(
+ await screen.findByTestId('metric-percentage-input')
+ ).toBeInTheDocument();
+
+ // check for number type
+ await waitForElement(() => screen.getByText('hasOwner (NUMBER)'));
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('hasOwner (NUMBER)'));
+ });
+
+ expect(
+ await screen.findByTestId('metric-number-input')
+ ).toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.tsx
index 618cabed282..afc0965b459 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/AddKPIPage.tsx
@@ -27,7 +27,7 @@ import {
Typography,
} from 'antd';
import { AxiosError } from 'axios';
-import { isUndefined } from 'lodash';
+import { isUndefined, kebabCase } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
@@ -46,7 +46,6 @@ import {
VALIDATE_MESSAGES,
} from '../../constants/DataInsight.constants';
import { ADD_KPI_TEXT } from '../../constants/HelperTextUtil';
-import { nameWithSpace } from '../../constants/regex.constants';
import { EntityType } from '../../enums/entity.enum';
import {
CreateKpiRequest,
@@ -60,9 +59,9 @@ import {
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { Kpi } from '../../generated/dataInsight/kpi/kpi';
import { KpiDate, KpiDates } from '../../interface/data-insight.interface';
-import { isUrlFriendlyName } from '../../utils/CommonUtils';
import {
getDisabledDates,
+ getKPIFormattedDates,
getKpiTargetValueByMetricType,
} from '../../utils/DataInsightUtils';
import { getTimeStampByDateTime } from '../../utils/TimeUtils';
@@ -164,8 +163,10 @@ const AddKPIPage = () => {
};
const handleSubmit: FormProps['onFinish'] = async (values) => {
- const startDate = getTimeStampByDateTime(kpiDates.startDate);
- const endDate = getTimeStampByDateTime(kpiDates.endDate);
+ const formattedDates = getKPIFormattedDates(kpiDates);
+
+ const startDate = getTimeStampByDateTime(formattedDates.startDate);
+ const endDate = getTimeStampByDateTime(formattedDates.endDate);
const metricType =
selectedMetric?.chartDataType as unknown as KpiTargetType;
@@ -177,7 +178,7 @@ const AddKPIPage = () => {
type: EntityType.DATA_INSIGHT_CHART,
},
description,
- name: values.name,
+ name: kebabCase(`${values.displayName} ${selectedMetric?.name}`),
displayName: values.displayName,
startDate,
endDate,
@@ -211,8 +212,8 @@ const AddKPIPage = () => {
data-testid="add-kpi-container"
gutter={[16, 16]}>
-
-
+
+
{t('label.add-new-kpi')}
@@ -222,43 +223,6 @@ const AddKPIPage = () => {
layout="vertical"
validateMessages={VALIDATE_MESSAGES}
onFinish={handleSubmit}>
- {
- if (
- !isUrlFriendlyName(value) ||
- nameWithSpace.test(value)
- ) {
- return Promise.reject(
- t('label.special-character-not-allowed')
- );
- }
-
- return Promise.resolve();
- },
- },
- ]}>
-
-
-
-
-
-
-
{
+
+
+
+
{
<>
{selectedMetric.chartDataType ===
ChartDataType.Percentage && (
-
+
{
{selectedMetric.chartDataType === ChartDataType.Number && (
setMetricValue(Number(value))}
@@ -377,11 +350,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
+ message: t('label.field-required', {
+ field: t('label.start-date'),
+ }),
},
]}>
@@ -398,11 +374,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
+ message: t('label.field-required', {
+ field: t('label.end-date'),
+ }),
},
]}>
@@ -442,7 +421,7 @@ const AddKPIPage = () => {
-
+
{t('label.add-kpi')}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx
new file mode 100644
index 00000000000..4217051389d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.test.tsx
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+
+import EditKPIPage from './EditKPIPage';
+
+import { DESCRIPTION_CHART, KPI_DATA } from './KPIMock.mock';
+
+const mockPush = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn().mockReturnValue({
+ push: mockPush,
+ }),
+ useParams: jest.fn().mockReturnValue({ useParams: 'description-kpi' }),
+}));
+
+jest.mock('../../axiosAPIs/DataInsightAPI', () => ({
+ getChartById: jest
+ .fn()
+ .mockImplementation(() => Promise.resolve(DESCRIPTION_CHART)),
+}));
+
+jest.mock('../../components/common/rich-text-editor/RichTextEditor', () =>
+ jest.fn().mockReturnValue(Editor
)
+);
+
+jest.mock(
+ '../../components/common/title-breadcrumb/title-breadcrumb.component',
+ () =>
+ jest.fn().mockReturnValue(BreadCrumb
)
+);
+
+jest.mock('../../components/Loader/Loader', () =>
+ jest.fn().mockReturnValue(Loader
)
+);
+
+jest.mock('../../axiosAPIs/KpiAPI', () => ({
+ getKPIByName: jest.fn().mockImplementation(() => Promise.resolve(KPI_DATA)),
+ patchKPI: jest.fn().mockImplementation(() => Promise.resolve(KPI_DATA)),
+}));
+
+jest.mock('../../hooks/authHooks', () => ({
+ useAuth: jest.fn().mockReturnValue({ isAdminUser: true }),
+}));
+
+jest.mock('../../utils/DataInsightUtils', () => ({
+ getKpiTargetValueByMetricType: jest.fn().mockReturnValue(10),
+ getKPIFormattedDates: jest.fn().mockReturnValue({
+ startDate: `2022-12-08 00:00`,
+ endDate: `2022-12-28 23:59`,
+ }),
+ getDisabledDates: jest.fn().mockReturnValue(true),
+ getKpiDateFormatByTimeStamp: jest.fn().mockReturnValue('2022-12-08'),
+}));
+
+describe('Edit KPI page', () => {
+ it('Should render all the components', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const container = await screen.findByTestId('edit-kpi-container');
+ const breadCrumb = await screen.findByTestId('breadcrumb');
+ const formTitle = await screen.findByTestId('form-title');
+ const rightPanel = await screen.findByTestId('right-panel');
+
+ expect(container).toBeInTheDocument();
+
+ expect(breadCrumb).toBeInTheDocument();
+
+ expect(formTitle).toBeInTheDocument();
+
+ expect(formTitle.textContent).toContain('label.edit-entity');
+
+ const formContainer = await screen.findByTestId('kpi-form');
+
+ expect(formContainer).toBeInTheDocument();
+
+ expect(rightPanel).toBeInTheDocument();
+ });
+
+ it('Should render all the form fields', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const formContainer = await screen.findByTestId('kpi-form');
+
+ const chart = await screen.findByTestId('dataInsightChart');
+ const displayName = await screen.findByTestId('displayName');
+ const metricType = await screen.findByTestId('metricType');
+ const startDate = await screen.findByTestId('start-date');
+ const endDate = await screen.findByTestId('end-date');
+ const editor = await screen.findByTestId('editor');
+ const cancelButton = await screen.findByTestId('cancel-btn');
+ const submitButton = await screen.findByTestId('submit-btn');
+
+ expect(formContainer).toBeInTheDocument();
+ expect(chart).toBeInTheDocument();
+ expect(displayName).toBeInTheDocument();
+ expect(metricType).toBeInTheDocument();
+ expect(startDate).toBeInTheDocument();
+ expect(endDate).toBeInTheDocument();
+ expect(editor).toBeInTheDocument();
+ expect(cancelButton).toBeInTheDocument();
+ expect(submitButton).toBeInTheDocument();
+ });
+
+ it('Chart input and Metric type input should be disable for edit form', async () => {
+ render( , { wrapper: MemoryRouter });
+
+ const chart = await screen.findByTestId('dataInsightChart');
+
+ const metricType = await screen.findByTestId('metricType');
+
+ expect(chart).toHaveClass('ant-input-disabled');
+
+ expect(metricType).toHaveClass('ant-input-disabled');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.tsx
index 7aaaa1b2d95..641c0ae7ea0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/EditKPIPage.tsx
@@ -47,10 +47,7 @@ import {
KPI_DATE_PICKER_FORMAT,
VALIDATE_MESSAGES,
} from '../../constants/DataInsight.constants';
-import {
- ADD_KPI_TEXT,
- NO_PERMISSION_FOR_ACTION,
-} from '../../constants/HelperTextUtil';
+import { ADD_KPI_TEXT } from '../../constants/HelperTextUtil';
import { DataInsightChart } from '../../generated/dataInsight/dataInsightChart';
import { Kpi, KpiTargetType } from '../../generated/dataInsight/kpi/kpi';
import { useAuth } from '../../hooks/authHooks';
@@ -58,12 +55,13 @@ import { KpiDate, KpiDates } from '../../interface/data-insight.interface';
import {
getDisabledDates,
getKpiDateFormatByTimeStamp,
+ getKPIFormattedDates,
getKpiTargetValueByMetricType,
} from '../../utils/DataInsightUtils';
import { getTimeStampByDateTime } from '../../utils/TimeUtils';
import { showErrorToast } from '../../utils/ToastUtils';
-const AddKPIPage = () => {
+const EditKPIPage = () => {
const { isAdminUser } = useAuth();
const { kpiName } = useParams<{ kpiName: string }>();
@@ -164,8 +162,10 @@ const AddKPIPage = () => {
const handleSubmit: FormProps['onFinish'] = async (values) => {
if (kpiData && metricData) {
- const startDate = getTimeStampByDateTime(kpiDates.startDate);
- const endDate = getTimeStampByDateTime(kpiDates.endDate);
+ const formattedDates = getKPIFormattedDates(kpiDates);
+
+ const startDate = getTimeStampByDateTime(formattedDates.startDate);
+ const endDate = getTimeStampByDateTime(formattedDates.endDate);
const targetValue = getKpiTargetValueByMetricType(
kpiData.metricType,
@@ -234,15 +234,15 @@ const AddKPIPage = () => {
{kpiData ? (
-
-
+
+
- {t('label.add-new-kpi')}
+ {t('label.edit-entity', { entity: t('label.kpi-uppercase') })}
+
@@ -268,19 +269,14 @@ const AddKPIPage = () => {
/>
-
+
-
-
-
-
{!isUndefined(metricData) && (
{
]}>
<>
{kpiData?.metricType === KpiTargetType.Percentage && (
-
+
{
{kpiData?.metricType === KpiTargetType.Number && (
setMetricValue(Number(value))}
@@ -354,11 +351,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
+ message: t('label.field-required', {
+ field: t('label.start-date'),
+ }),
},
]}>
@@ -375,11 +375,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
+ message: t('label.field-required', {
+ field: t('label.end-date'),
+ }),
},
]}>
@@ -409,7 +412,9 @@ const AddKPIPage = () => {
{
-
+
{t('label.edit-entity', { entity: t('label.kpi-uppercase') })}
@@ -441,4 +446,4 @@ const AddKPIPage = () => {
);
};
-export default AddKPIPage;
+export default EditKPIPage;
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIMock.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIMock.mock.ts
new file mode 100644
index 00000000000..5cd99b85764
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/KPIPage/KPIMock.mock.ts
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const KPI_CHARTS = [
+ {
+ id: 'd2f093d4-0ca8-42b8-8721-1c2a59951b59',
+ name: 'dailyActiveUsers',
+ displayName: 'Daily active users on the platform',
+ fullyQualifiedName: 'dailyActiveUsers',
+ description: 'Display the number of users active.',
+ dataIndexType: 'web_analytic_user_activity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ ],
+ metrics: [
+ {
+ name: 'activeUsers',
+ displayName: 'Number of active users',
+ chartDataType: 'NUMBER',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952802,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/d2f093d4-0ca8-42b8-8721-1c2a59951b59',
+ deleted: false,
+ },
+ {
+ id: 'fbad142d-16d5-479d-bed3-67bb5fd4104d',
+ name: 'mostActiveUsers',
+ displayName: 'Most Active Users',
+ fullyQualifiedName: 'mostActiveUsers',
+ description:
+ 'Displays the most active users on the platform based on page views.',
+ dataIndexType: 'web_analytic_user_activity_report_data_index',
+ dimensions: [
+ {
+ name: 'userName',
+ chartDataType: 'STRING',
+ },
+ {
+ name: 'team',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'lastSession',
+ displayName: 'Last time the user visited the platform',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'sessions',
+ displayName: 'Total number of sessions',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'avgSessionDuration',
+ displayName: 'The average duration time of a session',
+ chartDataType: 'FLOAT',
+ },
+ {
+ name: 'pageViews',
+ displayName: 'Total number of page view',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952821,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/fbad142d-16d5-479d-bed3-67bb5fd4104d',
+ deleted: false,
+ },
+ {
+ id: '9217560a-3fed-4c0d-85fa-d7c699feefac',
+ name: 'mostViewedEntities',
+ displayName: 'Most Viewed entites',
+ fullyQualifiedName: 'mostViewedEntities',
+ description: 'Displays the most viewed entities.',
+ dataIndexType: 'web_analytic_entity_view_report_data_index',
+ dimensions: [
+ {
+ name: 'entityFqn',
+ chartDataType: 'STRING',
+ },
+ {
+ name: 'owner',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'pageViews',
+ displayName: 'Total number of page view',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952805,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/9217560a-3fed-4c0d-85fa-d7c699feefac',
+ deleted: false,
+ },
+ {
+ id: '73d9b934-7664-4e84-8a7e-7fa00fe03f5f',
+ name: 'pageViewsByEntities',
+ displayName: 'Page views by entities',
+ fullyQualifiedName: 'pageViewsByEntities',
+ description: 'Displays the number of time an entity type was viewed.',
+ dataIndexType: 'web_analytic_entity_view_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityType',
+ chartDataType: 'INT',
+ },
+ ],
+ metrics: [
+ {
+ name: 'pageViews',
+ displayName: 'Total number of page view',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952798,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/73d9b934-7664-4e84-8a7e-7fa00fe03f5f',
+ deleted: false,
+ },
+ {
+ id: '7dc794d3-1881-408c-92fc-6182aa453bc8',
+ name: 'PercentageOfEntitiesWithDescriptionByType',
+ displayName: 'Percentage of Entities With Description',
+ fullyQualifiedName: 'PercentageOfEntitiesWithDescriptionByType',
+ description: 'Display the percentage of entities with description by type.',
+ dataIndexType: 'entity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityType',
+ displayName: 'Entity Type',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'completedDescriptionFraction',
+ displayName: 'Percentage of Completed Description',
+ chartDataType: 'PERCENTAGE',
+ },
+ {
+ name: 'completedDescription',
+ displayName: 'Entities with Completed Description',
+ chartDataType: 'NUMBER',
+ },
+ {
+ name: 'entityCount',
+ displayName: 'Total Entities',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952816,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/7dc794d3-1881-408c-92fc-6182aa453bc8',
+ deleted: false,
+ },
+ {
+ id: 'd712533a-ea8c-409f-a9e3-3f68d06d7864',
+ name: 'PercentageOfEntitiesWithOwnerByType',
+ displayName: 'Percentage of Entities With Owner',
+ fullyQualifiedName: 'PercentageOfEntitiesWithOwnerByType',
+ description: 'Display the percentage of entities with owner by type.',
+ dataIndexType: 'entity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityType',
+ displayName: 'Entity Type',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'hasOwnerFraction',
+ displayName: 'Percentage of Completed Owner',
+ chartDataType: 'PERCENTAGE',
+ },
+ {
+ name: 'hasOwner',
+ displayName: 'Entities with Owner',
+ chartDataType: 'NUMBER',
+ },
+ {
+ name: 'entityCount',
+ displayName: 'Total Entities',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952825,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/d712533a-ea8c-409f-a9e3-3f68d06d7864',
+ deleted: false,
+ },
+ {
+ id: '71d7f330-40d1-4c9e-b843-b5b62ff2efcd',
+ name: 'TotalEntitiesByTier',
+ displayName: 'Percentage of Entities With Tier',
+ fullyQualifiedName: 'TotalEntitiesByTier',
+ description: 'Display the percentage of entities with tier by type.',
+ dataIndexType: 'entity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityTier',
+ displayName: 'Entity Tier',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'entityCountFraction',
+ displayName: 'Total Count of Entity',
+ chartDataType: 'PERCENTAGE',
+ },
+ {
+ name: 'entityCount',
+ displayName: 'Total Entities',
+ chartDataType: 'NUMBER',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952786,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/71d7f330-40d1-4c9e-b843-b5b62ff2efcd',
+ deleted: false,
+ },
+ {
+ id: 'd3eff37a-1196-4e4a-bc6a-5a81bb6504b5',
+ name: 'TotalEntitiesByType',
+ displayName: 'Total Entities',
+ fullyQualifiedName: 'TotalEntitiesByType',
+ description: 'Display the total of entities by type.',
+ dataIndexType: 'entity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityType',
+ displayName: 'Entity Tier',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'entityCountFraction',
+ displayName: 'Total Count of Entity',
+ chartDataType: 'PERCENTAGE',
+ },
+ {
+ name: 'entityCount',
+ displayName: 'Total Entities',
+ chartDataType: 'NUMBER',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952810,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/d3eff37a-1196-4e4a-bc6a-5a81bb6504b5',
+ deleted: false,
+ },
+];
+
+export const KPI_LIST = [
+ {
+ id: 'dabd01bb-095d-448e-af21-0427859b99b5',
+ name: 'description-kpi',
+ displayName: 'Description KPI',
+ fullyQualifiedName: 'description-kpi',
+ description: '',
+ metricType: 'PERCENTAGE',
+ dataInsightChart: {
+ id: '7dc794d3-1881-408c-92fc-6182aa453bc8',
+ type: 'dataInsightChart',
+ name: 'PercentageOfEntitiesWithDescriptionByType',
+ fullyQualifiedName: 'PercentageOfEntitiesWithDescriptionByType',
+ description:
+ 'Display the percentage of entities with description by type.',
+ displayName: 'Percentage of Entities With Description',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/dataInsight/7dc794d3-1881-408c-92fc-6182aa453bc8',
+ },
+ targetDefinition: [
+ {
+ name: 'completedDescriptionFraction',
+ value: '0.65',
+ },
+ ],
+ startDate: 1670351400000,
+ endDate: 1672165800000,
+ version: 0.2,
+ updatedAt: 1670414685805,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/kpi/dabd01bb-095d-448e-af21-0427859b99b5',
+ changeDescription: {
+ fieldsAdded: [],
+ fieldsUpdated: [
+ {
+ name: 'startDate',
+ oldValue: 1670395661000,
+ newValue: 1670351400000,
+ },
+ {
+ name: 'endDate',
+ oldValue: 1672210072000,
+ newValue: 1672165800000,
+ },
+ ],
+ fieldsDeleted: [],
+ previousVersion: 0.1,
+ },
+ deleted: false,
+ },
+];
+
+export const KPI_DATA = {
+ id: 'dabd01bb-095d-448e-af21-0427859b99b5',
+ name: 'description-kpi',
+ displayName: 'Description KPI',
+ fullyQualifiedName: 'description-kpi',
+ description: '',
+ metricType: 'PERCENTAGE',
+ dataInsightChart: {
+ id: '7dc794d3-1881-408c-92fc-6182aa453bc8',
+ type: 'dataInsightChart',
+ name: 'PercentageOfEntitiesWithDescriptionByType',
+ fullyQualifiedName: 'PercentageOfEntitiesWithDescriptionByType',
+ description: 'Display the percentage of entities with description by type.',
+ displayName: 'Percentage of Entities With Description',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/dataInsight/7dc794d3-1881-408c-92fc-6182aa453bc8',
+ },
+ targetDefinition: [
+ {
+ name: 'completedDescriptionFraction',
+ value: '0.65',
+ },
+ ],
+ startDate: 1670351400000,
+ endDate: 1672165800000,
+ version: 0.2,
+ updatedAt: 1670414685805,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/kpi/dabd01bb-095d-448e-af21-0427859b99b5',
+ changeDescription: {
+ fieldsAdded: [],
+ fieldsUpdated: [
+ {
+ name: 'startDate',
+ oldValue: 1670395661000,
+ newValue: 1670351400000,
+ },
+ {
+ name: 'endDate',
+ oldValue: 1672210072000,
+ newValue: 1672165800000,
+ },
+ ],
+ fieldsDeleted: [],
+ previousVersion: 0.1,
+ },
+ deleted: false,
+};
+
+export const DESCRIPTION_CHART = {
+ id: '7dc794d3-1881-408c-92fc-6182aa453bc8',
+ name: 'PercentageOfEntitiesWithDescriptionByType',
+ displayName: 'Percentage of Entities With Description',
+ fullyQualifiedName: 'PercentageOfEntitiesWithDescriptionByType',
+ description: 'Display the percentage of entities with description by type.',
+ dataIndexType: 'entity_report_data_index',
+ dimensions: [
+ {
+ name: 'timestamp',
+ chartDataType: 'INT',
+ },
+ {
+ name: 'entityType',
+ displayName: 'Entity Type',
+ chartDataType: 'STRING',
+ },
+ ],
+ metrics: [
+ {
+ name: 'completedDescriptionFraction',
+ displayName: 'Percentage of Completed Description',
+ chartDataType: 'PERCENTAGE',
+ },
+ {
+ name: 'completedDescription',
+ displayName: 'Entities with Completed Description',
+ chartDataType: 'NUMBER',
+ },
+ {
+ name: 'entityCount',
+ displayName: 'Total Entities',
+ chartDataType: 'INT',
+ },
+ ],
+ version: 0.1,
+ updatedAt: 1670231952816,
+ updatedBy: 'admin',
+ href: 'http://localhost:8585/api/v1/dataInsight/7dc794d3-1881-408c-92fc-6182aa453bc8',
+ deleted: false,
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx
index e156ea03368..e6fa51ca67f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataInsightUtils.tsx
@@ -45,6 +45,7 @@ import { TotalEntitiesByTier } from '../generated/dataInsight/type/totalEntities
import {
ChartValue,
DataInsightChartTooltipProps,
+ KpiDates,
} from '../interface/data-insight.interface';
import { pluralize } from './CommonUtils';
import { getFormattedDateFromMilliSeconds } from './TimeUtils';
@@ -523,3 +524,10 @@ export const getKpiResultFeedback = (day: number, isTargetMet: boolean) => {
export const getDataInsightPathWithFqn = (fqn: string) =>
ROUTES.DATA_INSIGHT_WITH_TAB.replace(PLACEHOLDER_ROUTE_TAB, fqn);
+
+export const getKPIFormattedDates = (kpiDates: KpiDates): KpiDates => {
+ return {
+ startDate: `${kpiDates.startDate} 00:00`,
+ endDate: `${kpiDates.endDate} 23:59`,
+ };
+};