mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-05 23:23:42 +00:00
feat(assertion-v2): changed Validation tab to Quality and created new Governance tab (#10935)
This commit is contained in:
parent
fdbcb684ac
commit
d85da39a86
@ -1,15 +1,26 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route, useLocation, useHistory } from 'react-router-dom';
|
||||||
import { Layout } from 'antd';
|
import { Layout } from 'antd';
|
||||||
import { HomePage } from './home/HomePage';
|
import { HomePage } from './home/HomePage';
|
||||||
import { SearchRoutes } from './SearchRoutes';
|
import { SearchRoutes } from './SearchRoutes';
|
||||||
import EmbedRoutes from './EmbedRoutes';
|
import EmbedRoutes from './EmbedRoutes';
|
||||||
import { PageRoutes } from '../conf/Global';
|
import { NEW_ROUTE_MAP, PageRoutes } from '../conf/Global';
|
||||||
|
import { getRedirectUrl } from '../conf/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for all views behind an authentication wall.
|
* Container for all views behind an authentication wall.
|
||||||
*/
|
*/
|
||||||
export const ProtectedRoutes = (): JSX.Element => {
|
export const ProtectedRoutes = (): JSX.Element => {
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname.indexOf('/Validation') !== -1) {
|
||||||
|
history.replace(getRedirectUrl(NEW_ROUTE_MAP));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import AccessManagement from '../shared/tabs/Dataset/AccessManagement/AccessMana
|
|||||||
import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPathsRenderer';
|
import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPathsRenderer';
|
||||||
import { getLastUpdatedMs } from './shared/utils';
|
import { getLastUpdatedMs } from './shared/utils';
|
||||||
import { IncidentTab } from '../shared/tabs/Incident/IncidentTab';
|
import { IncidentTab } from '../shared/tabs/Incident/IncidentTab';
|
||||||
|
import { GovernanceTab } from '../shared/tabs/Dataset/Governance/GovernanceTab';
|
||||||
|
|
||||||
const SUBTYPES = {
|
const SUBTYPES = {
|
||||||
VIEW: 'view',
|
VIEW: 'view',
|
||||||
@ -166,14 +167,22 @@ export class DatasetEntity implements Entity<Dataset> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Validation',
|
name: 'Quality',
|
||||||
component: ValidationsTab,
|
component: ValidationsTab,
|
||||||
display: {
|
display: {
|
||||||
visible: (_, _1) => true,
|
visible: (_, _1) => true,
|
||||||
enabled: (_, dataset: GetDatasetQuery) => {
|
enabled: (_, dataset: GetDatasetQuery) => {
|
||||||
return (
|
return (dataset?.dataset?.assertions?.total || 0) > 0;
|
||||||
(dataset?.dataset?.assertions?.total || 0) > 0 || dataset?.dataset?.testResults !== null
|
},
|
||||||
);
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Governance',
|
||||||
|
component: GovernanceTab,
|
||||||
|
display: {
|
||||||
|
visible: (_, _1) => true,
|
||||||
|
enabled: (_, dataset: GetDatasetQuery) => {
|
||||||
|
return dataset?.dataset?.testResults !== null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const EntityHealth = ({ health, baseUrl, fontSize, tooltipPlacement }: Pr
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(unhealthy && (
|
{(unhealthy && (
|
||||||
<Link to={`${baseUrl}/Validation`}>
|
<Link to={`${baseUrl}/Quality`}>
|
||||||
<Container>
|
<Container>
|
||||||
<EntityHealthPopover health={health} baseUrl={baseUrl} placement={tooltipPlacement}>
|
<EntityHealthPopover health={health} baseUrl={baseUrl} placement={tooltipPlacement}>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { useHistory, useLocation } from 'react-router';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { FileDoneOutlined } from '@ant-design/icons';
|
||||||
|
import { useEntityData } from '../../../EntityContext';
|
||||||
|
import { TestResults } from './TestResults';
|
||||||
|
import TabToolbar from '../../../components/styled/TabToolbar';
|
||||||
|
import { ANTD_GRAY } from '../../../constants';
|
||||||
|
import { useGetValidationsTab } from '../Validations/useGetValidationsTab';
|
||||||
|
|
||||||
|
const TabTitle = styled.span`
|
||||||
|
margin-left: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabButton = styled(Button)<{ selected: boolean }>`
|
||||||
|
background-color: ${(props) => (props.selected && ANTD_GRAY[3]) || 'none'};
|
||||||
|
margin-left: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
enum TabPaths {
|
||||||
|
TESTS = 'Tests',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TAB = TabPaths.TESTS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used for rendering the Entity Governance Tab.
|
||||||
|
*/
|
||||||
|
export const GovernanceTab = () => {
|
||||||
|
const { entityData } = useEntityData();
|
||||||
|
const history = useHistory();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const passingTests = (entityData as any)?.testResults?.passing || [];
|
||||||
|
const maybeFailingTests = (entityData as any)?.testResults?.failing || [];
|
||||||
|
const totalTests = maybeFailingTests.length + passingTests.length;
|
||||||
|
|
||||||
|
const { selectedTab, basePath } = useGetValidationsTab(pathname, Object.values(TabPaths));
|
||||||
|
|
||||||
|
// If no tab was selected, select a default tab.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedTab) {
|
||||||
|
// Route to the default tab.
|
||||||
|
history.replace(`${basePath}/${DEFAULT_TAB}`);
|
||||||
|
}
|
||||||
|
}, [selectedTab, basePath, history]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top-level Toolbar tabs to display.
|
||||||
|
*/
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<FileDoneOutlined />
|
||||||
|
<TabTitle>Tests ({totalTests})</TabTitle>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
path: TabPaths.TESTS,
|
||||||
|
disabled: totalTests === 0,
|
||||||
|
content: <TestResults passing={passingTests} failing={maybeFailingTests} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TabToolbar>
|
||||||
|
<div>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabButton
|
||||||
|
type="text"
|
||||||
|
disabled={tab.disabled}
|
||||||
|
selected={selectedTab === tab.path}
|
||||||
|
onClick={() => history.replace(`${basePath}/${tab.path}`)}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</TabButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabToolbar>
|
||||||
|
{tabs.filter((tab) => tab.path === selectedTab).map((tab) => tab.content)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -188,7 +188,7 @@ export const DatasetAssertionsList = ({
|
|||||||
to={`${entityRegistry.getEntityUrl(
|
to={`${entityRegistry.getEntityUrl(
|
||||||
EntityType.Dataset,
|
EntityType.Dataset,
|
||||||
entityData.urn,
|
entityData.urn,
|
||||||
)}/Validation/Data Contract`}
|
)}/Quality/Data Contract`}
|
||||||
style={{ color: REDESIGN_COLORS.BLUE }}
|
style={{ color: REDESIGN_COLORS.BLUE }}
|
||||||
>
|
>
|
||||||
view
|
view
|
||||||
@ -200,7 +200,7 @@ export const DatasetAssertionsList = ({
|
|||||||
to={`${entityRegistry.getEntityUrl(
|
to={`${entityRegistry.getEntityUrl(
|
||||||
EntityType.Dataset,
|
EntityType.Dataset,
|
||||||
entityData.urn,
|
entityData.urn,
|
||||||
)}/Validation/Data Contract`}
|
)}/Quality/Data Contract`}
|
||||||
>
|
>
|
||||||
<DataContractLogo />
|
<DataContractLogo />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import React, { useEffect } from 'react';
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useHistory, useLocation } from 'react-router';
|
import { useHistory, useLocation } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { AuditOutlined, FileDoneOutlined, FileProtectOutlined } from '@ant-design/icons';
|
import { AuditOutlined, FileProtectOutlined } from '@ant-design/icons';
|
||||||
import { useEntityData } from '../../../EntityContext';
|
import { useEntityData } from '../../../EntityContext';
|
||||||
import { TestResults } from './TestResults';
|
|
||||||
import { Assertions } from './Assertions';
|
import { Assertions } from './Assertions';
|
||||||
import TabToolbar from '../../../components/styled/TabToolbar';
|
import TabToolbar from '../../../components/styled/TabToolbar';
|
||||||
import { useGetValidationsTab } from './useGetValidationsTab';
|
import { useGetValidationsTab } from './useGetValidationsTab';
|
||||||
@ -22,8 +21,7 @@ const TabButton = styled(Button)<{ selected: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
enum TabPaths {
|
enum TabPaths {
|
||||||
ASSERTIONS = 'Assertions',
|
ASSERTIONS = 'List',
|
||||||
TESTS = 'Tests',
|
|
||||||
DATA_CONTRACT = 'Data Contract',
|
DATA_CONTRACT = 'Data Contract',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,9 +37,6 @@ export const ValidationsTab = () => {
|
|||||||
const appConfig = useAppConfig();
|
const appConfig = useAppConfig();
|
||||||
|
|
||||||
const totalAssertions = (entityData as any)?.assertions?.total;
|
const totalAssertions = (entityData as any)?.assertions?.total;
|
||||||
const passingTests = (entityData as any)?.testResults?.passing || [];
|
|
||||||
const maybeFailingTests = (entityData as any)?.testResults?.failing || [];
|
|
||||||
const totalTests = maybeFailingTests.length + passingTests.length;
|
|
||||||
|
|
||||||
const { selectedTab, basePath } = useGetValidationsTab(pathname, Object.values(TabPaths));
|
const { selectedTab, basePath } = useGetValidationsTab(pathname, Object.values(TabPaths));
|
||||||
|
|
||||||
@ -68,17 +63,6 @@ export const ValidationsTab = () => {
|
|||||||
disabled: totalAssertions === 0,
|
disabled: totalAssertions === 0,
|
||||||
content: <Assertions />,
|
content: <Assertions />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<>
|
|
||||||
<FileDoneOutlined />
|
|
||||||
<TabTitle>Tests ({totalTests})</TabTitle>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
path: TabPaths.TESTS,
|
|
||||||
disabled: totalTests === 0,
|
|
||||||
content: <TestResults passing={passingTests} failing={maybeFailingTests} />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (appConfig.config.featureFlags?.dataContractsEnabled) {
|
if (appConfig.config.featureFlags?.dataContractsEnabled) {
|
||||||
|
|||||||
@ -2,37 +2,37 @@ import { useGetValidationsTab } from '../useGetValidationsTab';
|
|||||||
|
|
||||||
describe('useGetValidationsTab', () => {
|
describe('useGetValidationsTab', () => {
|
||||||
it('should correctly extract valid tab', () => {
|
it('should correctly extract valid tab', () => {
|
||||||
const pathname = '/dataset/urn:li:abc/Validation/Assertions';
|
const pathname = '/dataset/urn:li:abc/Quality/List';
|
||||||
const tabNames = ['Assertions'];
|
const tabNames = ['List'];
|
||||||
const res = useGetValidationsTab(pathname, tabNames);
|
const res = useGetValidationsTab(pathname, tabNames);
|
||||||
expect(res.selectedTab).toEqual('Assertions');
|
expect(res.selectedTab).toEqual('List');
|
||||||
expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation');
|
expect(res.basePath).toEqual('/dataset/urn:li:abc/Quality');
|
||||||
});
|
});
|
||||||
it('should extract undefined for invalid tab', () => {
|
it('should extract undefined for invalid tab', () => {
|
||||||
const pathname = '/dataset/urn:li:abc/Validation/Assertions';
|
const pathname = '/dataset/urn:li:abc/Quality/Assertions';
|
||||||
const tabNames = ['Tests'];
|
const tabNames = ['Tests'];
|
||||||
const res = useGetValidationsTab(pathname, tabNames);
|
const res = useGetValidationsTab(pathname, tabNames);
|
||||||
expect(res.selectedTab).toBeUndefined();
|
expect(res.selectedTab).toBeUndefined();
|
||||||
expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation');
|
expect(res.basePath).toEqual('/dataset/urn:li:abc/Quality');
|
||||||
});
|
});
|
||||||
it('should extract undefined for missing tab', () => {
|
it('should extract undefined for missing tab', () => {
|
||||||
const pathname = '/dataset/urn:li:abc/Validation';
|
const pathname = '/dataset/urn:li:abc/Quality';
|
||||||
const tabNames = ['Tests'];
|
const tabNames = ['Tests'];
|
||||||
const res = useGetValidationsTab(pathname, tabNames);
|
const res = useGetValidationsTab(pathname, tabNames);
|
||||||
expect(res.selectedTab).toBeUndefined();
|
expect(res.selectedTab).toBeUndefined();
|
||||||
expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation');
|
expect(res.basePath).toEqual('/dataset/urn:li:abc/Quality');
|
||||||
});
|
});
|
||||||
it('should handle trailing slashes', () => {
|
it('should handle trailing slashes', () => {
|
||||||
let pathname = '/dataset/urn:li:abc/Validation/Assertions/';
|
let pathname = '/dataset/urn:li:abc/Quality/Assertions/';
|
||||||
let tabNames = ['Assertions'];
|
let tabNames = ['Assertions'];
|
||||||
let res = useGetValidationsTab(pathname, tabNames);
|
let res = useGetValidationsTab(pathname, tabNames);
|
||||||
expect(res.selectedTab).toEqual('Assertions');
|
expect(res.selectedTab).toEqual('Assertions');
|
||||||
expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation');
|
expect(res.basePath).toEqual('/dataset/urn:li:abc/Quality');
|
||||||
|
|
||||||
pathname = '/dataset/urn:li:abc/Validation/';
|
pathname = '/dataset/urn:li:abc/Quality/';
|
||||||
tabNames = ['Assertions'];
|
tabNames = ['Assertions'];
|
||||||
res = useGetValidationsTab(pathname, tabNames);
|
res = useGetValidationsTab(pathname, tabNames);
|
||||||
expect(res.selectedTab).toBeUndefined();
|
expect(res.selectedTab).toBeUndefined();
|
||||||
expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation');
|
expect(res.basePath).toEqual('/dataset/urn:li:abc/Quality');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export const getHealthIcon = (type: HealthStatusType, status: HealthStatus, font
|
|||||||
export const getHealthRedirectPath = (type: HealthStatusType) => {
|
export const getHealthRedirectPath = (type: HealthStatusType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case HealthStatusType.Assertions: {
|
case HealthStatusType.Assertions: {
|
||||||
return 'Validation/Assertions';
|
return 'Quality/List';
|
||||||
}
|
}
|
||||||
case HealthStatusType.Incidents: {
|
case HealthStatusType.Incidents: {
|
||||||
return 'Incidents';
|
return 'Incidents';
|
||||||
|
|||||||
@ -41,3 +41,11 @@ export const CLIENT_AUTH_COOKIE = 'actor';
|
|||||||
* Name of the unique browser id cookie generated on client side
|
* Name of the unique browser id cookie generated on client side
|
||||||
*/
|
*/
|
||||||
export const BROWSER_ID_COOKIE = 'bid';
|
export const BROWSER_ID_COOKIE = 'bid';
|
||||||
|
|
||||||
|
/** New Routes Map for redirection */
|
||||||
|
export const NEW_ROUTE_MAP = {
|
||||||
|
'/Validation/Assertions': '/Quality/List',
|
||||||
|
'/Validation/Tests': '/Governance/Tests',
|
||||||
|
'/Validation/Data%20Contract': '/Quality/Data%20Contract',
|
||||||
|
'/Validation': '/Quality',
|
||||||
|
};
|
||||||
|
|||||||
26
datahub-web-react/src/conf/utils.ts
Normal file
26
datahub-web-react/src/conf/utils.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* as per the new route object
|
||||||
|
* We are redirecting older routes to new one
|
||||||
|
* e.g.
|
||||||
|
* {
|
||||||
|
'/Validation/Assertions': '/Quality/List',
|
||||||
|
}
|
||||||
|
* */
|
||||||
|
|
||||||
|
export const getRedirectUrl = (newRoutes: { [key: string]: string }) => {
|
||||||
|
let newPathname = `${window.location.pathname}${window.location.search}`;
|
||||||
|
if (!newRoutes) {
|
||||||
|
return newPathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const path of Object.keys(newRoutes)) {
|
||||||
|
if (newPathname.indexOf(path) !== -1) {
|
||||||
|
newPathname = newPathname.replace(path, newRoutes[path]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${newPathname}${window.location.search}`;
|
||||||
|
};
|
||||||
@ -7,8 +7,8 @@ describe("dataset health test", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.goToDataset(urn, datasetName);
|
cy.goToDataset(urn, datasetName);
|
||||||
// Ensure that the “Health” badge is present and there is an active incident warning
|
// Ensure that the “Health” badge is present and there is an active incident warning
|
||||||
cy.get(`[href="/dataset/${urn}/Validation"]`).should("be.visible");
|
cy.get(`[href="/dataset/${urn}/Quality"]`).should("be.visible");
|
||||||
cy.get(`[href="/dataset/${urn}/Validation"] span`).trigger("mouseover", {
|
cy.get(`[href="/dataset/${urn}/Quality"] span`).trigger("mouseover", {
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
cy.waitTextVisible("This asset may be unhealthy");
|
cy.waitTextVisible("This asset may be unhealthy");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user