feat#10187: added functionality for selection of custom date range for the timestamp (#11041)

* Added custom datePicker option also added yesterday option for picking the timestamp duration in data quality

* localization changes for other languages

* Fixed unit test failures and fixed bugs

* Fixed errors on column profiler page
This commit is contained in:
Aniket Katkar 2023-04-14 01:48:59 +05:30 committed by GitHub
parent 1957e7c964
commit d9564ef0d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 436 additions and 242 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 12 12"><path fill="currentColor" stroke="currentColor" stroke-width=".6" d="M6 8.66a.408.408 0 0 1-.29-.12L1.62 4.447a.409.409 0 1 1 .578-.578L6 7.672 9.802 3.87a.409.409 0 1 1 .578.578L6.29 8.54a.408.408 0 0 1-.29.12Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 12 12"><path fill="currentColor" stroke="currentColor" stroke-width=".1" d="M6 8.66a.408.408 0 0 1-.29-.12L1.62 4.447a.409.409 0 1 1 .578-.578L6 7.672 9.802 3.87a.409.409 0 1 1 .578.578L6.29 8.54a.408.408 0 0 1-.29.12Z"/></svg>

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 300 B

View File

@ -0,0 +1,152 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CloseCircleOutlined } from '@ant-design/icons';
import { Button, DatePicker, Dropdown, MenuProps, Space } from 'antd';
import { RangePickerProps } from 'antd/lib/date-picker';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import {
DEFAULT_SELECTED_RANGE,
PROFILER_FILTER_RANGE,
} from 'constants/profiler.constant';
import { MenuInfo } from 'rc-menu/lib/interface';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getDaysCount, getTimestampLabel } from 'utils/DatePickerMenuUtils';
import {
getCurrentDateTimeStamp,
getPastDatesTimeStampFromCurrentDate,
} from 'utils/TimeUtils';
import { ReactComponent as DropdownIcon } from '../../assets/svg/DropDown.svg';
import './DatePickerMenu.style.less';
interface DatePickerMenuProps {
showSelectedCustomRange?: boolean;
handleDateRangeChange: (value: DateRangeObject, days?: number) => void;
}
function DatePickerMenu({
showSelectedCustomRange,
handleDateRangeChange,
}: DatePickerMenuProps) {
const { t } = useTranslation();
// State to display the label for selected range value
const [selectedTimeRange, setSelectedTimeRange] = useState<string>(
DEFAULT_SELECTED_RANGE.title
);
// state to determine the selected value to highlight in the dropdown
const [selectedTimeRangeKey, setSelectedTimeRangeKey] = useState<
keyof typeof PROFILER_FILTER_RANGE
>(DEFAULT_SELECTED_RANGE.key as keyof typeof PROFILER_FILTER_RANGE);
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const handleCustomDateChange: RangePickerProps['onChange'] = (
values,
dateStrings
) => {
if (values) {
const startTs = Date.parse(dateStrings[0]) / 1000;
const endTs = Date.parse(dateStrings[1]) / 1000;
const daysCount = getDaysCount(dateStrings[0], dateStrings[1]);
const selectedRangeLabel = getTimestampLabel(
dateStrings[0],
dateStrings[1],
showSelectedCustomRange
);
setSelectedTimeRange(selectedRangeLabel);
setSelectedTimeRangeKey(
'customRange' as keyof typeof PROFILER_FILTER_RANGE
);
setIsMenuOpen(false);
handleDateRangeChange({ startTs, endTs }, daysCount);
}
};
const handleOptionClick = ({ key }: MenuInfo) => {
const selectedNumberOfDays =
PROFILER_FILTER_RANGE[key as keyof typeof PROFILER_FILTER_RANGE].days;
const keyString = key as keyof typeof PROFILER_FILTER_RANGE;
const startTs = getPastDatesTimeStampFromCurrentDate(selectedNumberOfDays);
const endTs = getCurrentDateTimeStamp();
setSelectedTimeRange(PROFILER_FILTER_RANGE[keyString].title);
setSelectedTimeRangeKey(keyString);
setIsMenuOpen(false);
handleDateRangeChange({ startTs, endTs }, selectedNumberOfDays);
};
const getMenuItems = () => {
const items: MenuProps['items'] = Object.entries(PROFILER_FILTER_RANGE).map(
([key, value]) => ({
label: value.title,
key,
})
);
items.push({
label: t('label.custom-range'),
key: 'customRange',
children: [
{
label: (
<DatePicker.RangePicker
bordered={false}
clearIcon={<CloseCircleOutlined />}
open={isMenuOpen}
placement="bottomRight"
suffixIcon={null}
onChange={handleCustomDateChange}
/>
),
key: 'datePicker',
},
],
popupClassName: 'date-picker-sub-menu-popup',
});
return items;
};
const items: MenuProps['items'] = getMenuItems();
return (
<>
<Dropdown
destroyPopupOnHide
menu={{
items,
triggerSubMenuAction: 'click',
onClick: handleOptionClick,
selectedKeys: [selectedTimeRangeKey],
}}
open={isMenuOpen}
trigger={['click']}
onOpenChange={(value) => setIsMenuOpen(value)}>
<Button>
<Space align="center" size={8}>
{selectedTimeRange}
<DropdownIcon height={14} width={14} />
</Space>
</Button>
</Dropdown>
</>
);
}
export default DatePickerMenu;

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.date-picker-sub-menu-popup {
visibility: hidden;
height: 0px;
ul {
height: 0px;
li {
height: 0px;
}
}
}

