UI : Data Insight KPI improvements (#9188)

* UI : Data Insight KPI improvements

* Remove commented code

* Remove time from start and end date

* refactor : add logic to generate kpi name

* Make insight to insights

* Show chart at the top

* Fix: breadcrumb spacing

* refactor : start date and end date of kpi

* test: add unit test for kpi list

* test : add unit tests for add and edit kpi forms

* chore : minor change

* Address review comments

* Address review comments
This commit is contained in:
Sachin Chaurasiya 2022-12-08 17:48:36 +05:30 committed by GitHub
parent 6f12c971e1
commit fce151c0d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1098 additions and 98 deletions

View File

@ -181,7 +181,7 @@ export const SUPPORTED_CHARTS_FOR_KPI = [
DataInsightChartType.PercentageOfEntitiesWithOwnerByType,
];
export const KPI_DATE_PICKER_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const KPI_DATE_PICKER_FORMAT = 'YYYY-MM-DD';
export const KPI_DATES = {
startDate: '',

View File

@ -383,6 +383,7 @@
"hide": "Hide",
"restore-team": "Restore Team",
"remove": "Remove",
"data-insight-plural": "Data Insights",
"configure-entity": "Configure {{entity}}",
"name-lowercase": "name",
"field-invalid": "{{field}} is invalid",

View File

@ -178,7 +178,9 @@ const DataInsightPage = () => {
<Col span={24}>
<Space className="w-full justify-between">
<div data-testid="data-insight-header">
<Typography.Title level={5}>Data Insight</Typography.Title>
<Typography.Title level={5}>
{t('label.data-insight-plural')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('label.data-insight-subtitle')}
</Typography.Text>

View File

@ -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 }) => (
<span>{children}</span>
)),
}));
jest.mock('../../components/common/DeleteWidget/DeleteWidgetModal', () =>
jest.fn().mockReturnValue(<div data-testid="delete-modal">Delete Modal</div>)
);
jest.mock('../../components/common/next-previous/NextPrevious', () =>
jest
.fn()
.mockReturnValue(<div data-testid="next-previous">Next Previous</div>)
);
jest.mock(
'../../components/common/rich-text-editor/RichTextEditorPreviewer',
() => jest.fn().mockReturnValue(<div data-testid="editor">Editor</div>)
);
jest.mock('../../components/Loader/Loader', () =>
jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>)
);
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(<KPIList />, { 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(<KPIList />, { 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();
});
});

View File

@ -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<Array<Kpi>>([]);
@ -122,10 +122,34 @@ const KPIList = () => {
key: 'actions',
render: (_, record) => {
return (
<Space>
<Tooltip
placement="left"
title={
isAdminUser ? t('label.delete') : NO_PERMISSION_FOR_ACTION
isAdminUser
? t('label.edit')
: t('message.no-permission-for-action')
}>
<Button
data-testid={`edit-action-${getEntityName(record)}`}
disabled={!isAdminUser}
icon={
<SVGIcons
alt={t('label.edit')}
icon={Icons.EDIT}
width="18px"
/>
}
type="text"
onClick={() => history.push(getKpiPath(record.name))}
/>
</Tooltip>
<Tooltip
placement="left"
title={
isAdminUser
? t('label.delete')
: t('message.no-permission-for-action')
}>
<Button
data-testid={`delete-action-${getEntityName(record)}`}
@ -137,6 +161,7 @@ const KPIList = () => {
onClick={() => setSelectedKpi(record)}
/>
</Tooltip>
</Space>
);
},
},
@ -165,6 +190,7 @@ const KPIList = () => {
<Table
bordered
columns={columns}
data-testid="kpi-table"
dataSource={kpiList}
loading={{ spinning: isLoading, indicator: <Loader /> }}
pagination={false}

View File

@ -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,
},
];

View File

