feat(web): UI pagination for Assertion List page (#14859)

This commit is contained in:
Adrian Machado 2025-09-30 15:06:14 -07:00 committed by GitHub
parent ddffb85e14
commit b25748eda1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 274 additions and 256 deletions

View File

@ -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',

View File

@ -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=""
/>

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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)}
</>
);
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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>
);
};

View File

@ -6,7 +6,6 @@ const AssertionTitleContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px;
div {
border-bottom: 0px;
}

View File

@ -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>

View File

@ -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}

View File

@ -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 = () => {

View File

@ -58,6 +58,7 @@ const ASSERTION_STATUS_NAME_MAP = {
FAILURE: 'Failing',
SUCCESS: 'Passing',
ERROR: 'Error',
INIT: 'Initializing',
[NO_STATUS]: 'No Status',
};

View File

@ -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) => {

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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',
};