mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-11 02:58:17 +00:00
* Profiler & Data Quality UI feedback & improvement #7090 Part 2 * addressing comment * fixed failing unit test
This commit is contained in:
parent
bcf2fc3057
commit
e180cb15af
@ -17,12 +17,11 @@ import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { EditorContentRef } from 'Models';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import {
|
||||
getTestDefinitionById,
|
||||
updateTestCaseById,
|
||||
} from '../../axiosAPIs/testAPI';
|
||||
import { codeMirrorOption } from '../../constants/profiler.constant';
|
||||
import { CSMode } from '../../enums/codemirror.enum';
|
||||
import { TestCaseParameterValue } from '../../generated/tests/testCase';
|
||||
import {
|
||||
TestDataType,
|
||||
@ -32,6 +31,7 @@ import jsonData from '../../jsons/en';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import RichTextEditor from '../common/rich-text-editor/RichTextEditor';
|
||||
import Loader from '../Loader/Loader';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
import { EditTestCaseModalProps } from './AddDataQualityTest.interface';
|
||||
import ParameterForm from './components/ParameterForm';
|
||||
|
||||
@ -63,16 +63,13 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
<Typography.Paragraph className="tw-mb-1.5">
|
||||
Profile Sample Query
|
||||
</Typography.Paragraph>
|
||||
<CodeMirror
|
||||
data-testid="sql-editor"
|
||||
options={codeMirrorOption}
|
||||
<SchemaEditor
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
readOnly: false,
|
||||
}}
|
||||
value={sqlQuery.value || ''}
|
||||
onBeforeChange={(_Editor, _EditorChange, value) => {
|
||||
setSqlQuery((pre) => ({ ...pre, value }));
|
||||
}}
|
||||
onChange={(_Editor, _EditorChange, value) => {
|
||||
setSqlQuery((pre) => ({ ...pre, value }));
|
||||
}}
|
||||
onChange={(value) => setSqlQuery((pre) => ({ ...pre, value }))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -171,6 +168,12 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
testDefinition: testCase?.testDefinition?.name,
|
||||
params: getParamsValue(),
|
||||
});
|
||||
setSqlQuery(
|
||||
testCase?.parameterValues?.[0] ?? {
|
||||
name: 'sqlExpression',
|
||||
value: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [testCase]);
|
||||
|
||||
@ -190,6 +193,7 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
<Loader />
|
||||
) : (
|
||||
<Form
|
||||
className="tw-h-70vh tw-overflow-auto"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="tableTestForm"
|
||||
|
@ -33,13 +33,16 @@ import {
|
||||
COLORS,
|
||||
PROFILER_FILTER_RANGE,
|
||||
} from '../../../constants/profiler.constant';
|
||||
import { CSMode } from '../../../enums/codemirror.enum';
|
||||
import {
|
||||
TestCaseResult,
|
||||
TestCaseStatus,
|
||||
} from '../../../generated/tests/tableTest';
|
||||
import { TestCaseParameterValue } from '../../../generated/tests/testCase';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
|
||||
import Loader from '../../Loader/Loader';
|
||||
import SchemaEditor from '../../schema-editor/SchemaEditor';
|
||||
import { TestSummaryProps } from '../profilerDashboard.interface';
|
||||
|
||||
type ChartDataType = {
|
||||
@ -146,6 +149,34 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
|
||||
fetchTestResults();
|
||||
}, [selectedTimeRange]);
|
||||
|
||||
const showParamsData = (param: TestCaseParameterValue) => {
|
||||
const isSqlQuery = param.name === 'sqlExpression';
|
||||
|
||||
if (isSqlQuery) {
|
||||
return (
|
||||
<div key={param.name}>
|
||||
<Typography.Text>{param.name}: </Typography.Text>
|
||||
<SchemaEditor
|
||||
className="tw-w-11/12 tw-mt-1"
|
||||
editorClass="table-query-editor"
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
styleActiveLine: false,
|
||||
}}
|
||||
value={param.value ?? ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={param.name}>
|
||||
<Typography.Text>{param.name}: </Typography.Text>
|
||||
<Typography.Text>{param.value}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const referenceArea = () => {
|
||||
const yValues = data.parameterValues?.reduce((acc, curr, i) => {
|
||||
return { ...acc, [`y${i + 1}`]: parseInt(curr.value || '') };
|
||||
@ -215,13 +246,14 @@ const TestSummary: React.FC<TestSummaryProps> = ({ data }) => {
|
||||
<Col span={24}>
|
||||
<Typography.Text type="secondary">Parameter: </Typography.Text>
|
||||
</Col>
|
||||
<Col offset={2} span={24}>
|
||||
{data.parameterValues?.map((param) => (
|
||||
<Typography key={param.name}>
|
||||
<Typography.Text>{param.name}: </Typography.Text>
|
||||
<Typography.Text>{param.value}</Typography.Text>
|
||||
</Typography>
|
||||
))}
|
||||
<Col offset={1} span={24}>
|
||||
{data.parameterValues && data.parameterValues.length > 0 ? (
|
||||
data.parameterValues.map(showParamsData)
|
||||
) : (
|
||||
<Typography.Text type="secondary">
|
||||
No Parameter Available
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col className="tw-flex tw-gap-2" span={24}>
|
||||
|
@ -27,35 +27,14 @@ import TableProfilerV1 from './TableProfilerV1';
|
||||
|
||||
// mock library imports
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn().mockImplementation(() => {
|
||||
jest.fn();
|
||||
}),
|
||||
Link: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }) => <a href="#">{children}</a>),
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => ({
|
||||
Button: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, ...props }) => (
|
||||
<button {...props}>{children}</button>
|
||||
)),
|
||||
|
||||
Col: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, ...props }) => (
|
||||
<div {...props}>{children}</div>
|
||||
)),
|
||||
Row: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, ...props }) => (
|
||||
<div {...props}>{children}</div>
|
||||
)),
|
||||
Empty: jest
|
||||
.fn()
|
||||
.mockImplementation(({ description }) => <div>{description}</div>),
|
||||
Link: jest.fn().mockImplementation(({ children }) => <a>{children}</a>),
|
||||
Tooltip: jest.fn().mockImplementation(({ children }) => <p>{children}</p>),
|
||||
}));
|
||||
|
||||
// mock internal imports
|
||||
jest.mock('./Component/ProfilerSettingsModal', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
@ -106,21 +85,6 @@ describe('Test TableProfiler component', () => {
|
||||
expect(addTableTest).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('No data placeholder should be visible where there is no profiler', async () => {
|
||||
render(
|
||||
<TableProfilerV1
|
||||
{...mockProps}
|
||||
table={{ ...mockProps.table, profile: undefined }}
|
||||
/>
|
||||
);
|
||||
|
||||
const noProfiler = await screen.findByTestId(
|
||||
'no-profiler-placeholder-container'
|
||||
);
|
||||
|
||||
expect(noProfiler).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('CTA: Add table test should work properly', async () => {
|
||||
render(<TableProfilerV1 {...mockProps} />);
|
||||
|
||||
|
@ -11,12 +11,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Col, Empty, Row, Tooltip } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { getListTestCase } from '../../axiosAPIs/testAPI';
|
||||
import { API_RES_MAX_SIZE } from '../../constants/constants';
|
||||
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
|
||||
@ -35,6 +43,7 @@ import {
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { generateEntityLink } from '../../utils/TableUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import { ProfilerDashboardTab } from '../ProfilerDashboard/profilerDashboard.interface';
|
||||
import ColumnProfileTable from './Component/ColumnProfileTable';
|
||||
import ProfilerSettingsModal from './Component/ProfilerSettingsModal';
|
||||
import {
|
||||
@ -50,12 +59,16 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
|
||||
hasEditAccess,
|
||||
}) => {
|
||||
const { profile, columns } = table;
|
||||
const history = useHistory();
|
||||
const [settingModalVisible, setSettingModalVisible] = useState(false);
|
||||
const [columnTests, setColumnTests] = useState<TestCase[]>([]);
|
||||
const [tableTests, setTableTests] = useState<TableTestsType>({
|
||||
tests: [],
|
||||
results: INITIAL_TEST_RESULT_SUMMARY,
|
||||
});
|
||||
const [activeTab] = useState<ProfilerDashboardTab>(
|
||||
ProfilerDashboardTab.SUMMARY
|
||||
);
|
||||
|
||||
const handleSettingModal = (value: boolean) => {
|
||||
setSettingModalVisible(value);
|
||||
@ -92,6 +105,24 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
|
||||
];
|
||||
}, [profile, tableTests]);
|
||||
|
||||
const tabOptions = useMemo(() => {
|
||||
return Object.values(ProfilerDashboardTab).filter(
|
||||
(value) => value !== ProfilerDashboardTab.PROFILER
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent) => {
|
||||
const value = e.target.value as ProfilerDashboardTab;
|
||||
if (ProfilerDashboardTab.DATA_QUALITY === value) {
|
||||
history.push(
|
||||
getProfilerDashboardWithFqnPath(
|
||||
ProfilerDashboardType.TABLE,
|
||||
table.fullyQualifiedName || ''
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllTests = async () => {
|
||||
try {
|
||||
const { data } = await getListTestCase({
|
||||
@ -130,67 +161,49 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
|
||||
fetchAllTests();
|
||||
}, [table]);
|
||||
|
||||
if (isUndefined(profile)) {
|
||||
return (
|
||||
<div
|
||||
className=" tw-m-2 tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8"
|
||||
data-testid="no-profiler-placeholder-container">
|
||||
<Empty
|
||||
description={
|
||||
<p>
|
||||
<span>
|
||||
Data Profiler is an optional configuration in Ingestion. Please
|
||||
enable the data profiler by following the documentation
|
||||
</span>
|
||||
<Link
|
||||
className="tw-ml-1"
|
||||
target="_blank"
|
||||
to={{
|
||||
pathname:
|
||||
'https://docs.open-metadata.org/openmetadata/ingestion/workflows/profiler',
|
||||
}}>
|
||||
here.
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="table-profiler-container"
|
||||
data-testid="table-profiler-container">
|
||||
<div className="tw-flex tw-justify-end tw-gap-4 tw-mb-4">
|
||||
<Tooltip title={hasEditAccess ? '' : NO_PERMISSION_FOR_ACTION}>
|
||||
<Link
|
||||
to={
|
||||
hasEditAccess
|
||||
? getAddDataQualityTableTestPath(
|
||||
ProfilerDashboardType.TABLE,
|
||||
table.fullyQualifiedName || ''
|
||||
)
|
||||
: '#'
|
||||
}>
|
||||
<Button
|
||||
className="tw-rounded"
|
||||
data-testid="profiler-add-table-test-btn"
|
||||
disabled={!hasEditAccess}
|
||||
type="primary">
|
||||
Add Test
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Button
|
||||
className="profiler-setting-btn tw-border tw-border-primary tw-rounded tw-text-primary"
|
||||
data-testid="profiler-setting-btn"
|
||||
icon={<SVGIcons alt="setting" icon={Icons.SETTINGS_PRIMERY} />}
|
||||
type="default"
|
||||
onClick={() => handleSettingModal(true)}>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<Row className="tw-mb-4" justify="space-between">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
optionType="button"
|
||||
options={tabOptions}
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
<Space>
|
||||
<Tooltip title={hasEditAccess ? '' : NO_PERMISSION_FOR_ACTION}>
|
||||
<Link
|
||||
to={
|
||||
hasEditAccess
|
||||
? getAddDataQualityTableTestPath(
|
||||
ProfilerDashboardType.TABLE,
|
||||
`${table.fullyQualifiedName}`
|
||||
)
|
||||
: '#'
|
||||
}>
|
||||
<Button
|
||||
className="tw-rounded"
|
||||
data-testid="profiler-add-table-test-btn"
|
||||
disabled={!hasEditAccess}
|
||||
type="primary">
|
||||
Add Test
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Button
|
||||
className="profiler-setting-btn tw-border tw-border-primary tw-rounded tw-text-primary"
|
||||
data-testid="profiler-setting-btn"
|
||||
icon={<SVGIcons alt="setting" icon={Icons.SETTINGS_PRIMERY} />}
|
||||
type="default"
|
||||
onClick={() => handleSettingModal(true)}>
|
||||
Settings
|
||||
</Button>
|
||||
</Space>
|
||||
</Row>
|
||||
|
||||
<Row className="tw-rounded tw-border tw-p-4 tw-mb-4">
|
||||
{overallSummery.map((summery) => (
|
||||
@ -211,15 +224,6 @@ const TableProfilerV1: FC<TableProfilerProps> = ({
|
||||
</p>
|
||||
</Col>
|
||||
))}
|
||||
<Col className="tw-flex tw-justify-end" span={24}>
|
||||
<Link
|
||||
to={getProfilerDashboardWithFqnPath(
|
||||
ProfilerDashboardType.TABLE,
|
||||
table.fullyQualifiedName || ''
|
||||
)}>
|
||||
View more detail
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<ColumnProfileTable
|
||||
|
@ -41,14 +41,17 @@ const SchemaEditor = ({
|
||||
},
|
||||
options,
|
||||
editorClass,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
className?: string;
|
||||
mode?: Mode;
|
||||
readOnly?: boolean;
|
||||
options?: {
|
||||
[key: string]: string | boolean | Array<string>;
|
||||
};
|
||||
editorClass?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) => {
|
||||
const defaultOptions = {
|
||||
tabSize: JSON_TAB_SIZE,
|
||||
@ -75,6 +78,13 @@ const SchemaEditor = ({
|
||||
): void => {
|
||||
setInternalValue(getSchemaEditorValue(value));
|
||||
};
|
||||
const handleEditorInputChange = (
|
||||
_editor: Editor,
|
||||
_data: EditorChange,
|
||||
value: string
|
||||
): void => {
|
||||
onChange && onChange(getSchemaEditorValue(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -83,6 +93,7 @@ const SchemaEditor = ({
|
||||
options={defaultOptions}
|
||||
value={internalValue}
|
||||
onBeforeChange={handleEditorInputBeforeChange}
|
||||
onChange={handleEditorInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user