mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-27 08:54:32 +00:00
feat(web): UI pagination for Assertion List page (#14859)
This commit is contained in:
parent
ddffb85e14
commit
b25748eda1
@ -16,6 +16,7 @@ module.exports = {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
@ -26,20 +26,22 @@ export const InlineListSearch: React.FC<InlineListSearchProps> = ({
|
||||
entityTypeName,
|
||||
options,
|
||||
}) => {
|
||||
const [debouncedSearchText, setDebouncedSearchText] = useState(searchText);
|
||||
const [localSearchText, setLocalSearchText] = useState(searchText);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
debouncedSetFilterText(debouncedSearchText);
|
||||
debouncedSetFilterText(localSearchText);
|
||||
},
|
||||
500,
|
||||
[debouncedSearchText],
|
||||
[localSearchText],
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchContainer>
|
||||
<StyledInput
|
||||
value={searchText}
|
||||
value={localSearchText}
|
||||
placeholder={options?.placeholder || 'Search...'}
|
||||
onChange={(e) => setDebouncedSearchText(e.target.value)}
|
||||
onChange={(e) => setLocalSearchText(e.target.value)}
|
||||
icon={options?.hidePrefix ? undefined : { icon: 'MagnifyingGlass', source: 'phosphor' }}
|
||||
label=""
|
||||
/>
|
||||
|
||||
@ -18,7 +18,7 @@ type StyledTableProps = {
|
||||
showSelect?: boolean;
|
||||
} & TableProps<any>;
|
||||
|
||||
export const StyledTable = styled(Table)<StyledTableProps>`
|
||||
const BaseStyledTable = styled(Table)<StyledTableProps>`
|
||||
${(props) => !props.showSelect && `margin-left: -50px;`}
|
||||
max-width: none;
|
||||
overflow: inherit;
|
||||
@ -27,6 +27,9 @@ export const StyledTable = styled(Table)<StyledTableProps>`
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: ${ANTD_GRAY[8]};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&&
|
||||
.ant-table-thead
|
||||
@ -40,6 +43,9 @@ export const StyledTable = styled(Table)<StyledTableProps>`
|
||||
.ant-table-tbody > tr > td {
|
||||
border: none;
|
||||
${(props) => props.showSelect && `padding: 16px 20px;`}
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
&&& .ant-table-cell {
|
||||
@ -55,8 +61,19 @@ export const StyledTable = styled(Table)<StyledTableProps>`
|
||||
&&& .acryl-selected-assertions-table-row {
|
||||
background-color: ${ANTD_GRAY[4]};
|
||||
}
|
||||
&&& .ant-table-fixed-right {
|
||||
background-color: inherit;
|
||||
}
|
||||
&&& .ant-table-tbody > tr > td.ant-table-cell-fix-right {
|
||||
background-color: inherit;
|
||||
}
|
||||
&&& .ant-table-thead > tr > th.ant-table-cell-fix-right {
|
||||
background-color: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledTable = BaseStyledTable as <T = any>(props: StyledTableProps & TableProps<T>) => JSX.Element;
|
||||
|
||||
const DetailsColumnWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,20 +1,10 @@
|
||||
import { AuditOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from '@components';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useEntityData } from '@app/entity/shared/EntityContext';
|
||||
import { REDESIGN_COLORS } from '@app/entityV2/shared/constants';
|
||||
import { AssertionName } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AssertionName';
|
||||
import { AssertionListItemActions } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/actions/AssertionListItemActions';
|
||||
import { AssertionResultDot } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/shared/AssertionResultDot';
|
||||
import { AssertionResultPopover } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/shared/result/AssertionResultPopover';
|
||||
import { AssertionDescription } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/summary/AssertionDescription';
|
||||
import { ResultStatusType } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/summary/shared/resultMessageUtils';
|
||||
import { isAssertionPartOfContract } from '@app/entityV2/shared/tabs/Dataset/Validations/contract/utils';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
|
||||
import { Assertion, AssertionRunEvent, DataContract, EntityType } from '@types';
|
||||
import { Assertion, AssertionRunEvent, DataContract } from '@types';
|
||||
|
||||
const DetailsContainer = styled.div`
|
||||
display: flex;
|
||||
@ -26,24 +16,12 @@ const DetailsContainer = styled.div`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const Result = styled.div`
|
||||
margin: 0px 40px 0px 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ActionButtonContainer = styled.div<{ removeRightPadding?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: ${(props) => (props.removeRightPadding ? 'auto' : undefined)};
|
||||
`;
|
||||
|
||||
const DataContractLogo = styled(AuditOutlined)`
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
color: ${REDESIGN_COLORS.BLUE};
|
||||
`;
|
||||
|
||||
interface DetailsColumnProps {
|
||||
assertion: Assertion;
|
||||
contract?: DataContract;
|
||||
@ -52,53 +30,20 @@ interface DetailsColumnProps {
|
||||
}
|
||||
|
||||
export function DetailsColumn({ assertion, contract, lastEvaluation, onViewAssertionDetails }: DetailsColumnProps) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const entityData = useEntityData();
|
||||
|
||||
if (!assertion.info) {
|
||||
return <>No details found</>;
|
||||
}
|
||||
const disabled = false;
|
||||
const isPartOfContract = contract && isAssertionPartOfContract(assertion, contract);
|
||||
|
||||
return (
|
||||
<DetailsContainer>
|
||||
<AssertionResultPopover
|
||||
<AssertionName
|
||||
assertion={assertion}
|
||||
run={lastEvaluation}
|
||||
showProfileButton
|
||||
lastEvaluation={lastEvaluation}
|
||||
lastEvaluationUrl={lastEvaluation?.result?.externalUrl}
|
||||
platform={assertion.platform}
|
||||
contract={contract}
|
||||
onClickProfileButton={onViewAssertionDetails}
|
||||
placement="right"
|
||||
resultStatusType={ResultStatusType.LATEST}
|
||||
>
|
||||
<Result>
|
||||
<AssertionResultDot run={lastEvaluation} disabled={disabled} size={18} />
|
||||
</Result>
|
||||
</AssertionResultPopover>
|
||||
<AssertionDescription assertion={assertion} options={{ hideSecondaryLabel: true, showColumnTag: true }} />
|
||||
{(isPartOfContract && entityData?.urn && (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Part of Data Contract{' '}
|
||||
<Link
|
||||
to={`${entityRegistry.getEntityUrl(
|
||||
EntityType.Dataset,
|
||||
entityData.urn,
|
||||
)}/Quality/Data Contract`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Link
|
||||
to={`${entityRegistry.getEntityUrl(EntityType.Dataset, entityData.urn)}/Quality/Data Contract`}
|
||||
>
|
||||
<DataContractLogo />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)) ||
|
||||
undefined}
|
||||
/>
|
||||
</DetailsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,13 +38,6 @@ const TabToolbar = styled.div`
|
||||
flex: 0 0 auto;
|
||||
`;
|
||||
|
||||
const TabContentWrapper = styled.div`
|
||||
@media screen and (max-height: 800px) {
|
||||
display: contents;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
enum TabPaths {
|
||||
ASSERTIONS = 'List',
|
||||
DATA_CONTRACT = 'Data Contract',
|
||||
@ -148,9 +141,7 @@ export const AcrylValidationsTab = () => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabToolbar>
|
||||
<TabContentWrapper>
|
||||
{tabs.filter((tab) => tab.path === selectedTab).map((tab) => tab.content)}
|
||||
</TabContentWrapper>
|
||||
{tabs.filter((tab) => tab.path === selectedTab).map((tab) => tab.content)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Empty } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useEntityData } from '@app/entity/shared/EntityContext';
|
||||
import { combineEntityDataWithSiblings } from '@app/entity/shared/siblingUtils';
|
||||
@ -19,6 +20,15 @@ import { useGetDatasetContractQuery } from '@src/graphql/contract.generated';
|
||||
import { useGetDatasetAssertionsWithRunEventsQuery } from '@src/graphql/dataset.generated';
|
||||
import { Assertion, DataContract } from '@src/types.generated';
|
||||
|
||||
const AssertionListContainer = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
margin: 0px 20px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Component used for rendering the Assertions Sub Tab on the Validations Tab
|
||||
*/
|
||||
@ -81,7 +91,6 @@ export const AcrylAssertionList = () => {
|
||||
<AcrylAssertionListTable
|
||||
contract={contract}
|
||||
assertionData={visibleAssertions}
|
||||
filter={selectedFilters}
|
||||
refetch={() => {
|
||||
refetch();
|
||||
contractRefetch();
|
||||
@ -93,7 +102,7 @@ export const AcrylAssertionList = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssertionListContainer>
|
||||
<AssertionListTitleContainer />
|
||||
{assertionMonitorData?.length > 0 && (
|
||||
<AcrylAssertionListFilters
|
||||
@ -106,6 +115,6 @@ export const AcrylAssertionList = () => {
|
||||
/>
|
||||
)}
|
||||
{renderListTable()}
|
||||
</>
|
||||
</AssertionListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,12 +5,10 @@ import { AcrylAssertionRecommendedFilters } from '@app/entityV2/shared/tabs/Data
|
||||
import {
|
||||
ASSERTION_DEFAULT_FILTERS,
|
||||
ASSERTION_FILTER_TYPES,
|
||||
ASSERTION_GROUP_BY_FILTER_OPTIONS,
|
||||
} from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/constant';
|
||||
import { useSetFilterFromURLParams } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/hooks';
|
||||
import { AssertionListFilter, AssertionTable } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import { FilterSelect } from '@src/app/entityV2/shared/FilterSelect';
|
||||
import { GroupBySelect } from '@src/app/entityV2/shared/GroupBySelect';
|
||||
import { InlineListSearch } from '@src/app/entityV2/shared/components/search/InlineListSearch';
|
||||
|
||||
interface FilterItem {
|
||||
@ -31,9 +29,6 @@ interface AcrylAssertionListFiltersProps {
|
||||
|
||||
const SearchFilterContainer = styled.div`
|
||||
display: flex;
|
||||
padding: 0px 10px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
@ -70,10 +65,6 @@ export const AcrylAssertionListFilters: React.FC<AcrylAssertionListFiltersProps>
|
||||
});
|
||||
};
|
||||
|
||||
const handleAssertionTypeChange = (value: string) => {
|
||||
handleFilterChange({ ...selectedFilters, groupBy: value });
|
||||
};
|
||||
|
||||
const handleFilterOptionChange = (updatedFilters: FilterItem[]) => {
|
||||
/** Set Recommended Filters when there is value in type,status or source if not then set it as empty to clear the filter */
|
||||
const selectedRecommendedFilters = updatedFilters.reduce<Record<string, string[]>>(
|
||||
@ -140,15 +131,6 @@ export const AcrylAssertionListFilters: React.FC<AcrylAssertionListFiltersProps>
|
||||
initialSelectedOptions={initialSelectedOptions}
|
||||
/>
|
||||
</StyledFilterContainer>
|
||||
{/* ************Render Group By Component ************************* */}
|
||||
<div>
|
||||
<GroupBySelect
|
||||
options={ASSERTION_GROUP_BY_FILTER_OPTIONS}
|
||||
selectedValue={selectedFilters.groupBy}
|
||||
onSelect={handleAssertionTypeChange}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
</FiltersContainer>
|
||||
</SearchFilterContainer>
|
||||
<div>
|
||||
|
||||
@ -1,43 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { StyledTableContainer } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/StyledComponents';
|
||||
import { StyledTable } from '@app/entityV2/shared/tabs/Dataset/Validations/AcrylAssertionsTable';
|
||||
import { useAssertionsTableColumns } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/hooks';
|
||||
import { AssertionListFilter, AssertionTable } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import {
|
||||
AssertionListTableRow,
|
||||
AssertionTable,
|
||||
} from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import { getEntityUrnForAssertion, getSiblingWithUrn } from '@app/entityV2/shared/tabs/Dataset/Validations/acrylUtils';
|
||||
import { useOpenAssertionDetailModal } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/builder/hooks';
|
||||
import { AssertionProfileDrawer } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/AssertionProfileDrawer';
|
||||
import { Table } from '@src/alchemy-components';
|
||||
import { SortingState } from '@src/alchemy-components/components/Table/types';
|
||||
import { useEntityData } from '@src/app/entity/shared/EntityContext';
|
||||
import { useGetExpandedTableGroupsFromEntityUrnInUrl } from '@src/app/entityV2/shared/hooks';
|
||||
import { DataContract } from '@src/types.generated';
|
||||
|
||||
type Props = {
|
||||
assertionData: AssertionTable;
|
||||
filter: AssertionListFilter;
|
||||
refetch: () => void;
|
||||
contract: DataContract;
|
||||
};
|
||||
|
||||
export const AcrylAssertionListTable = ({ assertionData, filter, refetch, contract }: Props) => {
|
||||
const HEADER_AND_PAGINATION_HEIGHT_PX = 130;
|
||||
|
||||
const TableContainer = styled.div`
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
`;
|
||||
|
||||
export const AcrylAssertionListTable = ({ assertionData, refetch, contract }: Props) => {
|
||||
const { entityData } = useEntityData();
|
||||
const { groupBy } = filter;
|
||||
|
||||
const [sortedOptions, setSortedOptions] = useState<{ sortColumn: string; sortOrder: SortingState }>({
|
||||
sortColumn: '',
|
||||
sortOrder: SortingState.ORIGINAL,
|
||||
});
|
||||
|
||||
const { expandedGroupIds, setExpandedGroupIds } = useGetExpandedTableGroupsFromEntityUrnInUrl(
|
||||
assertionData?.groupBy ? assertionData?.groupBy[groupBy] : [],
|
||||
{ isGroupBy: !!groupBy },
|
||||
'assertion_urn',
|
||||
(group) => group.assertions,
|
||||
);
|
||||
const [tableHeight, setTableHeight] = useState(0);
|
||||
|
||||
// get columns data from the custom hooks
|
||||
const assertionsTableCols = useAssertionsTableColumns({
|
||||
groupBy,
|
||||
contract,
|
||||
refetch,
|
||||
});
|
||||
@ -57,15 +53,6 @@ export const AcrylAssertionListTable = ({ assertionData, filter, refetch, contra
|
||||
|
||||
useOpenAssertionDetailModal(setFocusAssertionUrn);
|
||||
|
||||
const onAssertionExpand = (record) => {
|
||||
const key = record.name;
|
||||
setExpandedGroupIds((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
|
||||
};
|
||||
|
||||
const getGroupData = () => {
|
||||
return (assertionData?.groupBy && assertionData?.groupBy[groupBy]) || [];
|
||||
};
|
||||
|
||||
const rowClassName = (record): string => {
|
||||
if (record.groupName) {
|
||||
return 'group-header';
|
||||
@ -76,72 +63,47 @@ export const AcrylAssertionListTable = ({ assertionData, filter, refetch, contra
|
||||
return 'acryl-assertions-table-row';
|
||||
};
|
||||
|
||||
const onRowClick = (record) => {
|
||||
setFocusAssertionUrn(record.urn);
|
||||
};
|
||||
const memoizedData = useMemo(
|
||||
() => assertionData.assertions.map((assertion) => ({ ...assertion, key: assertion.urn })),
|
||||
[assertionData.assertions],
|
||||
);
|
||||
|
||||
const getSortedAssertions = (record) => {
|
||||
const { sortOrder, sortColumn } = sortedOptions;
|
||||
if (sortOrder === SortingState.ORIGINAL) {
|
||||
return record.assertions;
|
||||
}
|
||||
|
||||
const sortFunctions = {
|
||||
lastEvaluation: {
|
||||
[SortingState.DESCENDING]: (a, b) => a.lastEvaluationTimeMs - b.lastEvaluationTimeMs,
|
||||
[SortingState.ASCENDING]: (a, b) => b.lastEvaluationTimeMs - a.lastEvaluationTimeMs,
|
||||
},
|
||||
name: {
|
||||
[SortingState.ASCENDING]: (a, b) => a.description.localeCompare(b.description),
|
||||
[SortingState.DESCENDING]: (a, b) => b.description.localeCompare(a.description),
|
||||
},
|
||||
};
|
||||
|
||||
const sortFunction = sortFunctions[sortColumn]?.[sortOrder];
|
||||
return sortFunction ? [...record.assertions].sort(sortFunction) : record.assertions;
|
||||
};
|
||||
const handleRowClick = useCallback(
|
||||
(record) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
setFocusAssertionUrn(record.urn);
|
||||
},
|
||||
};
|
||||
},
|
||||
[setFocusAssertionUrn],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTableContainer style={{ height: '100vh', overflow: 'hidden' }}>
|
||||
<Table
|
||||
<TableContainer>
|
||||
<ResizeObserver
|
||||
onResize={(dimensions) => setTableHeight(dimensions.height - HEADER_AND_PAGINATION_HEIGHT_PX)}
|
||||
>
|
||||
<StyledTable<AssertionListTableRow>
|
||||
columns={assertionsTableCols}
|
||||
data={groupBy ? getGroupData() : assertionData.assertions || []}
|
||||
showSelect
|
||||
dataSource={memoizedData}
|
||||
showHeader
|
||||
isScrollable
|
||||
rowClassName={rowClassName}
|
||||
handleSortColumnChange={({
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
}: {
|
||||
sortColumn: string;
|
||||
sortOrder: SortingState;
|
||||
}) => setSortedOptions({ sortColumn, sortOrder })}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => {
|
||||
let sortedAssertions = record.assertions;
|
||||
if (sortedOptions.sortColumn && sortedOptions.sortOrder) {
|
||||
sortedAssertions = getSortedAssertions(record);
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
columns={assertionsTableCols}
|
||||
data={sortedAssertions}
|
||||
showHeader={false}
|
||||
isBorderless
|
||||
isExpandedInnerTable
|
||||
onRowClick={onRowClick}
|
||||
rowClassName={rowClassName}
|
||||
/>
|
||||
);
|
||||
},
|
||||
rowExpandable: () => !!groupBy,
|
||||
expandIconPosition: 'end',
|
||||
expandedGroupIds,
|
||||
scroll={{
|
||||
y: tableHeight,
|
||||
x: 'max-content',
|
||||
}}
|
||||
onExpand={onAssertionExpand}
|
||||
pagination={{
|
||||
pageSize: 50,
|
||||
position: ['bottomCenter'],
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
rowClassName={rowClassName}
|
||||
bordered={false}
|
||||
onRow={handleRowClick}
|
||||
tableLayout="fixed"
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
</ResizeObserver>
|
||||
{focusAssertionUrn && focusedAssertionEntity && (
|
||||
<AssertionProfileDrawer
|
||||
urn={focusAssertionUrn}
|
||||
@ -150,6 +112,6 @@ export const AcrylAssertionListTable = ({ assertionData, filter, refetch, contra
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,7 +6,6 @@ const AssertionTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 20px;
|
||||
div {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
@ -4,16 +4,15 @@ import styled from 'styled-components';
|
||||
|
||||
import AcrylAssertionListStatusDot from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionListStatusDot';
|
||||
import { DataContractBadge } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/DataContractBadge';
|
||||
import { AssertionListTableRow } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import { AssertionPlatformAvatar } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionPlatformAvatar';
|
||||
import { AssertionResultPopover } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/shared/result/AssertionResultPopover';
|
||||
import { ResultStatusType } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/summary/shared/resultMessageUtils';
|
||||
import { useBuildAssertionDescriptionLabels } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/summary/utils';
|
||||
import { useBuildAssertionPrimaryLabel } from '@app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/summary/utils';
|
||||
import { isAssertionPartOfContract } from '@app/entityV2/shared/tabs/Dataset/Validations/contract/utils';
|
||||
import { useEntityData } from '@src/app/entity/shared/EntityContext';
|
||||
import { UNKNOWN_DATA_PLATFORM } from '@src/app/entityV2/shared/constants';
|
||||
import { useEntityRegistry } from '@src/app/useEntityRegistry';
|
||||
import { DataContract, EntityType } from '@src/types.generated';
|
||||
import { Assertion, AssertionRunEvent, DataContract, DataPlatform, EntityType, Maybe } from '@src/types.generated';
|
||||
|
||||
const StyledAssertionNameContainer = styled.div`
|
||||
display: flex;
|
||||
@ -50,27 +49,29 @@ const StyledAssertionName = styled(Typography.Paragraph)`
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
record: AssertionListTableRow;
|
||||
groupBy: string;
|
||||
contract: DataContract;
|
||||
assertion: Assertion;
|
||||
lastEvaluation?: AssertionRunEvent;
|
||||
lastEvaluationUrl?: Maybe<string>;
|
||||
platform?: DataPlatform;
|
||||
contract?: DataContract;
|
||||
onClickProfileButton?: () => void;
|
||||
};
|
||||
|
||||
export const AssertionName = ({ record, groupBy, contract }: Props) => {
|
||||
export const AssertionName = ({
|
||||
assertion,
|
||||
contract,
|
||||
lastEvaluation,
|
||||
lastEvaluationUrl,
|
||||
platform,
|
||||
onClickProfileButton,
|
||||
}: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const entityData = useEntityData();
|
||||
|
||||
const { platform, assertion, lastEvaluation, lastEvaluationUrl } = record;
|
||||
const monitorSchedule = null;
|
||||
const { primaryLabel } = useBuildAssertionDescriptionLabels(record?.assertion?.info, monitorSchedule, {
|
||||
const name = useBuildAssertionPrimaryLabel(assertion.info, monitorSchedule, {
|
||||
showColumnTag: true,
|
||||
});
|
||||
let name = primaryLabel;
|
||||
|
||||
// if it is group header then just display group name instead of other fields
|
||||
if (groupBy && record.name) {
|
||||
name = <>{record.groupName}</>;
|
||||
return <Typography.Text>{name}</Typography.Text>;
|
||||
}
|
||||
|
||||
const disabled = false;
|
||||
const isPartOfContract = contract && isAssertionPartOfContract(assertion, contract);
|
||||
@ -83,6 +84,7 @@ export const AssertionName = ({ record, groupBy, contract }: Props) => {
|
||||
run={lastEvaluation}
|
||||
showProfileButton
|
||||
placement="right"
|
||||
onClickProfileButton={onClickProfileButton}
|
||||
resultStatusType={ResultStatusType.LATEST}
|
||||
>
|
||||
<Result>
|
||||
|
||||
@ -87,6 +87,7 @@ export const AcrylAssertionTagColumn: React.FC<AcrylAssertionTagColumnProps> = (
|
||||
{displayTags?.map((tag) => (
|
||||
<Tag
|
||||
tag={tag}
|
||||
key={tag.urn}
|
||||
options={{
|
||||
shouldNotOpenDrawerOnClick: true,
|
||||
shouldNotAddBottomMargin: true,
|
||||
@ -105,6 +106,7 @@ export const AcrylAssertionTagColumn: React.FC<AcrylAssertionTagColumnProps> = (
|
||||
?.slice(1, MAX_TAGS_FOR_HOVER)
|
||||
.map((tag) => (
|
||||
<Tag
|
||||
key={tag.urn}
|
||||
tag={{ tag: tag.tag } as TagAssociation}
|
||||
options={{ shouldNotOpenDrawerOnClick: true }}
|
||||
maxWidth={120}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { Typography } from 'antd';
|
||||
import React, { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ActionsColumn } from '@app/entityV2/shared/tabs/Dataset/Validations/AcrylAssertionsTableColumns';
|
||||
import { AssertionName } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AssertionName';
|
||||
import { AcrylAssertionTagColumn } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/Tags/AcrylAssertionTagColumn';
|
||||
import { AssertionListFilter } from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import {
|
||||
AssertionListFilter,
|
||||
AssertionListTableRow,
|
||||
} from '@app/entityV2/shared/tabs/Dataset/Validations/AssertionList/types';
|
||||
import { getAssertionGroupName } from '@app/entityV2/shared/tabs/Dataset/Validations/acrylUtils';
|
||||
import { getQueryParams } from '@app/entityV2/shared/tabs/Dataset/Validations/assertionUtils';
|
||||
import { REDESIGN_COLORS } from '@src/app/entityV2/shared/constants';
|
||||
@ -37,70 +41,129 @@ const LastRun = styled(Typography.Text)`
|
||||
|
||||
const TABLE_HEADER_HEIGHT = 50;
|
||||
|
||||
export const useAssertionsTableColumns = ({ groupBy, contract, refetch }) => {
|
||||
export const useAssertionsTableColumns = ({ contract, refetch }) => {
|
||||
const renderAssertionName = useCallback(
|
||||
(_, record) => (
|
||||
<AssertionName
|
||||
key={record.urn}
|
||||
assertion={record.assertion}
|
||||
lastEvaluation={record.lastEvaluation}
|
||||
lastEvaluationUrl={record.lastEvaluationUrl}
|
||||
platform={record.platform}
|
||||
contract={contract}
|
||||
/>
|
||||
),
|
||||
[contract],
|
||||
);
|
||||
|
||||
const renderCategory = useCallback(
|
||||
(_, record) =>
|
||||
!record.groupName &&
|
||||
record?.type && <CategoryType key={record.urn}>{getAssertionGroupName(record.type)}</CategoryType>,
|
||||
[],
|
||||
);
|
||||
|
||||
const renderLastRun = useCallback(
|
||||
(_, record) =>
|
||||
!record.groupName && <LastRun key={record.urn}>{getTimeFromNow(record.lastEvaluationTimeMs)}</LastRun>,
|
||||
[],
|
||||
);
|
||||
|
||||
const renderTags = useCallback(
|
||||
(_, record) =>
|
||||
!record.groupName && <AcrylAssertionTagColumn key={record.urn} record={record} refetch={refetch} />,
|
||||
[refetch],
|
||||
);
|
||||
|
||||
const renderActions = useCallback(
|
||||
(_, record) => {
|
||||
return (
|
||||
!record.groupName && (
|
||||
<ActionsColumn
|
||||
key={record.urn}
|
||||
assertion={record.assertion}
|
||||
contract={contract}
|
||||
canEditContract
|
||||
refetch={refetch}
|
||||
shouldRightAlign
|
||||
options={{ removeRightPadding: true }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
[contract, refetch],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const columns = [
|
||||
const columns: ColumnsType<AssertionListTableRow> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (record) => <AssertionName record={record} groupBy={groupBy} contract={contract} />,
|
||||
width: '42%',
|
||||
render: renderAssertionName,
|
||||
width: '45%',
|
||||
sorter: (a, b) => {
|
||||
return a - b;
|
||||
return a.description.localeCompare(b.description);
|
||||
},
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (record) =>
|
||||
!record.groupName && <CategoryType>{getAssertionGroupName(record?.type)}</CategoryType>,
|
||||
width: '10%',
|
||||
render: renderCategory,
|
||||
width: '12%',
|
||||
sorter: (a, b) => {
|
||||
if (a.type && b.type) {
|
||||
return getAssertionGroupName(a.type).localeCompare(getAssertionGroupName(b.type));
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Last Run',
|
||||
dataIndex: 'lastEvaluation',
|
||||
key: 'lastEvaluation',
|
||||
render: (record) => {
|
||||
return !record.groupName && <LastRun>{getTimeFromNow(record.lastEvaluationTimeMs)}</LastRun>;
|
||||
},
|
||||
width: '10%',
|
||||
render: renderLastRun,
|
||||
width: '15%',
|
||||
sorter: (sourceA, sourceB) => {
|
||||
if (!sourceA.lastEvaluationTimeMs || !sourceB.lastEvaluationTimeMs) {
|
||||
return 0;
|
||||
}
|
||||
return sourceA.lastEvaluationTimeMs - sourceB.lastEvaluationTimeMs;
|
||||
},
|
||||
defaultSortOrder: 'descend',
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
width: '20%',
|
||||
render: (record) => !record.groupName && <AcrylAssertionTagColumn record={record} refetch={refetch} />,
|
||||
width: '18%',
|
||||
render: renderTags,
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: 'actions',
|
||||
width: '15%',
|
||||
render: (record) => {
|
||||
return (
|
||||
!record.groupName && (
|
||||
<ActionsColumn
|
||||
assertion={record.assertion}
|
||||
contract={contract}
|
||||
canEditContract
|
||||
refetch={refetch}
|
||||
shouldRightAlign
|
||||
options={{ removeRightPadding: true }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
width: '10%',
|
||||
render: renderActions,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
}, [groupBy, contract, refetch]);
|
||||
}, [renderAssertionName, renderCategory, renderLastRun, renderTags, renderActions]);
|
||||
};
|
||||
|
||||
export const usePinnedAssertionTableHeaderProps = () => {
|
||||
|
||||
@ -58,6 +58,7 @@ const ASSERTION_STATUS_NAME_MAP = {
|
||||
FAILURE: 'Failing',
|
||||
SUCCESS: 'Passing',
|
||||
ERROR: 'Error',
|
||||
INIT: 'Initializing',
|
||||
[NO_STATUS]: 'No Status',
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '@compon
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
import { AssertionGroup, AssertionStatusSummary } from '@app/entityV2/shared/tabs/Dataset/Validations/acrylTypes';
|
||||
import { sortAssertions } from '@app/entityV2/shared/tabs/Dataset/Validations/assertionUtils';
|
||||
import { toProperTitleCase } from '@app/entityV2/shared/utils';
|
||||
import { lowerFirstLetter } from '@app/shared/textUtil';
|
||||
import { ASSERTION_TYPE_TO_ICON_MAP } from '@src/app/entityV2/shared/tabs/Dataset/Validations/shared/constant';
|
||||
import { GetDatasetAssertionsWithRunEventsQuery } from '@src/graphql/dataset.generated';
|
||||
@ -135,7 +134,7 @@ ASSERTION_INFO.forEach((info) => {
|
||||
});
|
||||
|
||||
export const getAssertionGroupName = (type: string): string => {
|
||||
return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).name : toProperTitleCase(type);
|
||||
return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).name : type;
|
||||
};
|
||||
|
||||
export const getAssertionGroupTypeIcon = (type: string) => {
|
||||
|
||||
@ -348,7 +348,7 @@ export const getFreshnessAssertionPlainTextDescription = (
|
||||
* @param monitorSchedule
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const useBuildPrimaryLabel = (
|
||||
export const useBuildAssertionPrimaryLabel = (
|
||||
assertionInfo?: Maybe<AssertionInfo>,
|
||||
monitorSchedule?: Maybe<CronSchedule>,
|
||||
options?: { showColumnTag?: boolean },
|
||||
@ -511,7 +511,7 @@ export const useBuildAssertionDescriptionLabels = (
|
||||
} => {
|
||||
// ------- Primary label with assertion description ------ //
|
||||
// IMPORTANT: if you modify this, also modify {@link #getPlainTextDescriptionFromAssertion} below
|
||||
const primaryLabel = useBuildPrimaryLabel(assertionInfo, monitorSchedule, options);
|
||||
const primaryLabel = useBuildAssertionPrimaryLabel(assertionInfo, monitorSchedule, options);
|
||||
|
||||
// ----------- Try displaying secondary label showing creator/updater context ------------ //
|
||||
const secondaryLabel = useBuildSecondaryLabel(assertionInfo);
|
||||
@ -523,16 +523,45 @@ export const useBuildAssertionDescriptionLabels = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Similar to {@link #useBuildPrimaryLabel}, but returns plaintext instead of jsx.
|
||||
* Similar to {@link #useBuildAssertionPrimaryLabel}, but returns plaintext instead of jsx.
|
||||
* Primarily used for building the search index!
|
||||
*/
|
||||
export const getPlainTextDescriptionFromAssertion = (assertionInfo?: AssertionInfo): string => {
|
||||
export const getPlainTextDescriptionFromAssertion = (
|
||||
assertionInfo?: AssertionInfo,
|
||||
monitorSchedule?: CronSchedule,
|
||||
): string => {
|
||||
// if description is present don't generate dynamic description
|
||||
if (assertionInfo?.description) {
|
||||
return assertionInfo.description;
|
||||
}
|
||||
|
||||
return assertionInfo
|
||||
? getDatasetAssertionPlainTextDescription(assertionInfo.datasetAssertion as DatasetAssertionInfo)
|
||||
: '';
|
||||
let primaryLabel = '';
|
||||
switch (assertionInfo?.type) {
|
||||
case AssertionType.Dataset:
|
||||
primaryLabel = getDatasetAssertionPlainTextDescription(
|
||||
assertionInfo.datasetAssertion as DatasetAssertionInfo,
|
||||
);
|
||||
break;
|
||||
case AssertionType.Freshness:
|
||||
primaryLabel = getFreshnessAssertionPlainTextDescription(
|
||||
assertionInfo.freshnessAssertion as FreshnessAssertionInfo,
|
||||
monitorSchedule as CronSchedule,
|
||||
);
|
||||
break;
|
||||
case AssertionType.Volume:
|
||||
primaryLabel = getVolumeAssertionPlainTextDescription(assertionInfo.volumeAssertion as VolumeAssertionInfo);
|
||||
break;
|
||||
case AssertionType.Sql:
|
||||
primaryLabel = assertionInfo.description || '';
|
||||
break;
|
||||
case AssertionType.Field:
|
||||
primaryLabel = getFieldAssertionPlainTextDescription(assertionInfo.fieldAssertion as FieldAssertionInfo);
|
||||
break;
|
||||
case AssertionType.DataSchema:
|
||||
primaryLabel = getSchemaAssertionPlainTextDescription(assertionInfo.schemaAssertion as SchemaAssertionInfo);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return primaryLabel;
|
||||
};
|
||||
|
||||
@ -21,8 +21,19 @@ import { useGetDatasetAssertionsWithRunEventsQuery } from '@src/graphql/dataset.
|
||||
import { useUpsertDataContractMutation } from '@graphql/contract.generated';
|
||||
import { Assertion, AssertionType, DataContract } from '@types';
|
||||
|
||||
const BuilderContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const AssertionsSection = styled.div`
|
||||
border: 0.5px solid ${ANTD_GRAY[4]};
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const HeaderText = styled.div`
|
||||
@ -34,7 +45,10 @@ const HeaderText = styled.div`
|
||||
const ActionContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
flex-shrink: 0;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid ${ANTD_GRAY[4]};
|
||||
margin-top: 0;
|
||||
`;
|
||||
|
||||
const CancelButton = styled(Button)`
|
||||
@ -42,7 +56,7 @@ const CancelButton = styled(Button)`
|
||||
`;
|
||||
|
||||
const SaveButton = styled(Button)`
|
||||
margin-right: 20px;
|
||||
margin-right: 0;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
@ -150,7 +164,7 @@ export const DataContractBuilder = ({ entityUrn, initialState, onSubmit, onCance
|
||||
const hasAssertions = freshnessAssertions.length || schemaAssertions.length || dataQualityAssertions.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BuilderContainer>
|
||||
{(hasAssertions && <HeaderText>Select the assertions that will make up your contract.</HeaderText>) || (
|
||||
<HeaderText>Add a few assertions on this entity to create a data contract out of them.</HeaderText>
|
||||
)}
|
||||
@ -195,6 +209,6 @@ export const DataContractBuilder = ({ entityUrn, initialState, onSubmit, onCance
|
||||
</SaveButton>
|
||||
</div>
|
||||
</ActionContainer>
|
||||
</>
|
||||
</BuilderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,9 +11,9 @@ const modalStyle = {};
|
||||
const modalBodyStyle = {
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
paddingBottom: 20,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
maxHeight: '70vh',
|
||||
height: '70vh',
|
||||
'overflow-x': 'auto',
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user