View File

@ -102,6 +102,14 @@ jest.mock('../containers/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
jest.mock('components/DatePickerMenu/DatePickerMenu.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="DatePickerMenu">DatePickerMenu</div>
))
);
describe('Test ProfilerDashboardPage component', () => {
beforeEach(() => cleanup());
@ -115,15 +123,13 @@ describe('Test ProfilerDashboardPage component', () => {
const profilerSwitch = await screen.findByTestId('profiler-switch');
const EntityPageInfo = await screen.findByText('EntityPageInfo component');
const ProfilerTab = await screen.findByText('ProfilerTab component');
const selectedTimeFrame = await screen.findByText(
'label.last-number-of-days'
);
const DatePickerMenu = await screen.findByTestId('DatePickerMenu');
const DataQualityTab = screen.queryByText('DataQualityTab component');
expect(profilerSwitch).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument();
expect(ProfilerTab).toBeInTheDocument();
expect(selectedTimeFrame).toBeInTheDocument();
expect(DatePickerMenu).toBeInTheDocument();
expect(DataQualityTab).not.toBeInTheDocument();
});
@ -167,15 +173,13 @@ describe('Test ProfilerDashboardPage component', () => {
const profilerSwitch = await screen.findByTestId('profiler-switch');
const EntityPageInfo = await screen.findByText('EntityPageInfo component');
const ProfilerTab = await screen.findByText('ProfilerTab component');
const selectedTimeFrame = await screen.findByText(
'label.last-number-of-days'
);
const DatePickerMenu = await screen.findByTestId('DatePickerMenu');
const DataQualityTab = screen.queryByText('DataQualityTab component');
expect(profilerSwitch).toBeInTheDocument();
expect(EntityPageInfo).toBeInTheDocument();
expect(ProfilerTab).toBeInTheDocument();
expect(selectedTimeFrame).toBeInTheDocument();
expect(DatePickerMenu).toBeInTheDocument();
expect(DataQualityTab).not.toBeInTheDocument();
});

View File

@ -26,6 +26,8 @@ import { RadioChangeEvent } from 'antd/lib/radio';
import { SwitchChangeEventHandler } from 'antd/lib/switch';
import { AxiosError } from 'axios';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component';
import { isEqual } from 'lodash';
import { EntityTags, ExtraInfo } from 'Models';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -41,7 +43,7 @@ import {
getTeamAndUserDetailsPath,
} from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant';
import { DEFAULT_RANGE_DATA } from '../../constants/profiler.constant';
import { EntityType, FqnPart } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { ProfilerDashboardType } from '../../enums/table.enum';
@ -80,6 +82,7 @@ import {
} from '../PermissionProvider/PermissionProvider.interface';
import DataQualityTab from './component/DataQualityTab';
import ProfilerTab from './component/ProfilerTab';
import { DateRangeObject } from './component/TestSummary';
import {
ProfilerDashboardProps,
ProfilerDashboardTab,
@ -114,8 +117,8 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
);
const [selectedTestCaseStatus, setSelectedTestCaseStatus] =
useState<string>('');
const [selectedTimeRange, setSelectedTimeRange] =
useState<keyof typeof PROFILER_FILTER_RANGE>('last3days');
const [dateRangeObject, setDateRangeObject] =
useState<DateRangeObject>(DEFAULT_RANGE_DATA);
const [activeColumnDetails, setActiveColumnDetails] = useState<Column>(
{} as Column
);
@ -147,13 +150,6 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
});
}, [dashboardType]);
const timeRangeOption = useMemo(() => {
return Object.entries(PROFILER_FILTER_RANGE).map(([key, value]) => ({
label: value.title,
value: key,
}));
}, []);
const testCaseStatusOption = useMemo(() => {
const testCaseStatus: Record<string, string>[] = Object.values(
TestCaseStatus
@ -396,11 +392,11 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
);
};
const handleTimeRangeChange = (value: keyof typeof PROFILER_FILTER_RANGE) => {
if (value !== selectedTimeRange) {
setSelectedTimeRange(value);
const handleDateRangeChange = (value: DateRangeObject) => {
if (!isEqual(value, dateRangeObject)) {
setDateRangeObject(value);
if (activeTab === ProfilerDashboardTab.PROFILER) {
fetchProfilerData(entityTypeFQN, PROFILER_FILTER_RANGE[value].days);
fetchProfilerData(entityTypeFQN, dateRangeObject);
}
}
};
@ -523,11 +519,9 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
</>
)}
{activeTab === ProfilerDashboardTab.PROFILER && (
<Select
className="tw-w-32"
options={timeRangeOption}
value={selectedTimeRange}
onChange={handleTimeRangeChange}
<DatePickerMenu
showSelectedCustomRange
handleDateRangeChange={handleDateRangeChange}
/>
)}
<Tooltip

View File

@ -11,11 +11,12 @@
* limitations under the License.
*/
import { Col, Row, Select, Space, Typography } from 'antd';
import { Col, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import { isEmpty, isEqual, isUndefined } from 'lodash';
import React, { ReactElement, useEffect, useState } from 'react';
import {
Legend,
Line,
@ -30,7 +31,7 @@ import {
import { getListTestCaseResults } from 'rest/testAPI';
import {
COLORS,
PROFILER_FILTER_RANGE,
DEFAULT_RANGE_DATA,
} from '../../../constants/profiler.constant';
import { CSMode } from '../../../enums/codemirror.enum';
import { SIZE } from '../../../enums/common.enum';
@ -41,11 +42,7 @@ import {
} from '../../../generated/tests/testCase';
import { axisTickFormatter } from '../../../utils/ChartUtils';
import { getEncodedFqn } from '../../../utils/StringsUtils';
import {
getCurrentDateTimeStamp,
getFormattedDateFromSeconds,
getPastDatesTimeStampFromCurrentDate,
} from '../../../utils/TimeUtils';
import { getFormattedDateFromSeconds } from '../../../utils/TimeUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
@ -58,26 +55,24 @@ type ChartDataType = {
data: { [key: string]: string }[];
};
export interface DateRangeObject {
startTs: number;
endTs: number;
}
const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
const [chartData, setChartData] = useState<ChartDataType>(
{} as ChartDataType
);
const [results, setResults] = useState<TestCaseResult[]>([]);
const [selectedTimeRange, setSelectedTimeRange] =
useState<keyof typeof PROFILER_FILTER_RANGE>('last3days');
const [dateRangeObject, setDateRangeObject] =
useState<DateRangeObject>(DEFAULT_RANGE_DATA);
const [isLoading, setIsLoading] = useState(true);
const [isGraphLoading, setIsGraphLoading] = useState(true);
const timeRangeOption = useMemo(() => {
return Object.entries(PROFILER_FILTER_RANGE).map(([key, value]) => ({
label: value.title,
value: key,
}));
}, []);
const handleTimeRangeChange = (value: keyof typeof PROFILER_FILTER_RANGE) => {
if (value !== selectedTimeRange) {
setSelectedTimeRange(value);
const handleDateRangeChange = (value: DateRangeObject) => {
if (!isEqual(value, dateRangeObject)) {
setDateRangeObject(value);
}
};
@ -97,13 +92,14 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
...values,
});
});
chartData.reverse();
setChartData({
information:
currentData[0]?.testResultValue?.map((info, i) => ({
label: info.name || '',
color: COLORS[i],
})) || [],
data: chartData.reverse(),
data: chartData,
});
};
@ -112,12 +108,12 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
props: any
): ReactElement<SVGElement> => {
const { cx = 0, cy = 0, payload } = props;
const fill =
payload.status === TestCaseStatus.Success
? '#28A745'
: payload.status === TestCaseStatus.Failed
? '#CB2431'
: '#EFAE2F';
let fill =
payload.status === TestCaseStatus.Success ? '#28A745' : undefined;
if (isUndefined(fill)) {
fill = payload.status === TestCaseStatus.Failed ? '#CB2431' : '#EFAE2F';
}
return (
<svg
@ -132,24 +128,15 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
);
};
const fetchTestResults = async () => {
const fetchTestResults = async (dateRangeObj: DateRangeObject) => {
if (isEmpty(data)) {
return;
}
setIsGraphLoading(true);
try {
const startTs = getPastDatesTimeStampFromCurrentDate(
PROFILER_FILTER_RANGE[selectedTimeRange].days
);
const endTs = getCurrentDateTimeStamp();
const { data: chartData } = await getListTestCaseResults(
getEncodedFqn(data.fullyQualifiedName || ''),
{
startTs,
endTs,
}
dateRangeObj
);
setResults(chartData);
generateChartData(chartData);
@ -161,9 +148,57 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
}
};
const getGraph = () => {
if (isGraphLoading) {
return <Loader />;
}
return results.length ? (
<ResponsiveContainer
className="tw-bg-white"
id={`${data.name}_graph`}
minHeight={300}>
<LineChart
data={chartData.data}
margin={{
top: 8,
bottom: 8,
right: 8,
}}>
<XAxis dataKey="name" padding={{ left: 8, right: 8 }} />
<YAxis
allowDataOverflow
padding={{ top: 8, bottom: 8 }}
tickFormatter={(value) => axisTickFormatter(value)}
/>
<Tooltip />
<Legend />
{data.parameterValues?.length === 2 && referenceArea()}
{chartData?.information?.map((info) => (
<Line
dataKey={info.label}
dot={updatedDot}
key={`${info.label}${info.color}`}
stroke={info.color}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
) : (
<ErrorPlaceHolder classes="tw-mt-0" size={SIZE.MEDIUM}>
<Typography.Paragraph className="m-b-md">
{t('message.try-different-time-period-filtering')}
</Typography.Paragraph>
</ErrorPlaceHolder>
);
};
useEffect(() => {
fetchTestResults();
}, [selectedTimeRange]);
if (dateRangeObject) {
fetchTestResults(dateRangeObject);
}
}, [dateRangeObject]);
const showParamsData = (param: TestCaseParameterValue) => {
const isSqlQuery = param.name === 'sqlExpression';
@ -217,55 +252,13 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
) : (
<div>
<Space align="end" className="tw-w-full" direction="vertical">
<Select
className="tw-w-32 tw-mb-2"
options={timeRangeOption}
value={selectedTimeRange}
onChange={handleTimeRangeChange}
<DatePickerMenu
showSelectedCustomRange
handleDateRangeChange={handleDateRangeChange}
/>
</Space>
{isGraphLoading ? (
<Loader />
) : results.length ? (
<ResponsiveContainer
className="tw-bg-white"
id={`${data.name}_graph`}
minHeight={300}>
<LineChart
data={chartData.data}
margin={{
top: 8,
bottom: 8,
right: 8,
}}>
<XAxis dataKey="name" padding={{ left: 8, right: 8 }} />
<YAxis
allowDataOverflow
padding={{ top: 8, bottom: 8 }}
tickFormatter={(value) => axisTickFormatter(value)}
/>
<Tooltip />
<Legend />
{data.parameterValues?.length === 2 && referenceArea()}
{chartData?.information?.map((info, i) => (
<Line
dataKey={info.label}
dot={updatedDot}
key={i}
stroke={info.color}
type="monotone"
/>
))}
</LineChart>
</ResponsiveContainer>
) : (
<ErrorPlaceHolder classes="tw-mt-0" size={SIZE.MEDIUM}>
<Typography.Paragraph className="m-b-md">
{t('message.try-different-time-period-filtering')}
</Typography.Paragraph>
</ErrorPlaceHolder>
)}
{getGraph()}
</div>
)}
</Col>

View File

@ -19,6 +19,7 @@ import {
Table,
} from '../../generated/entity/data/table';
import { TestCase } from '../../generated/tests/testCase';
import { DateRangeObject } from './component/TestSummary';
export interface ProfilerDashboardProps {
onTableChange: (table: Table) => void;
@ -26,7 +27,10 @@ export interface ProfilerDashboardProps {
table: Table;
testCases: TestCase[];
profilerData: ColumnProfile[];
fetchProfilerData: (tableId: string, days?: number) => void;
fetchProfilerData: (
tableId: string,
dateRangeObject?: DateRangeObject
) => void;
fetchTestCases: (fqn: string, params?: ListTestCaseParams) => void;
onTestCaseUpdate: (deleted?: boolean) => void;
}

View File

@ -14,37 +14,20 @@
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { getSystemProfileList, getTableProfilesList } from 'rest/tableAPI';
import {
getPastDatesTimeStampFromCurrentDate,
getPastDaysDateTimeMillis,
} from '../../../utils/TimeUtils';
import TableProfilerChart from './TableProfilerChart';
const mockFQN = 'testFQN';
const mockTimeValue = {
endSec: 1670667984,
startSec: 1670408784,
endMilli: 1670667985445,
startMilli: 1670408785445,
endMilli: 1670667984000,
startMilli: 1670408784000,
};
const mockDateRangeObject = { startTs: 1670408784, endTs: 1670667984 };
jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => ({ datasetFQN: mockFQN })),
}));
jest.mock('../../../utils/TimeUtils', () => ({
getCurrentDateTimeMillis: jest
.fn()
.mockImplementation(() => mockTimeValue.endMilli),
getCurrentDateTimeStamp: jest
.fn()
.mockImplementation(() => mockTimeValue.endSec),
getPastDatesTimeStampFromCurrentDate: jest
.fn()
.mockImplementation(() => mockTimeValue.startSec),
getPastDaysDateTimeMillis: jest
.fn()
.mockImplementation(() => mockTimeValue.startMilli),
}));
jest.mock('rest/tableAPI');
jest.mock('../../ProfilerDashboard/component/ProfilerLatestValue', () => {
return jest.fn().mockImplementation(() => <div>ProfilerLatestValue</div>);
@ -64,7 +47,7 @@ describe('TableProfilerChart component test', () => {
const mockGetSystemProfileList = getSystemProfileList as jest.Mock;
const mockGetTableProfilesList = getTableProfilesList as jest.Mock;
act(() => {
render(<TableProfilerChart selectedTimeRange="last3days" />);
render(<TableProfilerChart dateRangeObject={mockDateRangeObject} />);
});
expect(
@ -89,7 +72,7 @@ describe('TableProfilerChart component test', () => {
const mockGetSystemProfileList = getSystemProfileList as jest.Mock;
const mockGetTableProfilesList = getTableProfilesList as jest.Mock;
await act(async () => {
render(<TableProfilerChart selectedTimeRange="last3days" />);
render(<TableProfilerChart dateRangeObject={mockDateRangeObject} />);
});
// API should be call once
@ -111,20 +94,14 @@ describe('TableProfilerChart component test', () => {
it('If TimeRange change API should be call accordingly', async () => {
const startTime = {
inMilli: 1670063664901,
inSec: 1670063664,
inMilli: 1670408784000,
inSec: 1670408784,
};
const mockGetSystemProfileList = getSystemProfileList as jest.Mock;
const mockGetTableProfilesList = getTableProfilesList as jest.Mock;
(getPastDatesTimeStampFromCurrentDate as jest.Mock).mockImplementationOnce(
() => startTime.inSec
);
(getPastDaysDateTimeMillis as jest.Mock).mockImplementationOnce(
() => startTime.inMilli
);
await act(async () => {
render(<TableProfilerChart selectedTimeRange="last7days" />);
render(<TableProfilerChart dateRangeObject={mockDateRangeObject} />);
});
// API should be call with proper Param value

View File

@ -13,24 +13,18 @@
import { Card, Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getSystemProfileList, getTableProfilesList } from 'rest/tableAPI';
import {
INITIAL_OPERATION_METRIC_VALUE,
INITIAL_ROW_METRIC_VALUE,
PROFILER_FILTER_RANGE,
} from '../../../constants/profiler.constant';
import {
calculateRowCountMetrics,
calculateSystemMetrics,
} from '../../../utils/TableProfilerUtils';
import {
getCurrentDateTimeMillis,
getCurrentDateTimeStamp,
getPastDatesTimeStampFromCurrentDate,
getPastDaysDateTimeMillis,
} from '../../../utils/TimeUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import CustomBarChart from '../../Chart/CustomBarChart';
import OperationDateBarChart from '../../Chart/OperationDateBarChart';
@ -40,7 +34,7 @@ import ProfilerLatestValue from '../../ProfilerDashboard/component/ProfilerLates
import { MetricChartType } from '../../ProfilerDashboard/profilerDashboard.interface';
import { TableProfilerChartProps } from '../TableProfiler.interface';
const TableProfilerChart = ({ selectedTimeRange }: TableProfilerChartProps) => {
const TableProfilerChart = ({ dateRangeObject }: TableProfilerChartProps) => {
const { datasetFQN } = useParams<{ datasetFQN: string }>();
const [rowCountMetrics, setRowCountMetrics] = useState<MetricChartType>(
@ -53,28 +47,24 @@ const TableProfilerChart = ({ selectedTimeRange }: TableProfilerChartProps) => {
useState<MetricChartType>(INITIAL_OPERATION_METRIC_VALUE);
const [isLoading, setIsLoading] = useState(true);
const fetchTableProfiler = async (fqn: string, days = 3) => {
const fetchTableProfiler = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
try {
const startTs = getPastDatesTimeStampFromCurrentDate(days);
const endTs = getCurrentDateTimeStamp();
const { data } = await getTableProfilesList(fqn, {
startTs,
endTs,
});
const { data } = await getTableProfilesList(fqn, dateRangeObj);
const rowMetricsData = calculateRowCountMetrics(data, rowCountMetrics);
setRowCountMetrics(rowMetricsData);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchSystemProfiler = async (fqn: string, days = 3) => {
const fetchSystemProfiler = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
try {
const startTs = getPastDaysDateTimeMillis(days);
const endTs = getCurrentDateTimeMillis();
const { data } = await getSystemProfileList(fqn, {
startTs,
endTs,
});
const { data } = await getSystemProfileList(fqn, dateRangeObj);
const { operationMetrics: metricsData, operationDateMetrics } =
calculateSystemMetrics(data, operationMetrics);
@ -85,23 +75,26 @@ const TableProfilerChart = ({ selectedTimeRange }: TableProfilerChartProps) => {
}
};
const fetchProfilerData = async (fqn: string, days = 3) => {
const fetchProfilerData = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
setIsLoading(true);
await fetchTableProfiler(fqn, days);
await fetchSystemProfiler(fqn, days);
await fetchTableProfiler(fqn, dateRangeObj);
await fetchSystemProfiler(fqn, {
startTs: dateRangeObj.startTs * 1000,
endTs: dateRangeObj.endTs * 1000,
});
setIsLoading(false);
};
useEffect(() => {
if (datasetFQN) {
fetchProfilerData(
datasetFQN,
PROFILER_FILTER_RANGE[selectedTimeRange].days
);
fetchProfilerData(datasetFQN, dateRangeObject);
} else {
setIsLoading(false);
}
}, [datasetFQN, selectedTimeRange]);
}, [datasetFQN, dateRangeObject]);
if (isLoading) {
return <Loader />;

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import { SystemProfile } from '../../generated/api/data/createTableProfile';
import {
Column,
@ -82,7 +82,7 @@ export type TableProfilerData = {
};
export type TableProfilerChartProps = {
selectedTimeRange: keyof typeof PROFILER_FILTER_RANGE;
dateRangeObject: DateRangeObject;
};
export interface ProfilerSettingModalState {

View File

@ -28,7 +28,9 @@ import { DefaultOptionType } from 'antd/lib/select';
import { SwitchChangeEventHandler } from 'antd/lib/switch';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isUndefined, map } from 'lodash';
import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import { isEqual, isUndefined, map } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useParams } from 'react-router-dom';
@ -42,8 +44,8 @@ import { ReactComponent as TableProfileIcon } from '../../assets/svg/table-profi
import { API_RES_MAX_SIZE } from '../../constants/constants';
import { PAGE_HEADERS } from '../../constants/PageHeaders.constant';
import {
DEFAULT_RANGE_DATA,
INITIAL_TEST_RESULT_SUMMARY,
PROFILER_FILTER_RANGE,
} from '../../constants/profiler.constant';
import { ProfilerDashboardType } from '../../enums/table.enum';
import { ProfileSampleType, Table } from '../../generated/entity/data/table';
@ -95,8 +97,8 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
const [selectedTestType, setSelectedTestType] = useState('');
const [deleted, setDeleted] = useState<boolean>(false);
const [isTestCaseLoading, setIsTestCaseLoading] = useState(false);
const [selectedTimeRange, setSelectedTimeRange] =
useState<keyof typeof PROFILER_FILTER_RANGE>('last3days');
const [dateRangeObject, setDateRangeObject] =
useState<DateRangeObject>(DEFAULT_RANGE_DATA);
const isSummary = activeTab === ProfilerDashboardTab.SUMMARY;
const isDataQuality = activeTab === ProfilerDashboardTab.DATA_QUALITY;
const isProfiler = activeTab === ProfilerDashboardTab.PROFILER;
@ -229,20 +231,13 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
},
];
const timeRangeOption = useMemo(() => {
return Object.entries(PROFILER_FILTER_RANGE).map(([key, value]) => ({
label: value.title,
value: key,
}));
}, []);
const handleTabChange: MenuProps['onClick'] = (value) => {
setActiveTab(value.key as ProfilerDashboardTab);
};
const handleTimeRangeChange = (value: keyof typeof PROFILER_FILTER_RANGE) => {
if (value !== selectedTimeRange) {
setSelectedTimeRange(value);
const handleDateRangeChange = (value: DateRangeObject) => {
if (!isEqual(value, dateRangeObject)) {
setDateRangeObject(value);
}
};
@ -400,11 +395,9 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
)}
{isProfiler && (
<Select
className="tw-w-32"
options={timeRangeOption}
value={selectedTimeRange}
onChange={handleTimeRangeChange}
<DatePickerMenu
showSelectedCustomRange
handleDateRangeChange={handleDateRangeChange}
/>
)}
@ -515,7 +508,7 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
)}
{isProfiler && (
<TableProfilerChart selectedTimeRange={selectedTimeRange} />
<TableProfilerChart dateRangeObject={dateRangeObject} />
)}
{settingModalVisible && (

View File

@ -52,7 +52,7 @@ export const DATA_INSIGHT_GRAPH_COLORS = [
];
export const BAR_SIZE = 15;
export const DEFAULT_DAYS = 30;
export const DEFAULT_DAYS = 1;
export const ENTITIES_BAR_COLO_MAP: Record<string, string> = {
Chart: '#E7B85D',

View File

@ -14,6 +14,10 @@
import { t } from 'i18next';
import { StepperStepType } from 'Models';
import i18n from 'utils/i18next/LocalUtil';
import {
getCurrentDateTimeStamp,
getPastDatesTimeStampFromCurrentDate,
} from 'utils/TimeUtils';
import { CSMode } from '../enums/codemirror.enum';
import { DMLOperationType } from '../generated/api/data/createTableProfile';
import {
@ -67,6 +71,10 @@ export const PROFILER_METRIC = [
];
export const PROFILER_FILTER_RANGE = {
yesterday: {
days: 1,
title: t('label.yesterday'),
},
last3days: {
days: 3,
title: t('label.last-number-of-days', {
@ -99,6 +107,18 @@ export const PROFILER_FILTER_RANGE = {
},
};
export const DEFAULT_SELECTED_RANGE = {
key: 'yesterday',
title: t('label.yesterday'),
days: 1,
};
export const DEFAULT_RANGE_DATA = {
startTs: getPastDatesTimeStampFromCurrentDate(DEFAULT_SELECTED_RANGE.days),
endTs: getCurrentDateTimeStamp(),
};
export const COLORS = ['#7147E8', '#B02AAC', '#B02AAC', '#1890FF', '#008376'];
export const DEFAULT_CHART_COLLECTION_VALUE = {

View File

@ -153,6 +153,7 @@
"custom-oidc": "CustomOidc",
"custom-property": "Custom property",
"custom-property-plural": "Custom Properties",
"custom-range": "Custom Range",
"dag": "Dag",
"dag-view": "DAG view",
"daily-active-users-on-the-platform": "Daily Active Users on the Platform",
@ -910,6 +911,7 @@
"weekly-usage": "Weekly Usage",
"whats-new": "What's New",
"yes": "Yes",
"yesterday": "Yesterday",
"your-entity": "Your {{entity}}"
},
"message": {

View File

@ -153,6 +153,7 @@
"custom-oidc": "OIDC personalizado",
"custom-property": "Propiedad personalizada",
"custom-property-plural": "Propiedades personalizadas",
"custom-range": "Custom Range",
"dag": "DAG",
"dag-view": "Vista DAG",
"daily-active-users-on-the-platform": "Usuarios activos diarios en la plataforma",
@ -910,6 +911,7 @@
"weekly-usage": "Uso semanal",
"whats-new": "Novedades",
"yes": "Sí",
"yesterday": "Yesterday",
"your-entity": "Tu {{entity}}"
},
"message": {

View File

@ -153,6 +153,7 @@
"custom-oidc": "CustomOidc",
"custom-property": "Custom property",
"custom-property-plural": "Propriétés Personalisées",
"custom-range": "Custom Range",
"dag": "Dag",
"dag-view": "Vue DAG",
"daily-active-users-on-the-platform": "Utilisateurs actifs quotidiens",
@ -910,6 +911,7 @@
"weekly-usage": "Weekly Usage",
"whats-new": "Nouveau",
"yes": "Yes",
"yesterday": "Yesterday",
"your-entity": "Your {{entity}}"
},
"message": {

View File

@ -153,6 +153,7 @@
"custom-oidc": "CustomOidc",
"custom-property": "カスタムプロパティ",
"custom-property-plural": "カスタムプロパティ",
"custom-range": "Custom Range",
"dag": "Dag",
"dag-view": "DAGビュー",
"daily-active-users-on-the-platform": "このプラットフォームのアクティブなユーザー",
@ -910,6 +911,7 @@
"weekly-usage": "Weekly Usage",
"whats-new": "最新情報",
"yes": "はい",
"yesterday": "Yesterday",
"your-entity": "あなたの{{entity}}"
},
"message": {

View File

@ -153,6 +153,7 @@
"custom-oidc": "OIDC customizado",
"custom-property": "Propriedade customizada",
"custom-property-plural": "Propriedades customizadas",
"custom-range": "Custom Range",
"dag": "DAG",
"dag-view": "Visão da DAG",
"daily-active-users-on-the-platform": "Usuários ativos diariamente na plataforma",
@ -910,6 +911,7 @@
"weekly-usage": "Uso semanal",
"whats-new": "O que há de novo",
"yes": "Sim",
"yesterday": "Yesterday",
"your-entity": "Sua {{entity}}"
},
"message": {

View File

@ -153,6 +153,7 @@
"custom-oidc": "CustomOidc",
"custom-property": "Custom property",
"custom-property-plural": "定制属性",
"custom-range": "Custom Range",
"dag": "Dag",
"dag-view": "DAG view",
"daily-active-users-on-the-platform": "Daily active users on the platform",
@ -910,6 +911,7 @@
"weekly-usage": "Weekly Usage",
"whats-new": "What's new",
"yes": "是",
"yesterday": "Yesterday",
"your-entity": "Your {{entity}}"
},
"message": {

View File

@ -11,16 +11,7 @@
* limitations under the License.
*/
import {
Button,
Card,
Col,
Row,
Select,
Space,
Tooltip,
Typography,
} from 'antd';
import { Button, Card, Col, Row, Space, Tooltip, Typography } from 'antd';
import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import DailyActiveUsersChart from 'components/DataInsightDetail/DailyActiveUsersChart';
@ -33,11 +24,14 @@ import TierInsight from 'components/DataInsightDetail/TierInsight';
import TopActiveUsers from 'components/DataInsightDetail/TopActiveUsers';
import TopViewEntities from 'components/DataInsightDetail/TopViewEntities';
import TotalEntityInsight from 'components/DataInsightDetail/TotalEntityInsight';
import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import SearchDropdown from 'components/SearchDropdown/SearchDropdown';
import { SearchDropdownOption } from 'components/SearchDropdown/SearchDropdown.interface';
import { DEFAULT_RANGE_DATA } from 'constants/profiler.constant';
import { EntityFields } from 'enums/AdvancedSearch.enum';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { isEmpty, isEqual } from 'lodash';
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { ListItem } from 'react-awesome-query-builder';
import { useHistory, useParams } from 'react-router-dom';
@ -46,7 +40,6 @@ import { searchQuery } from 'rest/searchAPI';
import { autocomplete } from '../../constants/AdvancedSearch.constants';
import { PAGE_SIZE, ROUTES } from '../../constants/constants';
import {
DAY_FILTER,
DEFAULT_DAYS,
ENTITIES_CHARTS,
INITIAL_CHART_FILTER,
@ -64,11 +57,7 @@ import {
getDataInsightPathWithFqn,
getTeamFilter,
} from '../../utils/DataInsightUtils';
import {
getCurrentDateTimeMillis,
getFormattedDateFromMilliSeconds,
getPastDaysDateTimeMillis,
} from '../../utils/TimeUtils';
import { getFormattedDateFromMilliSeconds } from '../../utils/TimeUtils';
import { TeamStateType, TierStateType } from './DataInsight.interface';
import './DataInsight.less';
import DataInsightLeftPanel from './DataInsightLeftPanel';
@ -101,6 +90,8 @@ const DataInsightPage = () => {
useState<ChartFilter>(INITIAL_CHART_FILTER);
const [kpiList, setKpiList] = useState<Array<Kpi>>([]);
const [selectedDaysFilter, setSelectedDaysFilter] = useState(DEFAULT_DAYS);
const [dateRangeObject, setDateRangeObject] =
useState<DateRangeObject>(DEFAULT_RANGE_DATA);
const [selectedChart, setSelectedChart] = useState<DataInsightChartType>();
@ -133,13 +124,20 @@ const DataInsightPage = () => {
}));
};
const handleDaysChange = (days: number) => {
setSelectedDaysFilter(days);
setChartFilter((previous) => ({
...previous,
startTs: getPastDaysDateTimeMillis(days),
endTs: getCurrentDateTimeMillis(),
}));
const handleDateRangeChange = (
value: DateRangeObject,
daysValue?: number
) => {
if (!isEqual(value, dateRangeObject)) {
setDateRangeObject(value);
setSelectedDaysFilter(daysValue ?? 0);
setChartFilter((previous) => ({
...previous,
// Converting coming data to milliseconds
startTs: value.startTs * 1000,
endTs: value.endTs * 1000,
}));
}
};
const handleTeamChange = (teams: SearchDropdownOption[] = []) => {
@ -341,11 +339,9 @@ const DataInsightPage = () => {
'dd MMM yyyy'
)}`}
</Typography>
<Select
className="data-insight-select-dropdown"
defaultValue={DEFAULT_DAYS}
options={DAY_FILTER}
onChange={handleDaysChange}
<DatePickerMenu
handleDateRangeChange={handleDateRangeChange}
showSelectedCustomRange={false}
/>
</Space>
</Space>

View File

@ -20,8 +20,10 @@ import {
OperationPermission,
ResourceEntity,
} from 'components/PermissionProvider/PermissionProvider.interface';
import { DateRangeObject } from 'components/ProfilerDashboard/component/TestSummary';
import ProfilerDashboard from 'components/ProfilerDashboard/ProfilerDashboard';
import { ProfilerDashboardTab } from 'components/ProfilerDashboard/profilerDashboard.interface';
import { DEFAULT_RANGE_DATA } from 'constants/profiler.constant';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import React, { useEffect, useState } from 'react';
@ -46,10 +48,6 @@ import {
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getDecodedFqn } from '../../utils/StringsUtils';
import { generateEntityLink } from '../../utils/TableUtils';
import {
getCurrentDateTimeStamp,
getPastDatesTimeStampFromCurrentDate,
} from '../../utils/TimeUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const ProfilerDashboardPage = () => {
@ -89,16 +87,15 @@ const ProfilerDashboardPage = () => {
}
};
const fetchProfilerData = async (fqn: string, days = 3) => {
const fetchProfilerData = async (
fqn: string,
dateRangeObject?: DateRangeObject
) => {
try {
const startTs = getPastDatesTimeStampFromCurrentDate(days);
const endTs = getCurrentDateTimeStamp();
const { data } = await getColumnProfilerList(fqn, {
startTs,
endTs,
});
const { data } = await getColumnProfilerList(
fqn,
dateRangeObject ?? DEFAULT_RANGE_DATA
);
setProfilerData(data);
} catch (error) {
showErrorToast(error as AxiosError);

View File

@ -0,0 +1,37 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { t } from 'i18next';
export const getTimestampLabel = (
startDate: string,
endDate: string,
showSelectedCustomRange?: boolean
) => {
let label = t('label.custom-range');
if (showSelectedCustomRange) {
label += `: ${startDate} -> ${endDate}`;
}
return label;
};
export const getDaysCount = (startDate: string, endDate: string) => {
const startDateObj = new Date(startDate);
const endDateObj = new Date(endDate);
const timeDifference = endDateObj.getTime() - startDateObj.getTime();
// Dividing time difference with number of milliseconds in a day to get number of days
const numOfDays = timeDifference / (1000 * 60 * 60 * 24);
return numOfDays;
};