mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-19 14:37:52 +00:00
UI : Data profiler and Feed changes (#3267)
This commit is contained in:
parent
643eadb118
commit
b93a8c8d0d
@ -23,6 +23,8 @@ import React, {
|
||||
} from 'react';
|
||||
import { getFeedById } from '../../../axiosAPIs/feedsAPI';
|
||||
import { getEntityField, getReplyText } from '../../../utils/FeedUtils';
|
||||
import { Button } from '../../buttons/Button/Button';
|
||||
import PopOver from '../../common/popover/PopOver';
|
||||
import Loader from '../../Loader/Loader';
|
||||
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
|
||||
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
||||
@ -39,6 +41,7 @@ interface FeedPanelHeaderProp
|
||||
Pick<ActivityFeedPanelProp, 'onCancel'> {
|
||||
entityField: string;
|
||||
noun?: string;
|
||||
onShowNewConversation?: (v: boolean) => void;
|
||||
}
|
||||
interface FeedPanelOverlayProp
|
||||
extends HTMLAttributes<HTMLButtonElement>,
|
||||
@ -53,6 +56,7 @@ export const FeedPanelHeader: FC<FeedPanelHeaderProp> = ({
|
||||
entityField,
|
||||
className,
|
||||
noun,
|
||||
onShowNewConversation,
|
||||
}) => {
|
||||
return (
|
||||
<header className={className}>
|
||||
@ -61,21 +65,41 @@ export const FeedPanelHeader: FC<FeedPanelHeaderProp> = ({
|
||||
{noun ? noun : 'Conversation'} on{' '}
|
||||
<span className="tw-heading">{entityField}</span>
|
||||
</p>
|
||||
<svg
|
||||
className="tw-w-5 tw-h-5 tw-ml-1 tw-cursor-pointer"
|
||||
data-testid="closeDrawer"
|
||||
fill="none"
|
||||
stroke="#6B7280"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={onCancel}>
|
||||
<path
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
<div className="tw-flex">
|
||||
{onShowNewConversation ? (
|
||||
<PopOver
|
||||
position="bottom"
|
||||
title="Start conversation"
|
||||
trigger="mouseenter">
|
||||
<Button
|
||||
className={classNames('tw-h-7 tw-px-2')}
|
||||
data-testid="add-teams"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
onShowNewConversation?.(true);
|
||||
}}>
|
||||
<i aria-hidden="true" className="fa fa-plus" />
|
||||
</Button>
|
||||
</PopOver>
|
||||
) : null}
|
||||
<svg
|
||||
className="tw-w-5 tw-h-5 tw-ml-2 tw-cursor-pointer tw-self-center"
|
||||
data-testid="closeDrawer"
|
||||
fill="none"
|
||||
stroke="#6B7280"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={onCancel}>
|
||||
<path
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="tw--mx-4" />
|
||||
</header>
|
||||
|
@ -225,9 +225,15 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
|
||||
const [threads, setThreads] = useState<EntityThread[]>([]);
|
||||
const [selectedThread, setSelectedThread] = useState<EntityThread>();
|
||||
const [selectedThreadId, setSelectedThreadId] = useState<string>('');
|
||||
const [showNewConversation, setShowNewConversation] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const entityField = getEntityField(threadLink);
|
||||
|
||||
const onShowNewConversation = (value: boolean) => {
|
||||
setShowNewConversation(value);
|
||||
};
|
||||
|
||||
const getThreads = () => {
|
||||
getAllFeeds(threadLink).then((res: AxiosResponse) => {
|
||||
const { data } = res.data;
|
||||
@ -310,6 +316,9 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
|
||||
entityField={entityField as string}
|
||||
noun="Conversations"
|
||||
onCancel={onCancel}
|
||||
onShowNewConversation={
|
||||
threads.length > 0 ? onShowNewConversation : undefined
|
||||
}
|
||||
/>
|
||||
{!isUndefined(selectedThread) ? (
|
||||
<Fragment>
|
||||
@ -326,6 +335,20 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{showNewConversation || threads.length === 0 ? (
|
||||
<div className="tw-pt-6">
|
||||
<p className="tw-ml-9 tw-mr-2 tw-my-2">
|
||||
You are starting a new conversation
|
||||
</p>
|
||||
<ActivityFeedEditor
|
||||
buttonClass="tw-mr-4"
|
||||
className="tw-ml-5 tw-mr-2"
|
||||
placeHolder="Enter a message"
|
||||
onSave={onPostThread}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ActivityThreadList
|
||||
className="tw-py-6 tw-pl-5"
|
||||
postFeed={postFeed}
|
||||
@ -334,12 +357,6 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
|
||||
onThreadIdSelect={onThreadIdSelect}
|
||||
onThreadSelect={onThreadSelect}
|
||||
/>
|
||||
<ActivityFeedEditor
|
||||
buttonClass="tw-mr-4"
|
||||
className="tw-ml-5 tw-mr-2 tw-mb-6"
|
||||
placeHolder="Enter a message"
|
||||
onSave={onPostThread}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@ export interface DatasetDetailsProps {
|
||||
showTestForm: boolean;
|
||||
qualityTestFormHandler: (
|
||||
tabValue: number,
|
||||
testMode: DatasetTestModeType
|
||||
testMode?: DatasetTestModeType
|
||||
) => void;
|
||||
handleShowTestForm: (value: boolean) => void;
|
||||
handleTestModeChange: (mode: DatasetTestModeType) => void;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
ColumnTest,
|
||||
DatasetTestModeType,
|
||||
} from '../../interface/dataQuality.interface';
|
||||
import { getRoundedValue } from '../../utils/ProfilerUtils';
|
||||
import { getConstraintIcon } from '../../utils/TableUtils';
|
||||
import { Button } from '../buttons/Button/Button';
|
||||
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
||||
@ -38,29 +39,26 @@ type Props = {
|
||||
}>;
|
||||
qualityTestFormHandler: (
|
||||
tabValue: number,
|
||||
testMode: DatasetTestModeType
|
||||
testMode?: DatasetTestModeType
|
||||
) => void;
|
||||
};
|
||||
|
||||
const PercentageGraph = ({
|
||||
percentage,
|
||||
title,
|
||||
}: {
|
||||
percentage: number;
|
||||
title: string;
|
||||
}) => {
|
||||
return (
|
||||
<PopOver
|
||||
position="top"
|
||||
title={`${percentage}% ${title}`}
|
||||
trigger="mouseenter">
|
||||
<div className="tw-border tw-border-primary tw-h-6 tw-w-20">
|
||||
<div
|
||||
className="tw-bg-primary tw-opacity-40 tw-h-full"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
<div className="tw-border tw-inline-block tw-border-primary tw-h-5 tw-w-20 ">
|
||||
<div
|
||||
className="tw-bg-primary-hover-lite tw-h-full"
|
||||
style={{ width: `${percentage}%` }}>
|
||||
<div className="tw-w-20 tw-text-grey-body tw-font-medium tw-text-xs tw-leading-5 tw-text-center">
|
||||
{getRoundedValue(percentage)}%
|
||||
</div>
|
||||
</div>
|
||||
</PopOver>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -91,10 +89,12 @@ const TableProfiler: FC<Props> = ({
|
||||
|
||||
return {
|
||||
name: column,
|
||||
columnMetrics: Object.entries(data?.[0] ?? {}).map((d) => ({
|
||||
key: d[0],
|
||||
value: d[1],
|
||||
})),
|
||||
columnMetrics: Object.entries(data?.[0] ?? {})
|
||||
.map((d) => ({
|
||||
key: d[0],
|
||||
value: d[1],
|
||||
}))
|
||||
.filter((m) => !excludedMetrics.includes(m.key)),
|
||||
columnTests: column.colTests,
|
||||
data,
|
||||
type: column.colType,
|
||||
@ -112,9 +112,9 @@ const TableProfiler: FC<Props> = ({
|
||||
<tr className="tableHead-row">
|
||||
<th className="tableHead-cell">Column Name</th>
|
||||
<th className="tableHead-cell">Type</th>
|
||||
<th className="tableHead-cell">Null</th>
|
||||
<th className="tableHead-cell">Unique</th>
|
||||
<th className="tableHead-cell">Distinct</th>
|
||||
<th className="tableHead-cell">Null %</th>
|
||||
<th className="tableHead-cell">Unique %</th>
|
||||
<th className="tableHead-cell">Distinct %</th>
|
||||
<th className="tableHead-cell">Metrics</th>
|
||||
<th className="tableHead-cell">Tests</th>
|
||||
<th className="tableHead-cell" />
|
||||
@ -139,7 +139,32 @@ const TableProfiler: FC<Props> = ({
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span>{col.name.colName}</span>
|
||||
<div className="tw-flex">
|
||||
{col.name.colName.length > 25 ? (
|
||||
<span>
|
||||
<PopOver
|
||||
html={
|
||||
<div className="tw-break-words">
|
||||
<span>{col.name.colName}</span>
|
||||
</div>
|
||||
}
|
||||
position="bottom"
|
||||
theme="light"
|
||||
trigger="click">
|
||||
<div className="tw-cursor-pointer tw-underline tw-inline-block">
|
||||
<RichTextEditorPreviewer
|
||||
markdown={`${col.name.colName.slice(
|
||||
0,
|
||||
20
|
||||
)}...`}
|
||||
/>
|
||||
</div>
|
||||
</PopOver>
|
||||
</span>
|
||||
) : (
|
||||
col.name.colName
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
@ -188,9 +213,7 @@ const TableProfiler: FC<Props> = ({
|
||||
<td className="tw-relative tableBody-cell profiler-graph">
|
||||
<PercentageGraph
|
||||
percentage={
|
||||
((col.data?.[0]?.distinctCount ?? 0) /
|
||||
(col.data?.[0]?.rows ?? 0)) *
|
||||
100
|
||||
(col.data?.[0]?.distinctProportion ?? 0) * 100
|
||||
}
|
||||
title="distinct value"
|
||||
/>
|
||||
@ -198,17 +221,21 @@ const TableProfiler: FC<Props> = ({
|
||||
<td
|
||||
className="tw-relative tableBody-cell"
|
||||
data-testid="tableBody-cell">
|
||||
<div className="tw-border tw-border-main tw-rounded tw-p-2 tw-min-h-32 tw-max-h-44 tw-overflow-y-auto">
|
||||
{col.columnMetrics
|
||||
.filter((m) => !excludedMetrics.includes(m.key))
|
||||
.map((m, i) => (
|
||||
{col.columnMetrics.length ? (
|
||||
<div className=" tw-rounded tw-max-h-44 tw-overflow-y-auto">
|
||||
{col.columnMetrics.map((m, i) => (
|
||||
<p className="tw-mb-1 tw-flex" key={i}>
|
||||
<span className="tw-mx-1">{m.key}</span>
|
||||
<span className="tw-mx-1">-</span>
|
||||
<span className="tw-mx-1">{m.value}</span>
|
||||
<span className="tw-mx-1">
|
||||
{getRoundedValue(m.value)}
|
||||
</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
`No metrics available`
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className="tw-relative tableBody-cell"
|
||||
@ -216,7 +243,7 @@ const TableProfiler: FC<Props> = ({
|
||||
data-testid="tableBody-cell">
|
||||
<div className="tw-flex tw-justify-between">
|
||||
{col.columnTests ? (
|
||||
<div className="tw-border tw-border-main tw-rounded tw-p-2 tw-min-h-32 tw-max-h-44 tw-overflow-y-auto tw-flex-1">
|
||||
<div className="tw-rounded tw-max-h-44 tw-overflow-y-auto tw-flex-1">
|
||||
{col.columnTests.map((m, i) => (
|
||||
<div className="tw-flex tw-mb-2" key={i}>
|
||||
<p className="tw-mr-2">
|
||||
@ -230,7 +257,9 @@ const TableProfiler: FC<Props> = ({
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
<span className="tw-mx-1 tw-font-medium">
|
||||
<span
|
||||
className="tw-mx-1 tw-font-medium tw-cursor-pointer"
|
||||
onClick={() => qualityTestFormHandler(6)}>
|
||||
{m.testCase.columnTestType}
|
||||
</span>
|
||||
<div className="tw-mx-1">
|
||||
@ -263,6 +292,7 @@ const TableProfiler: FC<Props> = ({
|
||||
}
|
||||
)}
|
||||
size="custom"
|
||||
theme="primary"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
onClick={() =>
|
||||
|
@ -24,6 +24,7 @@ import { getVersion } from '../../axiosAPIs/miscAPI';
|
||||
import {
|
||||
getExplorePathWithSearch,
|
||||
getTeamDetailsPath,
|
||||
getUserPath,
|
||||
navLinkSettings,
|
||||
ROUTES,
|
||||
} from '../../constants/constants';
|
||||
@ -149,7 +150,10 @@ const Appbar: React.FC = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div data-testid="greeting-text">
|
||||
<span className="tw-font-medium">{name}</span>
|
||||
<Link to={getUserPath(currentUser?.name as string)}>
|
||||
{' '}
|
||||
<span className="tw-font-medium tw-cursor-pointer">{name}</span>
|
||||
</Link>
|
||||
<hr className="tw-my-1.5" />
|
||||
{(roles?.length ?? 0) > 0 ? (
|
||||
<div>
|
||||
|
@ -20,4 +20,7 @@ export const excludedMetrics = [
|
||||
'uniqueProportion',
|
||||
'rows',
|
||||
'histogram',
|
||||
'missingCount',
|
||||
'missingPercentage',
|
||||
'distinctProportion',
|
||||
];
|
||||
|
@ -632,6 +632,8 @@ export interface ColumnProfile {
|
||||
minLength?: number;
|
||||
|
||||
maxLength?: number;
|
||||
|
||||
distinctProportion?: number;
|
||||
}
|
||||
|
||||
export interface HistogramObject {
|
||||
|
@ -193,11 +193,13 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
|
||||
const qualityTestFormHandler = (
|
||||
tabValue: number,
|
||||
testMode: DatasetTestModeType
|
||||
testMode?: DatasetTestModeType
|
||||
) => {
|
||||
activeTabHandler(tabValue);
|
||||
setTestMode(testMode);
|
||||
setShowTestForm(true);
|
||||
if (testMode) {
|
||||
setTestMode(testMode);
|
||||
setShowTestForm(true);
|
||||
}
|
||||
};
|
||||
|
||||
const getLineageData = () => {
|
||||
|
@ -410,7 +410,7 @@ const RolesPage = () => {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
'tw-border tw-border-grey-muted tw-rounded tw-px-1 tw-font-normal',
|
||||
'tw-border tw-border-grey-muted tw-rounded tw-px-1 tw-font-normal tw-text-sm',
|
||||
className
|
||||
)}>
|
||||
Default
|
||||
|
@ -485,7 +485,7 @@ export const getRandomColor = (name: string) => {
|
||||
const b = num & 255;
|
||||
|
||||
return {
|
||||
color: 'rgb(' + r + ', ' + g + ', ' + b + ', 0.3)',
|
||||
color: 'rgb(' + r + ', ' + g + ', ' + b + ', 0.6)',
|
||||
character: firstAlphabet.toUpperCase(),
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HistogramObject } from '../generated/entity/data/table';
|
||||
|
||||
export const getRoundedValue = (
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| HistogramObject
|
||||
| null
|
||||
| undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| any[]
|
||||
) => {
|
||||
if (typeof value == 'number' && !isNaN(value)) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
} else {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user