@ -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(<div data-testid="editor">Editor</div>)
);
jest.mock(
'../../components/common/title-breadcrumb/title-breadcrumb.component',
() =>
jest.fn().mockReturnValue(<div data-testid="breadcrumb">BreadCrumb</div>)
);
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(<AddKPIPage />, { 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(<AddKPIPage />, { 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(<AddKPIPage />, { 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(<AddKPIPage />, { 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(<AddKPIPage />, { 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();
});
});

View File

@ -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]}>
<Col offset={4} span={12}>
<TitleBreadcrumb titleLinks={breadcrumb} />
<Card className="mt-4">
<TitleBreadcrumb className="my-4" titleLinks={breadcrumb} />
<Card>
<Typography.Paragraph className="text-base" data-testid="form-title">
{t('label.add-new-kpi')}
</Typography.Paragraph>
@ -222,43 +223,6 @@ const AddKPIPage = () => {
layout="vertical"
validateMessages={VALIDATE_MESSAGES}
onFinish={handleSubmit}>
<Form.Item
label={t('label.name')}
name="name"
rules={[
{
required: true,
max: 128,
min: 1,
validator: (_, value) => {
if (
!isUrlFriendlyName(value) ||
nameWithSpace.test(value)
) {
return Promise.reject(
t('label.special-character-not-allowed')
);
}
return Promise.resolve();
},
},
]}>
<Input
data-testid="name"
placeholder={t('label.kpi-name')}
type="text"
/>
</Form.Item>
<Form.Item label={t('label.display-name')} name="displayName">
<Input
data-testid="displayName"
placeholder={t('label.kpi-display-name')}
type="text"
/>
</Form.Item>
<Form.Item
label={t('label.select-a-chart')}
name="dataInsightChart"
@ -282,6 +246,14 @@ const AddKPIPage = () => {
</Select>
</Form.Item>
<Form.Item label={t('label.display-name')} name="displayName">
<Input
data-testid="displayName"
placeholder={t('label.kpi-display-name')}
type="text"
/>
</Form.Item>
<Form.Item
label={t('label.select-a-metric-type')}
name="metricType"
@ -324,7 +296,7 @@ const AddKPIPage = () => {
<>
{selectedMetric.chartDataType ===
ChartDataType.Percentage && (
<Row gutter={20}>
<Row data-testid="metric-percentage-input" gutter={20}>
<Col span={20}>
<Slider
className="kpi-slider"
@ -359,6 +331,7 @@ const AddKPIPage = () => {
{selectedMetric.chartDataType === ChartDataType.Number && (
<InputNumber
className="w-full"
data-testid="metric-number-input"
min={0}
value={metricValue}
onChange={(value) => setMetricValue(Number(value))}
@ -377,11 +350,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
message: t('label.field-required', {
field: t('label.start-date'),
}),
},
]}>
<DatePicker
showTime
className="w-full"
data-testid="start-date"
disabledDate={getDisabledDates}
format={KPI_DATE_PICKER_FORMAT}
onChange={(_, dateString) =>
@ -398,11 +374,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
message: t('label.field-required', {
field: t('label.end-date'),
}),
},
]}>
<DatePicker
showTime
className="w-full"
data-testid="end-date"
disabledDate={getDisabledDates}
format={KPI_DATE_PICKER_FORMAT}
onChange={(_, dateString) =>
@ -442,7 +421,7 @@ const AddKPIPage = () => {
</Form>
</Card>
</Col>
<Col className="m-t-md" span={4}>
<Col className="m-t-md" data-testid="right-panel" span={4}>
<Typography.Paragraph className="text-base font-medium">
{t('label.add-kpi')}
</Typography.Paragraph>

View File

@ -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(<div data-testid="editor">Editor</div>)
);
jest.mock(
'../../components/common/title-breadcrumb/title-breadcrumb.component',
() =>
jest.fn().mockReturnValue(<div data-testid="breadcrumb">BreadCrumb</div>)
);
jest.mock('../../components/Loader/Loader', () =>
jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>)
);
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(<EditKPIPage />, { 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(<EditKPIPage />, { 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(<EditKPIPage />, { 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');
});
});

View File

@ -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 ? (
<Row
className="bg-body-main h-full"
data-testid="add-kpi-container"
data-testid="edit-kpi-container"
gutter={[16, 16]}>
<Col offset={4} span={12}>
<TitleBreadcrumb titleLinks={breadcrumb} />
<Card className="mt-4">
<TitleBreadcrumb className="my-4" titleLinks={breadcrumb} />
<Card>
<Typography.Paragraph
className="text-base"
data-testid="form-title">
{t('label.add-new-kpi')}
{t('label.edit-entity', { entity: t('label.kpi-uppercase') })}
</Typography.Paragraph>
<Form
data-testid="kpi-form"
@ -251,12 +251,13 @@ const AddKPIPage = () => {
layout="vertical"
validateMessages={VALIDATE_MESSAGES}
onFinish={handleSubmit}>
<Form.Item label={t('label.name')} name="name">
<Form.Item
label={t('label.data-insight-chart')}
name="dataInsightChart">
<Input
disabled
data-testid="name"
placeholder={t('label.kpi-name')}
type="text"
data-testid="dataInsightChart"
value={selectedChart?.displayName || selectedChart?.name}
/>
</Form.Item>
@ -268,19 +269,14 @@ const AddKPIPage = () => {
/>
</Form.Item>
<Form.Item
label={t('label.data-insight-chart')}
name="dataInsightChart">
<Form.Item label={t('label.metric-type')} name="metricType">
<Input
disabled
value={selectedChart?.displayName || selectedChart?.name}
data-testid="metricType"
value={metricData?.name}
/>
</Form.Item>
<Form.Item label={t('label.metric-type')} name="metricType">
<Input disabled value={metricData?.name} />
</Form.Item>
{!isUndefined(metricData) && (
<Form.Item
label={t('label.metric-value')}
@ -301,7 +297,7 @@ const AddKPIPage = () => {
]}>
<>
{kpiData?.metricType === KpiTargetType.Percentage && (
<Row gutter={20}>
<Row data-testid="metric-percentage-input" gutter={20}>
<Col span={20}>
<Slider
className="kpi-slider"
@ -336,6 +332,7 @@ const AddKPIPage = () => {
{kpiData?.metricType === KpiTargetType.Number && (
<InputNumber
className="w-full"
data-testid="metric-number-input"
min={0}
value={metricValue}
onChange={(value) => setMetricValue(Number(value))}
@ -354,11 +351,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
message: t('label.field-required', {
field: t('label.start-date'),
}),
},
]}>
<DatePicker
showTime
className="w-full"
data-testid="start-date"
disabledDate={getDisabledDates}
format={KPI_DATE_PICKER_FORMAT}
onChange={(_, dateString) =>
@ -375,11 +375,14 @@ const AddKPIPage = () => {
rules={[
{
required: true,
message: t('label.field-required', {
field: t('label.end-date'),
}),
},
]}>
<DatePicker
showTime
className="w-full"
data-testid="end-date"
disabledDate={getDisabledDates}
format={KPI_DATE_PICKER_FORMAT}
onChange={(_, dateString) =>
@ -409,7 +412,9 @@ const AddKPIPage = () => {
</Button>
<Tooltip
title={
isAdminUser ? t('label.save') : NO_PERMISSION_FOR_ACTION
isAdminUser
? t('label.save')
: t('message.no-permission-for-action')
}>
<Button
data-testid="submit-btn"
@ -425,7 +430,7 @@ const AddKPIPage = () => {
</Form>
</Card>
</Col>
<Col className="m-t-md" span={4}>
<Col className="m-t-md" data-testid="right-panel" span={4}>
<Typography.Paragraph className="text-base font-medium">
{t('label.edit-entity', { entity: t('label.kpi-uppercase') })}
</Typography.Paragraph>
@ -441,4 +446,4 @@ const AddKPIPage = () => {
);
};
export default AddKPIPage;
export default EditKPIPage;

View File

@ -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,
};

View File

@ -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`,
};
};