mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-25 09:28:23 +00:00
Feat: Column level profiler dashboard (#6683)
* added column dashboard page and route * added redirection from table to profiler dashboard * fetch the requierd information from api * added loader and error placeholder * added profiler dashboard component * added table summery in column profiler page * added tab section * added range dropdown and add test button * added chart for column level dashboard * added formatting for chart and data * extracted profiler tab as componene * added table level profiler card * added indicator for test summery * added description summary card * added latest profiler filter card * removed bottom border and added padding-x for table cell * show laast 7 days opption * added content on tab switch for profiler and data quality * remove loading state and updated summary spelling * removed latest option from profiler dropdown and relavent code * added more metrics in the graph and made default dropdown option as last 3 days * change style of Legend component * fixed failing test * restrict float number to show only 2 decimal * addressing comments * fixed failing unit test
This commit is contained in:
parent
36338eb727
commit
bbe424dbed
@ -18,11 +18,13 @@ import { CreateTableTest } from '../generated/api/tests/createTableTest';
|
|||||||
import {
|
import {
|
||||||
ColumnTestType,
|
ColumnTestType,
|
||||||
Table,
|
Table,
|
||||||
|
TableProfile,
|
||||||
TableProfilerConfig,
|
TableProfilerConfig,
|
||||||
} from '../generated/entity/data/table';
|
} from '../generated/entity/data/table';
|
||||||
import { TableTestType } from '../generated/tests/tableTest';
|
import { TableTestType } from '../generated/tests/tableTest';
|
||||||
import { EntityHistory } from '../generated/type/entityHistory';
|
import { EntityHistory } from '../generated/type/entityHistory';
|
||||||
import { EntityReference } from '../generated/type/entityReference';
|
import { EntityReference } from '../generated/type/entityReference';
|
||||||
|
import { Paging } from '../generated/type/paging';
|
||||||
import { getURLWithQueryFields } from '../utils/APIUtils';
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
import APIClient from './index';
|
import APIClient from './index';
|
||||||
|
|
||||||
@ -212,3 +214,23 @@ export const putTableProfileConfig = async (
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTableProfilesList = async (
|
||||||
|
tableId: string,
|
||||||
|
params?: {
|
||||||
|
startTs?: number;
|
||||||
|
endTs?: number;
|
||||||
|
limit?: number;
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const url = `/tables/${tableId}/tableProfile`;
|
||||||
|
|
||||||
|
const response = await APIClient.get<{
|
||||||
|
data: TableProfile[];
|
||||||
|
paging: Paging;
|
||||||
|
}>(url, { params });
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,353 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Collate
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button, Col, Radio, Row, Select, Space } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { EntityTags, ExtraInfo } from 'Models';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { addFollower, removeFollower } from '../../axiosAPIs/tableAPI';
|
||||||
|
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||||
|
import {
|
||||||
|
getDatabaseDetailsPath,
|
||||||
|
getDatabaseSchemaDetailsPath,
|
||||||
|
getServiceDetailsPath,
|
||||||
|
getTableTabPath,
|
||||||
|
getTeamAndUserDetailsPath,
|
||||||
|
} from '../../constants/constants';
|
||||||
|
import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant';
|
||||||
|
import { EntityType, FqnPart } from '../../enums/entity.enum';
|
||||||
|
import { ServiceCategory } from '../../enums/service.enum';
|
||||||
|
import { OwnerType } from '../../enums/user.enum';
|
||||||
|
import { Column, Table } from '../../generated/entity/data/table';
|
||||||
|
import { EntityReference } from '../../generated/type/entityReference';
|
||||||
|
import { LabelType, State } from '../../generated/type/tagLabel';
|
||||||
|
import jsonData from '../../jsons/en';
|
||||||
|
import {
|
||||||
|
getCurrentUserId,
|
||||||
|
getEntityName,
|
||||||
|
getEntityPlaceHolder,
|
||||||
|
getNameFromFQN,
|
||||||
|
getPartialNameFromTableFQN,
|
||||||
|
hasEditAccess,
|
||||||
|
} from '../../utils/CommonUtils';
|
||||||
|
import { serviceTypeLogo } from '../../utils/ServiceUtils';
|
||||||
|
import {
|
||||||
|
getTagsWithoutTier,
|
||||||
|
getTierTags,
|
||||||
|
getUsagePercentile,
|
||||||
|
} from '../../utils/TableUtils';
|
||||||
|
import { showErrorToast } from '../../utils/ToastUtils';
|
||||||
|
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||||
|
import PageLayout from '../containers/PageLayout';
|
||||||
|
import ProfilerTab from './component/ProfilerTab';
|
||||||
|
import {
|
||||||
|
ProfilerDashboardProps,
|
||||||
|
ProfilerDashboardTab,
|
||||||
|
} from './profilerDashboard.interface';
|
||||||
|
import './profilerDashboard.less';
|
||||||
|
|
||||||
|
const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
|
||||||
|
table,
|
||||||
|
fetchProfilerData,
|
||||||
|
profilerData,
|
||||||
|
onTableChange,
|
||||||
|
}) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const { entityTypeFQN } = useParams<Record<string, string>>();
|
||||||
|
const [follower, setFollower] = useState<EntityReference[]>([]);
|
||||||
|
const [isFollowing, setIsFollowing] = useState<boolean>(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<ProfilerDashboardTab>(
|
||||||
|
ProfilerDashboardTab.PROFILER
|
||||||
|
);
|
||||||
|
const [selectedTimeRange, setSelectedTimeRange] =
|
||||||
|
useState<keyof typeof PROFILER_FILTER_RANGE>('last3days');
|
||||||
|
const [activeColumnDetails, setActiveColumnDetails] = useState<Column>(
|
||||||
|
{} as Column
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeRangeOption = useMemo(() => {
|
||||||
|
return Object.entries(PROFILER_FILTER_RANGE).map(([key, value]) => ({
|
||||||
|
label: value.title,
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tier = useMemo(() => getTierTags(table.tags ?? []), [table]);
|
||||||
|
const breadcrumb = useMemo(() => {
|
||||||
|
const serviceName = getEntityName(table.service);
|
||||||
|
const fqn = table.fullyQualifiedName || '';
|
||||||
|
const columnName = getNameFromFQN(entityTypeFQN);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: getEntityName(table.service),
|
||||||
|
url: serviceName
|
||||||
|
? getServiceDetailsPath(
|
||||||
|
serviceName,
|
||||||
|
ServiceCategory.DATABASE_SERVICES
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
imgSrc: table.serviceType
|
||||||
|
? serviceTypeLogo(table.serviceType)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: getPartialNameFromTableFQN(fqn, [FqnPart.Database]),
|
||||||
|
url: getDatabaseDetailsPath(fqn),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: getPartialNameFromTableFQN(fqn, [FqnPart.Schema]),
|
||||||
|
url: getDatabaseSchemaDetailsPath(fqn),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: getEntityName(table),
|
||||||
|
url: getTableTabPath(table.fullyQualifiedName || ''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
url: '',
|
||||||
|
activeTitle: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [table]);
|
||||||
|
|
||||||
|
const extraInfo: Array<ExtraInfo> = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'Owner',
|
||||||
|
value:
|
||||||
|
table.owner?.type === OwnerType.TEAM
|
||||||
|
? getTeamAndUserDetailsPath(table.owner?.name || '')
|
||||||
|
: getEntityName(table.owner),
|
||||||
|
placeholderText: getEntityPlaceHolder(
|
||||||
|
getEntityName(table.owner),
|
||||||
|
table.owner?.deleted
|
||||||
|
),
|
||||||
|
isLink: table.owner?.type === OwnerType.TEAM,
|
||||||
|
openInNewTab: false,
|
||||||
|
profileName:
|
||||||
|
table.owner?.type === OwnerType.USER ? table.owner?.name : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Tier',
|
||||||
|
value: tier?.tagFQN ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : '',
|
||||||
|
},
|
||||||
|
{ key: 'Type', value: `${table.tableType}`, showLabel: true },
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
getUsagePercentile(
|
||||||
|
table.usageSummary?.weeklyStats?.percentileRank || 0,
|
||||||
|
true
|
||||||
|
) || '--',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${
|
||||||
|
table.usageSummary?.weeklyStats?.count.toLocaleString() || '--'
|
||||||
|
} queries`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [table]);
|
||||||
|
|
||||||
|
const handleOwnerUpdate = (newOwner?: Table['owner']) => {
|
||||||
|
if (newOwner) {
|
||||||
|
const updatedTableDetails = {
|
||||||
|
...table,
|
||||||
|
owner: {
|
||||||
|
...table.owner,
|
||||||
|
...newOwner,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
onTableChange(updatedTableDetails);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTierUpdate = (newTier?: string) => {
|
||||||
|
if (newTier) {
|
||||||
|
const tierTag: Table['tags'] = newTier
|
||||||
|
? [
|
||||||
|
...getTagsWithoutTier(table.tags as Array<EntityTags>),
|
||||||
|
{
|
||||||
|
tagFQN: newTier,
|
||||||
|
labelType: LabelType.Manual,
|
||||||
|
state: State.Confirmed,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: table.tags;
|
||||||
|
const updatedTableDetails = {
|
||||||
|
...table,
|
||||||
|
tags: tierTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
return onTableChange(updatedTableDetails);
|
||||||
|
} else {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulates updated tags and updates table entity data for API call
|
||||||
|
* @param selectedTags
|
||||||
|
*/
|
||||||
|
const handleTagUpdate = (selectedTags?: Array<EntityTags>) => {
|
||||||
|
if (selectedTags) {
|
||||||
|
const updatedTags = [...(tier ? [tier] : []), ...selectedTags];
|
||||||
|
const updatedTable = { ...table, tags: updatedTags };
|
||||||
|
onTableChange(updatedTable);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollowTable = async () => {
|
||||||
|
try {
|
||||||
|
const data = await removeFollower(table.id, getCurrentUserId());
|
||||||
|
const { oldValue } = data.changeDescription.fieldsDeleted[0];
|
||||||
|
|
||||||
|
setFollower(
|
||||||
|
follower.filter((follower) => follower.id !== oldValue[0].id)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
error as AxiosError,
|
||||||
|
jsonData['api-error-messages']['update-entity-unfollow-error']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const followTable = async () => {
|
||||||
|
try {
|
||||||
|
const data = await addFollower(table.id, getCurrentUserId());
|
||||||
|
const { newValue } = data.changeDescription.fieldsAdded[0];
|
||||||
|
|
||||||
|
setFollower([...follower, ...newValue]);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
error as AxiosError,
|
||||||
|
jsonData['api-error-messages']['update-entity-follow-error']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFollowClick = () => {
|
||||||
|
if (isFollowing) {
|
||||||
|
setIsFollowing(false);
|
||||||
|
unfollowTable();
|
||||||
|
} else {
|
||||||
|
setIsFollowing(true);
|
||||||
|
followTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent) => {
|
||||||
|
const value = e.target.value as ProfilerDashboardTab;
|
||||||
|
if (ProfilerDashboardTab.SUMMARY === value) {
|
||||||
|
history.push(getTableTabPath(table.fullyQualifiedName || '', 'profiler'));
|
||||||
|
}
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTestClick = () => {
|
||||||
|
history.push(
|
||||||
|
getTableTabPath(table.fullyQualifiedName || '', 'data-quality')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeRangeChange = (value: keyof typeof PROFILER_FILTER_RANGE) => {
|
||||||
|
if (value !== selectedTimeRange) {
|
||||||
|
setSelectedTimeRange(value);
|
||||||
|
fetchProfilerData(table.id, PROFILER_FILTER_RANGE[value].days);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (table) {
|
||||||
|
const columnName = getNameFromFQN(entityTypeFQN);
|
||||||
|
const selectedColumn = table.columns.find(
|
||||||
|
(col) => col.name === columnName
|
||||||
|
);
|
||||||
|
setActiveColumnDetails(selectedColumn || ({} as Column));
|
||||||
|
setFollower(table?.followers || []);
|
||||||
|
setIsFollowing(
|
||||||
|
follower.some(({ id }: { id: string }) => id === getCurrentUserId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [table]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<EntityPageInfo
|
||||||
|
isTagEditable
|
||||||
|
deleted={table.deleted}
|
||||||
|
entityFqn={table.fullyQualifiedName}
|
||||||
|
entityId={table.id}
|
||||||
|
entityName={table.name}
|
||||||
|
entityType={EntityType.TABLE}
|
||||||
|
extraInfo={extraInfo}
|
||||||
|
followHandler={handleFollowClick}
|
||||||
|
followers={follower.length}
|
||||||
|
followersList={follower}
|
||||||
|
hasEditAccess={hasEditAccess(
|
||||||
|
table.owner?.type || '',
|
||||||
|
table.owner?.id || ''
|
||||||
|
)}
|
||||||
|
isFollowing={isFollowing}
|
||||||
|
tags={getTagsWithoutTier(table.tags || [])}
|
||||||
|
tagsHandler={handleTagUpdate}
|
||||||
|
tier={tier}
|
||||||
|
titleLinks={breadcrumb}
|
||||||
|
updateOwner={handleOwnerUpdate}
|
||||||
|
updateTier={handleTierUpdate}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Row justify="space-between">
|
||||||
|
<Radio.Group
|
||||||
|
buttonStyle="solid"
|
||||||
|
optionType="button"
|
||||||
|
options={Object.values(ProfilerDashboardTab)}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Space size={16}>
|
||||||
|
<Select
|
||||||
|
className="tw-w-32"
|
||||||
|
options={timeRangeOption}
|
||||||
|
value={selectedTimeRange}
|
||||||
|
onChange={handleTimeRangeChange}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={handleAddTestClick}>
|
||||||
|
Add Test
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
{activeTab === ProfilerDashboardTab.PROFILER && (
|
||||||
|
<Col span={24}>
|
||||||
|
<ProfilerTab
|
||||||
|
activeColumnDetails={activeColumnDetails}
|
||||||
|
profilerData={profilerData}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === ProfilerDashboardTab.DATA_QUALITY && (
|
||||||
|
<Col span={24}>Data Quality</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilerDashboard;
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Card, Col, Empty, Row, Space, Statistic } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Legend,
|
||||||
|
LegendValueFormatter,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { ProfilerDetailsCardProps } from '../profilerDashboard.interface';
|
||||||
|
|
||||||
|
const ProfilerDetailsCard: React.FC<ProfilerDetailsCardProps> = ({
|
||||||
|
chartCollection,
|
||||||
|
tickFormatter,
|
||||||
|
}) => {
|
||||||
|
const { data, information } = chartCollection;
|
||||||
|
|
||||||
|
const renderColorfulLegendText: LegendValueFormatter = (value, entry) => {
|
||||||
|
return <span style={{ color: entry?.color }}>{value}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="tw-rounded-md tw-border">
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Space direction="vertical" size={16}>
|
||||||
|
{information.map((info) => (
|
||||||
|
<Statistic
|
||||||
|
key={info.title}
|
||||||
|
title={<span className="tw-text-grey-body">{info.title}</span>}
|
||||||
|
value={
|
||||||
|
tickFormatter
|
||||||
|
? `${info.latestValue}${tickFormatter}`
|
||||||
|
: info.latestValue
|
||||||
|
}
|
||||||
|
valueStyle={{ color: info.color }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col span={20}>
|
||||||
|
{data.length > 0 ? (
|
||||||
|
<ResponsiveContainer minHeight={300}>
|
||||||
|
<LineChart className="tw-w-full" data={data}>
|
||||||
|
<XAxis dataKey="name" padding={{ left: 16, right: 16 }} />
|
||||||
|
|
||||||
|
<YAxis
|
||||||
|
allowDataOverflow
|
||||||
|
padding={{ top: 16, bottom: 16 }}
|
||||||
|
tickFormatter={(props) =>
|
||||||
|
tickFormatter ? `${props}${tickFormatter}` : props
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
{information.map((info) => (
|
||||||
|
<Line
|
||||||
|
dataKey={info.dataKey}
|
||||||
|
key={info.dataKey}
|
||||||
|
name={info.title}
|
||||||
|
stroke={info.color}
|
||||||
|
type="monotone"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Legend formatter={renderColorfulLegendText} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<Empty description="No Data Available" />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilerDetailsCard;
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Card, Col, Row, Statistic } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestCaseStatus } from '../../../generated/tests/tableTest';
|
||||||
|
import { formTwoDigitNmber } from '../../../utils/CommonUtils';
|
||||||
|
import TestIndicator from '../../common/TestIndicator/TestIndicator';
|
||||||
|
import { ProfilerSummaryCardProps } from '../profilerDashboard.interface';
|
||||||
|
|
||||||
|
const ProfilerSummaryCard: React.FC<ProfilerSummaryCardProps> = ({
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
showIndicator = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card className="tw-rounded-md tw-border">
|
||||||
|
<p className="tw-text-base tw-font-medium tw-mb-7">{title}</p>
|
||||||
|
<Row className="table-profiler-summary">
|
||||||
|
{data.map((item) => (
|
||||||
|
<Col className="overall-summary-card" key={item.title} span={8}>
|
||||||
|
<Statistic
|
||||||
|
title={item.title}
|
||||||
|
value={item.value}
|
||||||
|
valueRender={(prop) =>
|
||||||
|
showIndicator ? (
|
||||||
|
<TestIndicator
|
||||||
|
type={item.title as TestCaseStatus}
|
||||||
|
value={formTwoDigitNmber(item.value as number)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
prop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilerSummaryCard;
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Card, Col, Row, Statistic } from 'antd';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
INITIAL_COUNT_METRIC_VALUE,
|
||||||
|
INITIAL_MATH_METRIC_VALUE,
|
||||||
|
INITIAL_PROPORTION_METRIC_VALUE,
|
||||||
|
INITIAL_SUM_METRIC_VALUE,
|
||||||
|
} from '../../../constants/profiler.constant';
|
||||||
|
import { getNameFromFQN } from '../../../utils/CommonUtils';
|
||||||
|
import Ellipses from '../../common/Ellipses/Ellipses';
|
||||||
|
import {
|
||||||
|
MetricChartType,
|
||||||
|
ProfilerTabProps,
|
||||||
|
} from '../profilerDashboard.interface';
|
||||||
|
import ProfilerDetailsCard from './ProfilerDetailsCard';
|
||||||
|
import ProfilerSummaryCard from './ProfilerSummaryCard';
|
||||||
|
|
||||||
|
const ProfilerTab: React.FC<ProfilerTabProps> = ({
|
||||||
|
activeColumnDetails,
|
||||||
|
profilerData,
|
||||||
|
}) => {
|
||||||
|
const { entityTypeFQN } = useParams<Record<string, string>>();
|
||||||
|
const [countMetrics, setCountMetrics] = useState<MetricChartType>(
|
||||||
|
INITIAL_COUNT_METRIC_VALUE
|
||||||
|
);
|
||||||
|
const [proportionMetrics, setProportionMetrics] = useState<MetricChartType>(
|
||||||
|
INITIAL_PROPORTION_METRIC_VALUE
|
||||||
|
);
|
||||||
|
const [mathMetrics, setMathMetrics] = useState<MetricChartType>(
|
||||||
|
INITIAL_MATH_METRIC_VALUE
|
||||||
|
);
|
||||||
|
const [sumMetrics, setSumMetrics] = useState<MetricChartType>(
|
||||||
|
INITIAL_SUM_METRIC_VALUE
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableState = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: 'Row Count',
|
||||||
|
value: profilerData[0]?.rowCount || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Column Count',
|
||||||
|
value: profilerData[0]?.columnCount || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Table Sample %',
|
||||||
|
value: `${profilerData[0]?.profileSample || 100}%`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[profilerData]
|
||||||
|
);
|
||||||
|
const testSummary = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: 'Success',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Aborted',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Failed',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMetricsChartData = () => {
|
||||||
|
const columnName = getNameFromFQN(entityTypeFQN);
|
||||||
|
const countMetricData: MetricChartType['data'] = [];
|
||||||
|
const proportionMetricData: MetricChartType['data'] = [];
|
||||||
|
const mathMetricData: MetricChartType['data'] = [];
|
||||||
|
const sumMetricData: MetricChartType['data'] = [];
|
||||||
|
profilerData.forEach((item) => {
|
||||||
|
const x = moment.unix(item.timestamp || 0).format('DD/MMM HH:mm');
|
||||||
|
const col = item.columnProfile?.find((col) => col.name === columnName);
|
||||||
|
|
||||||
|
countMetricData.push({
|
||||||
|
name: x,
|
||||||
|
timestamp: item.timestamp || 0,
|
||||||
|
distinctCount: col?.distinctCount || 0,
|
||||||
|
nullCount: col?.nullCount || 0,
|
||||||
|
uniqueCount: col?.uniqueCount || 0,
|
||||||
|
valuesCount: col?.valuesCount || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
sumMetricData.push({
|
||||||
|
name: x,
|
||||||
|
timestamp: item.timestamp || 0,
|
||||||
|
sum: col?.sum || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
mathMetricData.push({
|
||||||
|
name: x,
|
||||||
|
timestamp: item.timestamp || 0,
|
||||||
|
max: (col?.max as number) || 0,
|
||||||
|
min: (col?.min as number) || 0,
|
||||||
|
mean: col?.mean || 0,
|
||||||
|
median: col?.median || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
proportionMetricData.push({
|
||||||
|
name: x,
|
||||||
|
timestamp: item.timestamp || 0,
|
||||||
|
distinctProportion: col?.distinctProportion || 0,
|
||||||
|
nullProportion: col?.nullProportion || 0,
|
||||||
|
uniqueProportion: col?.uniqueProportion || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const countMetricInfo = countMetrics.information.map((item) => ({
|
||||||
|
...item,
|
||||||
|
latestValue: countMetricData[0][item.dataKey],
|
||||||
|
}));
|
||||||
|
const proportionMetricInfo = proportionMetrics.information.map((item) => ({
|
||||||
|
...item,
|
||||||
|
latestValue: parseFloat(
|
||||||
|
`${proportionMetricData[0][item.dataKey]}`
|
||||||
|
).toFixed(2),
|
||||||
|
}));
|
||||||
|
const mathMetricInfo = mathMetrics.information.map((item) => ({
|
||||||
|
...item,
|
||||||
|
latestValue: mathMetricData[0][item.dataKey],
|
||||||
|
}));
|
||||||
|
const sumMetricInfo = sumMetrics.information.map((item) => ({
|
||||||
|
...item,
|
||||||
|
latestValue: sumMetricData[0][item.dataKey],
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCountMetrics((pre) => ({
|
||||||
|
...pre,
|
||||||
|
information: countMetricInfo,
|
||||||
|
data: countMetricData,
|
||||||
|
}));
|
||||||
|
setProportionMetrics((pre) => ({
|
||||||
|
...pre,
|
||||||
|
information: proportionMetricInfo,
|
||||||
|
data: proportionMetricData,
|
||||||
|
}));
|
||||||
|
setMathMetrics((pre) => ({
|
||||||
|
...pre,
|
||||||
|
information: mathMetricInfo,
|
||||||
|
data: mathMetricData,
|
||||||
|
}));
|
||||||
|
setSumMetrics((pre) => ({
|
||||||
|
...pre,
|
||||||
|
information: sumMetricInfo,
|
||||||
|
data: sumMetricData,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createMetricsChartData();
|
||||||
|
}, [profilerData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card className="tw-rounded-md tw-border tw-h-full">
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={18}>
|
||||||
|
<p className="tw-font-medium tw-text-base">Column summary</p>
|
||||||
|
<Ellipses className="tw-text-grey-muted" rows={4}>
|
||||||
|
{activeColumnDetails.description}
|
||||||
|
</Ellipses>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Statistic
|
||||||
|
title="Data type"
|
||||||
|
value={activeColumnDetails.dataTypeDisplay || ''}
|
||||||
|
valueStyle={{
|
||||||
|
color: '#1890FF',
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<ProfilerSummaryCard data={tableState} title="Table Metrics Summary" />
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<ProfilerSummaryCard
|
||||||
|
showIndicator
|
||||||
|
data={testSummary}
|
||||||
|
title="Quality Tests Summary"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<ProfilerDetailsCard chartCollection={countMetrics} />
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<ProfilerDetailsCard
|
||||||
|
chartCollection={proportionMetrics}
|
||||||
|
tickFormatter="%"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<ProfilerDetailsCard chartCollection={mathMetrics} />
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<ProfilerDetailsCard chartCollection={sumMetrics} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilerTab;
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Column, Table, TableProfile } from '../../generated/entity/data/table';
|
||||||
|
|
||||||
|
export interface ProfilerDashboardProps {
|
||||||
|
onTableChange: (table: Table) => void;
|
||||||
|
table: Table;
|
||||||
|
profilerData: TableProfile[];
|
||||||
|
fetchProfilerData: (tableId: string, days?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetricChartType = {
|
||||||
|
information: {
|
||||||
|
title: string;
|
||||||
|
dataKey: string;
|
||||||
|
color: string;
|
||||||
|
latestValue?: string | number;
|
||||||
|
}[];
|
||||||
|
data: Record<string, string | number>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProfilerDetailsCardProps {
|
||||||
|
chartCollection: MetricChartType;
|
||||||
|
tickFormatter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProfilerDashboardTab {
|
||||||
|
SUMMARY = 'Summary',
|
||||||
|
PROFILER = 'Profiler',
|
||||||
|
DATA_QUALITY = 'Data Quality',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartData = {
|
||||||
|
name: string;
|
||||||
|
proportion?: number;
|
||||||
|
value: number;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChartCollection = {
|
||||||
|
data: ChartData[];
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChartDataCollection = Record<string, ChartCollection>;
|
||||||
|
|
||||||
|
export interface ProfilerTabProps {
|
||||||
|
profilerData: TableProfile[];
|
||||||
|
activeColumnDetails: Column;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfilerSummaryCardProps {
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
value: number | string;
|
||||||
|
}[];
|
||||||
|
showIndicator?: boolean;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.table-profiler-summary {
|
||||||
|
.overall-summary-card:not(:first-child) {
|
||||||
|
border-left: 1px solid rgb(229, 231, 235);
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-profiler-data-container {
|
||||||
|
table {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table.ant-table-small .ant-table-thead > tr > th,
|
||||||
|
.ant-table.ant-table-small .ant-table-tbody > tr > td {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Column, ColumnProfile } from '../../../generated/entity/data/table';
|
import { Column, ColumnProfile } from '../../../generated/entity/data/table';
|
||||||
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
|
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
|
||||||
import { ColumnProfileTableProps } from '../TableProfiler.interface';
|
import { ColumnProfileTableProps } from '../TableProfiler.interface';
|
||||||
@ -88,7 +89,7 @@ jest.mock('./ProfilerProgressWidget', () => {
|
|||||||
</span>
|
</span>
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
jest.mock('./TestIndicator', () => {
|
jest.mock('../../common/TestIndicator/TestIndicator', () => {
|
||||||
return jest.fn().mockImplementation(({ value, type }) => (
|
return jest.fn().mockImplementation(({ value, type }) => (
|
||||||
<span data-testid="test-indicator">
|
<span data-testid="test-indicator">
|
||||||
{value} <span>{type}</span>
|
{value} <span>{type}</span>
|
||||||
@ -109,7 +110,9 @@ describe('Test ColumnProfileTable component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render without crashing', async () => {
|
it('should render without crashing', async () => {
|
||||||
render(<ColumnProfileTable {...mockProps} />);
|
render(<ColumnProfileTable {...mockProps} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
const container = await screen.findByTestId(
|
const container = await screen.findByTestId(
|
||||||
'column-profile-table-container'
|
'column-profile-table-container'
|
||||||
@ -125,7 +128,10 @@ describe('Test ColumnProfileTable component', () => {
|
|||||||
<ColumnProfileTable
|
<ColumnProfileTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
columns={undefined as unknown as Column[]}
|
columns={undefined as unknown as Column[]}
|
||||||
/>
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const container = await screen.findByTestId(
|
const container = await screen.findByTestId(
|
||||||
@ -138,7 +144,9 @@ describe('Test ColumnProfileTable component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('search box should work as expected', async () => {
|
it('search box should work as expected', async () => {
|
||||||
render(<ColumnProfileTable {...mockProps} />);
|
render(<ColumnProfileTable {...mockProps} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
const searchbox = await screen.findByTestId('searchbar');
|
const searchbox = await screen.findByTestId('searchbar');
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import { Button, Space, Table } from 'antd';
|
import { Button, Space, Table } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
PRIMERY_COLOR,
|
PRIMERY_COLOR,
|
||||||
SECONDARY_COLOR,
|
SECONDARY_COLOR,
|
||||||
@ -23,11 +24,12 @@ import { ColumnProfile } from '../../../generated/entity/data/table';
|
|||||||
import { TestCaseStatus } from '../../../generated/tests/tableTest';
|
import { TestCaseStatus } from '../../../generated/tests/tableTest';
|
||||||
import { formatNumberWithComma } from '../../../utils/CommonUtils';
|
import { formatNumberWithComma } from '../../../utils/CommonUtils';
|
||||||
import { getCurrentDatasetTab } from '../../../utils/DatasetDetailsUtils';
|
import { getCurrentDatasetTab } from '../../../utils/DatasetDetailsUtils';
|
||||||
|
import { getProfilerDashboardWithFqnPath } from '../../../utils/RouterUtils';
|
||||||
import Ellipses from '../../common/Ellipses/Ellipses';
|
import Ellipses from '../../common/Ellipses/Ellipses';
|
||||||
import Searchbar from '../../common/searchbar/Searchbar';
|
import Searchbar from '../../common/searchbar/Searchbar';
|
||||||
|
import TestIndicator from '../../common/TestIndicator/TestIndicator';
|
||||||
import { ColumnProfileTableProps } from '../TableProfiler.interface';
|
import { ColumnProfileTableProps } from '../TableProfiler.interface';
|
||||||
import ProfilerProgressWidget from './ProfilerProgressWidget';
|
import ProfilerProgressWidget from './ProfilerProgressWidget';
|
||||||
import TestIndicator from './TestIndicator';
|
|
||||||
|
|
||||||
const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
|
const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
|
||||||
columnProfile,
|
columnProfile,
|
||||||
@ -57,6 +59,18 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
|
|||||||
title: 'Name',
|
title: 'Name',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
|
render: (name: string) => {
|
||||||
|
const data = columns.find((col) => col.name === name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={getProfilerDashboardWithFqnPath(
|
||||||
|
data?.fullyQualifiedName || ''
|
||||||
|
)}>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Data Type',
|
title: 'Data Type',
|
||||||
|
@ -30,7 +30,7 @@ import {
|
|||||||
codeMirrorOption,
|
codeMirrorOption,
|
||||||
DEFAULT_INCLUDE_PROFILE,
|
DEFAULT_INCLUDE_PROFILE,
|
||||||
PROFILER_METRIC,
|
PROFILER_METRIC,
|
||||||
} from '../../../constants/entity.constants';
|
} from '../../../constants/profiler.constant';
|
||||||
import {
|
import {
|
||||||
ColumnProfilerConfig,
|
ColumnProfilerConfig,
|
||||||
TableProfilerConfig,
|
TableProfilerConfig,
|
||||||
@ -49,7 +49,7 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<TableProfilerConfig>();
|
const [data, setData] = useState<TableProfilerConfig>();
|
||||||
const [sqlQuery, setSqlQuery] = useState<string>('');
|
const [sqlQuery, setSqlQuery] = useState<string>('');
|
||||||
const [profileSample, setProfileSample] = useState<number>(0);
|
const [profileSample, setProfileSample] = useState<number>(100);
|
||||||
const [excludeCol, setExcludeCol] = useState<string[]>([]);
|
const [excludeCol, setExcludeCol] = useState<string[]>([]);
|
||||||
const [includeCol, setIncludeCol] = useState<ColumnProfilerConfig[]>(
|
const [includeCol, setIncludeCol] = useState<ColumnProfilerConfig[]>(
|
||||||
DEFAULT_INCLUDE_PROFILE
|
DEFAULT_INCLUDE_PROFILE
|
||||||
@ -81,7 +81,7 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
|
|||||||
const updateInitialConfig = (tableProfilerConfig: TableProfilerConfig) => {
|
const updateInitialConfig = (tableProfilerConfig: TableProfilerConfig) => {
|
||||||
const { includeColumns } = tableProfilerConfig;
|
const { includeColumns } = tableProfilerConfig;
|
||||||
setSqlQuery(tableProfilerConfig.profileQuery || '');
|
setSqlQuery(tableProfilerConfig.profileQuery || '');
|
||||||
setProfileSample(tableProfilerConfig.profileSample || 0);
|
setProfileSample(tableProfilerConfig.profileSample || 100);
|
||||||
setExcludeCol(tableProfilerConfig.excludeColumns || []);
|
setExcludeCol(tableProfilerConfig.excludeColumns || []);
|
||||||
if (includeColumns && includeColumns?.length > 0) {
|
if (includeColumns && includeColumns?.length > 0) {
|
||||||
const includeColValue = includeColumns.map((col) => {
|
const includeColValue = includeColumns.map((col) => {
|
||||||
|
@ -51,7 +51,7 @@ export interface ProfilerSettingsModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TestIndicatorProps {
|
export interface TestIndicatorProps {
|
||||||
value: number;
|
value: number | string;
|
||||||
type: TestCaseStatus;
|
type: TestCaseStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,23 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-indicator {
|
|
||||||
display: inline-block;
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
background: @succesColor;
|
|
||||||
}
|
|
||||||
&.failed {
|
|
||||||
background: @failedColor;
|
|
||||||
}
|
|
||||||
&.aborted {
|
|
||||||
background: @abortedColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-row .ant-table-cell:first-child,
|
.ant-table-row .ant-table-cell:first-child,
|
||||||
.ant-table-thead .ant-table-cell:first-child {
|
.ant-table-thead .ant-table-cell:first-child {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
import { cleanup, render, screen } from '@testing-library/react';
|
import { cleanup, render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestCaseStatus } from '../../../generated/tests/tableTest';
|
import { TestCaseStatus } from '../../../generated/tests/tableTest';
|
||||||
import { TestIndicatorProps } from '../TableProfiler.interface';
|
import { TestIndicatorProps } from '../../TableProfiler/TableProfiler.interface';
|
||||||
import TestIndicator from './TestIndicator';
|
import TestIndicator from './TestIndicator';
|
||||||
|
|
||||||
const mockProps: TestIndicatorProps = {
|
const mockProps: TestIndicatorProps = {
|
@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestIndicatorProps } from '../TableProfiler.interface';
|
import { TestIndicatorProps } from '../../TableProfiler/TableProfiler.interface';
|
||||||
|
import './testIndicator.less';
|
||||||
|
|
||||||
const TestIndicator: React.FC<TestIndicatorProps> = ({ value, type }) => {
|
const TestIndicator: React.FC<TestIndicatorProps> = ({ value, type }) => {
|
||||||
return (
|
return (
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@succesColor: #28a745;
|
||||||
|
@failedColor: #cb2431;
|
||||||
|
@abortedColor: #efae2f;
|
||||||
|
|
||||||
|
.test-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: @succesColor;
|
||||||
|
}
|
||||||
|
&.failed {
|
||||||
|
background: @failedColor;
|
||||||
|
}
|
||||||
|
&.aborted {
|
||||||
|
background: @abortedColor;
|
||||||
|
}
|
||||||
|
}
|
@ -65,7 +65,7 @@ interface Props {
|
|||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
followers?: number;
|
followers?: number;
|
||||||
extraInfo: Array<ExtraInfo>;
|
extraInfo: Array<ExtraInfo>;
|
||||||
tier: TagLabel;
|
tier: TagLabel | undefined;
|
||||||
tags: Array<EntityTags>;
|
tags: Array<EntityTags>;
|
||||||
isTagEditable?: boolean;
|
isTagEditable?: boolean;
|
||||||
owner?: EntityReference;
|
owner?: EntityReference;
|
||||||
|
@ -217,6 +217,7 @@ export const ROUTES = {
|
|||||||
CUSTOM_PROPERTIES: `/custom-properties`,
|
CUSTOM_PROPERTIES: `/custom-properties`,
|
||||||
CUSTOM_ENTITY_DETAIL: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
CUSTOM_ENTITY_DETAIL: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
||||||
ADD_CUSTOM_PROPERTY: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}/add-field`,
|
ADD_CUSTOM_PROPERTY: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}/add-field`,
|
||||||
|
PROFILER_DASHBOARD: `/profiler-dashboard/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
||||||
|
|
||||||
// Tasks Routes
|
// Tasks Routes
|
||||||
REQUEST_DESCRIPTION: `/request-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
REQUEST_DESCRIPTION: `/request-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||||
|
@ -11,61 +11,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CSMode } from '../enums/codemirror.enum';
|
|
||||||
import { ColumnProfilerConfig } from '../generated/entity/data/table';
|
|
||||||
import { JSON_TAB_SIZE } from './constants';
|
|
||||||
|
|
||||||
export const ENTITY_DELETE_STATE = {
|
export const ENTITY_DELETE_STATE = {
|
||||||
loading: 'initial',
|
loading: 'initial',
|
||||||
state: false,
|
state: false,
|
||||||
softDelete: true,
|
softDelete: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PROFILER_METRIC = [
|
|
||||||
'valuesCount',
|
|
||||||
'valuesPercentage',
|
|
||||||
'validCount',
|
|
||||||
'duplicateCount',
|
|
||||||
'nullCount',
|
|
||||||
'nullProportion',
|
|
||||||
'missingPercentage',
|
|
||||||
'missingCount',
|
|
||||||
'uniqueCount',
|
|
||||||
'uniqueProportion',
|
|
||||||
'distinctCount',
|
|
||||||
'distinctProportion',
|
|
||||||
'min',
|
|
||||||
'max',
|
|
||||||
'minLength',
|
|
||||||
'maxLength',
|
|
||||||
'mean',
|
|
||||||
'sum',
|
|
||||||
'stddev',
|
|
||||||
'variance',
|
|
||||||
'median',
|
|
||||||
'histogram',
|
|
||||||
'customMetricsProfile',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DEFAULT_INCLUDE_PROFILE: ColumnProfilerConfig[] = [
|
|
||||||
{
|
|
||||||
columnName: undefined,
|
|
||||||
metrics: ['all'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const codeMirrorOption = {
|
|
||||||
tabSize: JSON_TAB_SIZE,
|
|
||||||
indentUnit: JSON_TAB_SIZE,
|
|
||||||
indentWithTabs: true,
|
|
||||||
lineNumbers: true,
|
|
||||||
lineWrapping: true,
|
|
||||||
styleActiveLine: true,
|
|
||||||
matchBrackets: true,
|
|
||||||
autoCloseBrackets: true,
|
|
||||||
foldGutter: true,
|
|
||||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
|
||||||
mode: {
|
|
||||||
name: CSMode.SQL,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { CSMode } from '../enums/codemirror.enum';
|
||||||
|
import { ColumnProfilerConfig } from '../generated/entity/data/table';
|
||||||
|
import { JSON_TAB_SIZE } from './constants';
|
||||||
|
|
||||||
export const excludedMetrics = [
|
export const excludedMetrics = [
|
||||||
'profilDate',
|
'profilDate',
|
||||||
'name',
|
'name',
|
||||||
@ -24,3 +28,151 @@ export const excludedMetrics = [
|
|||||||
'missingPercentage',
|
'missingPercentage',
|
||||||
'distinctProportion',
|
'distinctProportion',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PROFILER_METRIC = [
|
||||||
|
'valuesCount',
|
||||||
|
'valuesPercentage',
|
||||||
|
'validCount',
|
||||||
|
'duplicateCount',
|
||||||
|
'nullCount',
|
||||||
|
'nullProportion',
|
||||||
|
'missingPercentage',
|
||||||
|
'missingCount',
|
||||||
|
'uniqueCount',
|
||||||
|
'uniqueProportion',
|
||||||
|
'distinctCount',
|
||||||
|
'distinctProportion',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'minLength',
|
||||||
|
'maxLength',
|
||||||
|
'mean',
|
||||||
|
'sum',
|
||||||
|
'stddev',
|
||||||
|
'variance',
|
||||||
|
'median',
|
||||||
|
'histogram',
|
||||||
|
'customMetricsProfile',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PROFILER_FILTER_RANGE = {
|
||||||
|
last3days: { days: 3, title: 'Last 3 days' },
|
||||||
|
last7days: { days: 7, title: 'Last 7 days' },
|
||||||
|
last14days: { days: 14, title: 'Last 14 days' },
|
||||||
|
last30days: { days: 30, title: 'Last 30 days' },
|
||||||
|
last60days: { days: 60, title: 'Last 60 days' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CHART_COLLECTION_VALUE = {
|
||||||
|
distinctCount: { data: [], color: '#1890FF' },
|
||||||
|
uniqueCount: { data: [], color: '#008376' },
|
||||||
|
nullCount: { data: [], color: '#7147E8' },
|
||||||
|
nullProportion: { data: [], color: '#B02AAC' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_COUNT_METRIC_VALUE = {
|
||||||
|
information: [
|
||||||
|
{
|
||||||
|
title: 'Distinct Count',
|
||||||
|
dataKey: 'distinctCount',
|
||||||
|
color: '#1890FF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Null Count',
|
||||||
|
dataKey: 'nullCount',
|
||||||
|
color: '#7147E8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unique Count',
|
||||||
|
dataKey: 'uniqueCount',
|
||||||
|
color: '#008376',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Values Count',
|
||||||
|
dataKey: 'valuesCount',
|
||||||
|
color: '#B02AAC',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_PROPORTION_METRIC_VALUE = {
|
||||||
|
information: [
|
||||||
|
{
|
||||||
|
title: 'Distinct Proportion',
|
||||||
|
dataKey: 'distinctProportion',
|
||||||
|
color: '#1890FF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Null Proportion',
|
||||||
|
dataKey: 'nullProportion',
|
||||||
|
color: '#7147E8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unique Proportion',
|
||||||
|
dataKey: 'uniqueProportion',
|
||||||
|
color: '#008376',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_MATH_METRIC_VALUE = {
|
||||||
|
information: [
|
||||||
|
{
|
||||||
|
title: 'Median',
|
||||||
|
dataKey: 'median',
|
||||||
|
color: '#1890FF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Max',
|
||||||
|
dataKey: 'max',
|
||||||
|
color: '#7147E8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Mean',
|
||||||
|
dataKey: 'mean',
|
||||||
|
color: '#008376',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Min',
|
||||||
|
dataKey: 'min',
|
||||||
|
color: '#B02AAC',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_SUM_METRIC_VALUE = {
|
||||||
|
information: [
|
||||||
|
{
|
||||||
|
title: 'Sum',
|
||||||
|
dataKey: 'sum',
|
||||||
|
color: '#1890FF',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_INCLUDE_PROFILE: ColumnProfilerConfig[] = [
|
||||||
|
{
|
||||||
|
columnName: undefined,
|
||||||
|
metrics: ['all'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const codeMirrorOption = {
|
||||||
|
tabSize: JSON_TAB_SIZE,
|
||||||
|
indentUnit: JSON_TAB_SIZE,
|
||||||
|
indentWithTabs: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
styleActiveLine: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
mode: {
|
||||||
|
name: CSMode.SQL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -275,6 +275,18 @@ jest.mock('react-router-dom', () => ({
|
|||||||
useParams: jest.fn().mockImplementation(() => mockUseParams),
|
useParams: jest.fn().mockImplementation(() => mockUseParams),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../utils/CommonUtils', () => ({
|
||||||
|
addToRecentViewed: jest.fn(),
|
||||||
|
getCurrentUserId: jest.fn().mockReturnValue('test'),
|
||||||
|
getEntityMissingError: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => <span>Entity missing error</span>),
|
||||||
|
getEntityName: jest.fn().mockReturnValue('getEntityName'),
|
||||||
|
getFeedCounts: jest.fn(),
|
||||||
|
getFields: jest.fn().mockReturnValue('field'),
|
||||||
|
getPartialNameFromTableFQN: jest.fn().mockReturnValue('name'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Test DatasetDetails page', () => {
|
describe('Test DatasetDetails page', () => {
|
||||||
it('Component should render properly', async () => {
|
it('Component should render properly', async () => {
|
||||||
const { container } = render(<DatasetDetailsPage />, {
|
const { container } = render(<DatasetDetailsPage />, {
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AxiosError } from 'axios';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
getTableDetailsByFQN,
|
||||||
|
getTableProfilesList,
|
||||||
|
patchTableDetails,
|
||||||
|
} from '../../axiosAPIs/tableAPI';
|
||||||
|
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
|
import PageContainerV1 from '../../components/containers/PageContainerV1';
|
||||||
|
import Loader from '../../components/Loader/Loader';
|
||||||
|
import ProfilerDashboard from '../../components/ProfilerDashboard/ProfilerDashboard';
|
||||||
|
import { API_RES_MAX_SIZE } from '../../constants/constants';
|
||||||
|
import { Table, TableProfile } from '../../generated/entity/data/table';
|
||||||
|
import jsonData from '../../jsons/en';
|
||||||
|
import {
|
||||||
|
getNameFromFQN,
|
||||||
|
getTableFQNFromColumnFQN,
|
||||||
|
} from '../../utils/CommonUtils';
|
||||||
|
import { showErrorToast } from '../../utils/ToastUtils';
|
||||||
|
|
||||||
|
const ProfilerDashboardPage = () => {
|
||||||
|
const { entityTypeFQN } = useParams<Record<string, string>>();
|
||||||
|
const [table, setTable] = useState<Table>({} as Table);
|
||||||
|
const [profilerData, setProfilerData] = useState<TableProfile[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const fetchProfilerData = async (tableId: string, days = 3) => {
|
||||||
|
try {
|
||||||
|
const startTs = moment().subtract(days, 'days').unix();
|
||||||
|
|
||||||
|
const data = await getTableProfilesList(tableId, {
|
||||||
|
startTs: startTs,
|
||||||
|
limit: API_RES_MAX_SIZE,
|
||||||
|
});
|
||||||
|
setProfilerData(data.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTableEntity = async (fqn: string) => {
|
||||||
|
try {
|
||||||
|
getTableFQNFromColumnFQN(fqn);
|
||||||
|
const data = await getTableDetailsByFQN(
|
||||||
|
getTableFQNFromColumnFQN(fqn),
|
||||||
|
'tags, usageSummary, owner, followers'
|
||||||
|
);
|
||||||
|
setTable(data ?? ({} as Table));
|
||||||
|
fetchProfilerData(data.id);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
error as AxiosError,
|
||||||
|
jsonData['api-error-messages']['fetch-table-details-error']
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTableHandler = async (updatedTable: Table) => {
|
||||||
|
const jsonPatch = compare(table, updatedTable);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableRes = await patchTableDetails(table.id, jsonPatch);
|
||||||
|
setTable(tableRes);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
error as AxiosError,
|
||||||
|
jsonData['api-error-messages']['update-entity-error']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (entityTypeFQN) {
|
||||||
|
fetchTableEntity(entityTypeFQN);
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
}, [entityTypeFQN]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorPlaceHolder>
|
||||||
|
<p className="tw-text-center">
|
||||||
|
No data found{' '}
|
||||||
|
{entityTypeFQN ? `for column ${getNameFromFQN(entityTypeFQN)}` : ''}
|
||||||
|
</p>
|
||||||
|
</ErrorPlaceHolder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainerV1 className="tw-py-4">
|
||||||
|
<ProfilerDashboard
|
||||||
|
fetchProfilerData={fetchProfilerData}
|
||||||
|
profilerData={profilerData}
|
||||||
|
table={table}
|
||||||
|
onTableChange={updateTableHandler}
|
||||||
|
/>
|
||||||
|
</PageContainerV1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilerDashboardPage;
|
@ -23,6 +23,12 @@ const GlobalSettingPage = withSuspenseFallback(
|
|||||||
React.lazy(() => import('../pages/GlobalSettingPage/GlobalSettingPage'))
|
React.lazy(() => import('../pages/GlobalSettingPage/GlobalSettingPage'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ProfilerDashboardPage = withSuspenseFallback(
|
||||||
|
React.lazy(
|
||||||
|
() => import('../pages/ProfilerDashboardPage/ProfilerDashboardPage')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const MyDataPage = withSuspenseFallback(
|
const MyDataPage = withSuspenseFallback(
|
||||||
React.lazy(() => import('../pages/MyDataPage/MyDataPage.component'))
|
React.lazy(() => import('../pages/MyDataPage/MyDataPage.component'))
|
||||||
);
|
);
|
||||||
@ -275,6 +281,11 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
<Route exact component={UserPage} path={ROUTES.USER_PROFILE} />
|
<Route exact component={UserPage} path={ROUTES.USER_PROFILE} />
|
||||||
<Route exact component={UserPage} path={ROUTES.USER_PROFILE_WITH_TAB} />
|
<Route exact component={UserPage} path={ROUTES.USER_PROFILE_WITH_TAB} />
|
||||||
<Route exact component={MlModelPage} path={ROUTES.MLMODEL_DETAILS} />
|
<Route exact component={MlModelPage} path={ROUTES.MLMODEL_DETAILS} />
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
component={ProfilerDashboardPage}
|
||||||
|
path={ROUTES.PROFILER_DASHBOARD}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
component={MlModelPage}
|
component={MlModelPage}
|
||||||
|
@ -152,14 +152,9 @@ export const getPartialNameFromTableFQN = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentUserId = (): string => {
|
export const getCurrentUserId = (): string => {
|
||||||
// TODO: Replace below with USERID from Logged-in data
|
const currentUser = AppState.getCurrentUserDetails();
|
||||||
const { id: userId } = !isEmpty(AppState.userDetails)
|
|
||||||
? AppState.userDetails
|
|
||||||
: AppState.users?.length
|
|
||||||
? AppState.users[0]
|
|
||||||
: { id: undefined };
|
|
||||||
|
|
||||||
return userId as string;
|
return currentUser?.id || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pluralize = (count: number, noun: string, suffix = 's') => {
|
export const pluralize = (count: number, noun: string, suffix = 's') => {
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
||||||
import {
|
import {
|
||||||
IN_PAGE_SEARCH_ROUTES,
|
IN_PAGE_SEARCH_ROUTES,
|
||||||
|
PLACEHOLDER_ENTITY_TYPE_FQN,
|
||||||
PLACEHOLDER_GLOSSARY_NAME,
|
PLACEHOLDER_GLOSSARY_NAME,
|
||||||
PLACEHOLDER_GLOSSARY_TERMS_FQN,
|
PLACEHOLDER_GLOSSARY_TERMS_FQN,
|
||||||
PLACEHOLDER_ROUTE_FQN,
|
PLACEHOLDER_ROUTE_FQN,
|
||||||
@ -292,3 +293,11 @@ export const getPath = (pathName: string) => {
|
|||||||
return getSettingPath();
|
return getSettingPath();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProfilerDashboardWithFqnPath = (entityTypeFQN: string) => {
|
||||||
|
let path = ROUTES.PROFILER_DASHBOARD;
|
||||||
|
|
||||||
|
path = path.replace(PLACEHOLDER_ENTITY_TYPE_FQN, entityTypeFQN);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user