Fix #6791 : Table styling on Settings page made consistent to that on entity page (#6938)

* Fix #6791 : Replaced react-table with antd table component

* Updated unit tests

* Fixed failing cypress tests

* Refactored code, removed code smells.

* Fixed code smells
This commit is contained in:
Aniket Katkar 2022-08-29 11:51:16 +05:30 committed by GitHub
parent 3f491a8578
commit 1b084fd956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 579 additions and 771 deletions

View File

@ -371,7 +371,7 @@ export const addNewTagToEntity = (entity, term) => {
.should('be.visible')
.contains(term);
cy.get('[data-testid="table-body"] > :nth-child(1) > :nth-child(5)')
cy.get(':nth-child(1) > :nth-child(5) [data-testid="tag-container"]')
.contains('Tags')
.should('be.visible')
.click();
@ -386,7 +386,7 @@ export const addNewTagToEntity = (entity, term) => {
.scrollIntoView()
.should('be.visible')
.click();
cy.get('[data-testid="table-body"] > :nth-child(1) > :nth-child(5)')
cy.get(':nth-child(1) > :nth-child(5) [data-testid="tag-container"]')
.scrollIntoView()
.contains(term)
.should('exist');

View File

@ -366,7 +366,7 @@ describe('Glossary page should work properly', () => {
//Remove the added column tag from entity
cy.get(
':nth-child(1) > :nth-child(5) > [data-testid="tags-wrapper"] > :nth-child(1) > :nth-child(1) > [data-testid="tag-container"] > div > span.tw-text-primary > [data-testid="tags"]'
':nth-child(1) > :nth-child(5) span.tw-text-primary > [data-testid="tags"]'
)
.scrollIntoView()
.should('be.visible')

View File

@ -13,13 +13,18 @@
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Popover } from 'antd';
import { Popover, Table } from 'antd';
import classNames from 'classnames';
import { cloneDeep, isEmpty, isNil, isUndefined, lowerCase } from 'lodash';
import { EntityFieldThreads, EntityTags, TagOption } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Link, useHistory } from 'react-router-dom';
import { useExpanded, useTable } from 'react-table';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTableDetailsPath } from '../../constants/constants';
@ -68,8 +73,9 @@ import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPr
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import { TABLE_HEADERS } from './EntityTable.constant';
import { TABLE_HEADERS_V1 } from './EntityTable.constants';
import { EntityTableProps } from './EntityTable.interface';
import './EntityTable.style.less';
const EntityTable = ({
tableColumns,
@ -88,7 +94,6 @@ const EntityTable = ({
const { isAdminUser, userPermissions } = useAuth();
const { isAuthDisabled } = useAuthContext();
const history = useHistory();
const columns = TABLE_HEADERS;
const [searchedColumns, setSearchedColumns] = useState<ModifiedTableColumn[]>(
[]
@ -99,21 +104,6 @@ const EntityTable = ({
[searchedColumns]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
toggleAllRowsExpanded,
} = useTable(
{
columns,
data,
autoResetExpanded: false,
},
useExpanded
);
const [editColumn, setEditColumn] = useState<{
column: Column;
index: number;
@ -162,6 +152,7 @@ const EntityTable = ({
} else {
setTagFetchFailed(true);
}
setIsTagLoading(false);
})
.catch(() => {
setAllTags([]);
@ -208,9 +199,7 @@ const EntityTable = ({
) => {
const getUpdatedTags = (column: Column) => {
const prevTags = column?.tags?.filter((tag) => {
return newColumnTags
.map((tag) => tag.fqn)
.includes(tag?.tagFQN as string);
return newColumnTags.map((tag) => tag.fqn).includes(tag.tagFQN);
});
const newTags: Array<EntityTags> = newColumnTags
@ -304,8 +293,6 @@ const EntityTable = ({
} else if (!isUndefined(column.children)) {
const searchedChildren = searchInColumns(column.children, searchText);
if (searchedChildren.length > 0) {
toggleAllRowsExpanded(true);
return [
...searchedCols,
{
@ -329,7 +316,7 @@ const EntityTable = ({
hasPemission(Operation.EditDescription, EntityType.TABLE, userPermissions);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const getColumnName = (cell: any) => {
const fqn = cell?.row?.original?.fullyQualifiedName || '';
const fqn = cell?.fullyQualifiedName || '';
const columnName = getPartialNameFromTableFQN(fqn, [FqnPart.NestedColumn]);
// wrap it in quotes if dot is present
@ -406,13 +393,12 @@ const EntityTable = ({
handleEditColumn(column, index);
};
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const getRequestDescriptionElement = (cell: any) => {
const hasDescription = Boolean(cell.value);
const getRequestDescriptionElement = (cell: ModifiedTableColumn) => {
const hasDescription = Boolean(cell.fullyQualifiedName);
return (
<button
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-opacity-0 group-hover:tw-opacity-100"
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none hover-cell-icon"
data-testid="request-description"
onClick={() =>
hasDescription
@ -446,7 +432,7 @@ const EntityTable = ({
return (
<button
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-opacity-0 group-hover:tw-opacity-100 tw-align-top"
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-align-top hover-cell-icon"
data-testid="request-tags"
onClick={() =>
hasTags ? onUpdateTagsHandler(cell) : onRequestTagsHandler(cell)
@ -463,113 +449,34 @@ const EntityTable = ({
);
};
/* eslint-disable-next-line */
const handleTagContainerClick = (row: any) => {
if (!editColumnTag) {
handleEditColumnTag(row.original, row.id);
// Fetch tags and terms only once
if (allTags.length === 0 || tagFetchFailed) {
fetchTagsAndGlossaryTerms();
}
}
};
useEffect(() => {
if (!searchText) {
setSearchedColumns(tableColumns);
} else {
const searchCols = searchInColumns(tableColumns, searchText);
setSearchedColumns(searchCols);
}
}, [searchText, tableColumns]);
useEffect(() => {
toggleAllRowsExpanded(isReadOnly);
}, []);
return (
<div className="tw-table-responsive" id="schemaTable">
<table
className="tw-w-full"
{...getTableProps()}
data-testid="entity-table">
<thead data-testid="table-header">
{/* eslint-disable-next-line */}
{headerGroups.map((headerGroup: any, index: number) => (
<tr
className="tableHead-row"
key={index}
{...headerGroup.getHeaderGroupProps()}>
{/* eslint-disable-next-line */}
{headerGroup.headers.map((column: any, index: number) => (
<th
className={classNames('tableHead-cell', {
'tw-w-60':
column.id === 'tags' || column.id === 'columnTests',
})}
data-testid={column.id}
key={index}
{...column.getHeaderProps()}>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()} data-testid="table-body">
{/* eslint-disable-next-line */}
{rows.map((row: any, index: number) => {
prepareRow(row);
return (
<tr
className={classNames('tableBody-row')}
data-testid="row"
key={index}
{...row.getRowProps()}>
{/* eslint-disable-next-line */}
{row.cells.map((cell: any, index: number) => {
const getTestStats = (
record: ModifiedTableColumn | Column,
columnTestLength: number | undefined
) => {
const columnTests =
cell.column.id === 'columnTests'
? ((cell.value ?? []) as ColumnTest[])
columnTestLength && columnTestLength > 0
? record.columnTests ?? []
: ([] as ColumnTest[]);
const columnTestLength = columnTests.length;
const failingTests = columnTests.filter((test) =>
test.results?.some(
(t) => t.testCaseStatus === TestCaseStatus.Failed
)
test.results?.some((t) => t.testCaseStatus === TestCaseStatus.Failed)
);
const passingTests = columnTests.filter((test) =>
test.results?.some(
(t) => t.testCaseStatus === TestCaseStatus.Success
)
test.results?.some((t) => t.testCaseStatus === TestCaseStatus.Success)
);
return (
<td
className={classNames(
'tableBody-cell tw-group tw-relative tw-align-baseline'
)}
key={index}
{...cell.getCellProps()}>
{row.canExpand && cell.column.id === 'name' ? (
<span
{...row.getToggleRowExpandedProps({})}
className="tw-mr-2 tw-cursor-pointer"
style={{
marginLeft: `${row.depth * 35}px`,
}}>
<FontAwesomeIcon
icon={row.isExpanded ? faCaretDown : faCaretRight}
/>
</span>
) : null}
return [failingTests, passingTests];
};
{cell.column.id === 'columnTests' && (
<Fragment>
const getColumnTestsCell = (
columnTestLength: number | undefined,
failingTests: ColumnTest[],
passingTests: ColumnTest[]
) => {
return (
<>
{columnTestLength ? (
<Fragment>
<>
{failingTests.length ? (
<div className="tw-flex">
<p className="tw-mr-2">
@ -583,7 +490,7 @@ const EntityTable = ({
</p>
</div>
) : (
<Fragment>
<>
{passingTests.length ? (
<div className="tw-flex">
<div className="tw-mr-2">
@ -597,43 +504,44 @@ const EntityTable = ({
) : (
<p>{`${columnTestLength} tests`}</p>
)}
</Fragment>
</>
)}
</Fragment>
</>
) : (
'--'
)}
</Fragment>
)}
</>
);
};
{cell.column.id === 'dataTypeDisplay' && (
const getDataTypeDisplayCell = (record: ModifiedTableColumn | Column) => {
return (
<>
{cell.value ? (
{record.dataTypeDisplay ? (
<>
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap tw-w-60 tw-overflow-x-auto">
<RichTextEditorPreviewer
markdown={cell.value.toLowerCase()}
markdown={record.dataTypeDisplay.toLowerCase()}
/>
</div>
) : (
<>
{cell.value.length > 25 ? (
{record.dataTypeDisplay.length > 25 ? (
<span>
<PopOver
html={
<div className="tw-break-words">
<span>
{cell.value.toLowerCase()}
</span>
<span>{record.dataTypeDisplay.toLowerCase()}</span>
</div>
}
key="pop-over"
position="bottom"
theme="light"
trigger="click">
<div className="tw-cursor-pointer tw-underline tw-inline-block">
<RichTextEditorPreviewer
markdown={`${cell.value
markdown={`${record.dataTypeDisplay
.slice(0, 20)
.toLowerCase()}...`}
/>
@ -641,7 +549,7 @@ const EntityTable = ({
</PopOver>
</span>
) : (
cell.value.toLowerCase()
record.dataTypeDisplay.toLowerCase()
)}
</>
)}
@ -650,109 +558,35 @@ const EntityTable = ({
'--'
)}
</>
)}
);
};
{cell.column.id === 'tags' && (
<>
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap">
<TagsViewer
sizeCap={-1}
tags={cell.value || []}
/>
</div>
) : (
<div
data-testid="tags-wrapper"
onClick={() => handleTagContainerClick(row)}>
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.EditTags}
position="left"
trigger="click">
<TagsContainer
showAddTagButton
editable={editColumnTag?.index === row.id}
isLoading={
isTagLoading &&
editColumnTag?.index === row.id
}
selectedTags={cell.value || []}
size="small"
tagList={allTags}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags, row.original.name);
}}
/>
</NonAdminAction>
<div className="tw-mt-1">
{getRequestTagsElement(cell)}
{getFieldThreadElement(
getColumnName(cell),
'tags',
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
cell
)}${ENTITY_LINK_SEPARATOR}tags`,
Boolean(cell.value.length)
)}
{getFieldThreadElement(
getColumnName(cell),
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`${
EntityField.COLUMNS
}${ENTITY_LINK_SEPARATOR}${getColumnName(
cell
)}${ENTITY_LINK_SEPARATOR}${
EntityField.TAGS
}`,
Boolean(cell.value),
ThreadType.Task
)}
</div>
</div>
)}
</>
)}
{cell.column.id === 'description' && (
<div>
const getDescriptionCell = (
index: number,
record: ModifiedTableColumn | Column
) => {
return (
<div className="hover-icon-group">
<div className="tw-inline-block">
<div
className="tw-flex"
data-testid="description"
id={`column-description-${index}`}>
<div>
{cell.value ? (
<RichTextEditorPreviewer
markdown={cell.value}
/>
{record?.description ? (
<RichTextEditorPreviewer markdown={record?.description} />
) : (
<span className="tw-no-description">
No description{' '}
</span>
<span className="tw-no-description">No description</span>
)}
</div>
<div className="tw-flex tw--mt-2">
{!isReadOnly ? (
<Fragment>
{checkPermission() && (
<>
<button
className="tw-self-start tw-w-8 tw-h-8 tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none tw-flex-none"
onClick={() =>
handleUpdate(row.original, row.id)
}>
className="tw-self-start tw-w-8 tw-h-8 tw-ml-1 focus:tw-outline-none tw-flex-none hover-cell-icon"
onClick={() => handleUpdate(record, index)}>
<SVGIcons
alt="edit"
icon="icon-edit"
@ -760,31 +594,32 @@ const EntityTable = ({
width="14px"
/>
</button>
</>
)}
{getRequestDescriptionElement(cell)}
{getRequestDescriptionElement(record)}
{getFieldThreadElement(
getColumnName(cell),
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
cell
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(cell.value)
Boolean(record)
)}
{getFieldThreadElement(
getColumnName(cell),
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
cell
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(cell.value),
Boolean(record),
ThreadType.Task
)}
</Fragment>
@ -792,56 +627,40 @@ const EntityTable = ({
</div>
</div>
</div>
{checkIfJoinsAvailable(row.original.name) && (
<div
className="tw-mt-3"
data-testid="frequently-joined-columns">
{checkIfJoinsAvailable(record?.name) && (
<div className="tw-mt-3" data-testid="frequently-joined-columns">
<span className="tw-text-grey-muted tw-mr-1">
Frequently joined columns:
</span>
<span>
{getFrequentlyJoinedWithColumns(
row.original.name
)
{getFrequentlyJoinedWithColumns(record?.name)
.slice(0, 3)
.map((columnJoin, index) => (
<Fragment key={index}>
{index > 0 && (
<span className="tw-mr-1">,</span>
)}
{index > 0 && <span className="tw-mr-1">,</span>}
<Link
className="link-text"
to={getTableDetailsPath(
getTableFQNFromColumnFQN(
columnJoin?.fullyQualifiedName as string
),
getTableFQNFromColumnFQN(columnJoin.fullyQualifiedName),
getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName as string,
columnJoin.fullyQualifiedName,
[FqnPart.Column]
)
)}>
{getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName as string,
[
FqnPart.Database,
FqnPart.Table,
FqnPart.Column,
],
columnJoin.fullyQualifiedName,
[FqnPart.Database, FqnPart.Table, FqnPart.Column],
FQN_SEPARATOR_CHAR
)}
</Link>
</Fragment>
))}
{getFrequentlyJoinedWithColumns(
row.original.name
).length > 3 && (
{getFrequentlyJoinedWithColumns(record?.name).length > 3 && (
<PopOver
html={
<div className="tw-text-left">
{getFrequentlyJoinedWithColumns(
row.original.name
)
{getFrequentlyJoinedWithColumns(record?.name)
?.slice(3)
.map((columnJoin, index) => (
<Fragment key={index}>
@ -849,15 +668,15 @@ const EntityTable = ({
className="link-text tw-block tw-py-1"
href={getTableDetailsPath(
getTableFQNFromColumnFQN(
columnJoin?.fullyQualifiedName as string
columnJoin?.fullyQualifiedName
),
getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName as string,
columnJoin?.fullyQualifiedName,
[FqnPart.Column]
)
)}>
{getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName as string,
columnJoin?.fullyQualifiedName,
[
FqnPart.Database,
FqnPart.Table,
@ -872,46 +691,248 @@ const EntityTable = ({
position="bottom"
theme="light"
trigger="click">
<span className="show-more tw-ml-1 tw-underline">
...
</span>
<span className="show-more tw-ml-1 tw-underline">...</span>
</PopOver>
)}
</span>
</div>
)}
</div>
);
};
const getTagsCell = (index: number, record: ModifiedTableColumn | Column) => {
return (
<div className="hover-icon-group">
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap">
<TagsViewer sizeCap={-1} tags={record?.tags || []} />
</div>
) : (
<div
className={classNames(
`tw-flex tw-justify-content`,
editColumnTag?.index === index || !isEmpty(record.tags)
? 'tw-flex-col tw-items-start'
: 'tw-items-center'
)}
{cell.column.id === 'name' && (
data-testid="tags-wrapper"
onClick={() => {
if (!editColumnTag) {
handleEditColumnTag(record, index);
// Fetch tags and terms only once
if (allTags.length === 0 || tagFetchFailed) {
fetchTagsAndGlossaryTerms();
}
}
}}>
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.EditTags}
position="left"
trigger="click">
<TagsContainer
showAddTagButton
editable={editColumnTag?.index === index}
isLoading={isTagLoading && editColumnTag?.index === index}
selectedTags={record?.tags || []}
size="small"
tagList={allTags}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags, record?.name);
}}
/>
</NonAdminAction>
<div className="tw-mt-1 tw-flex">
{getRequestTagsElement(record)}
{getFieldThreadElement(
getColumnName(record),
'tags',
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}tags`,
Boolean(record?.name?.length)
)}
{getFieldThreadElement(
getColumnName(record),
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`${EntityField.COLUMNS}${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`,
Boolean(record?.name),
ThreadType.Task
)}
</div>
</div>
)}
</div>
);
};
const renderCell = useCallback(
(key: string, record: ModifiedTableColumn | Column, index: number) => {
const columnTestLength = record?.columnTests?.length;
const [failingTests, passingTests] = getTestStats(
record,
columnTestLength
);
switch (key) {
case TABLE_HEADERS_V1.columnTests:
return getColumnTestsCell(
columnTestLength,
failingTests,
passingTests
);
case TABLE_HEADERS_V1.dataTypeDisplay:
return getDataTypeDisplayCell(record);
case TABLE_HEADERS_V1.description:
return getDescriptionCell(index, record);
case TABLE_HEADERS_V1.tags:
return getTagsCell(index, record);
default:
return (
<Fragment>
{isReadOnly ? (
<div className="tw-inline-block">
<RichTextEditorPreviewer markdown={cell.value} />
<RichTextEditorPreviewer markdown={record.name} />
</div>
) : (
<span
style={{
paddingLeft: `${
row.canExpand ? '0px' : `${row.depth * 35}px`
}`,
}}>
{prepareConstraintIcon(
cell.value,
row.original.constraint
)}
{cell.render('Cell')}
<span>
{prepareConstraintIcon(record.name, record.constraint)}
<span className="tw-ml-4">{record.name}</span>
</span>
)}
</Fragment>
)}
</td>
);
})}
</tr>
}
},
[editColumnTag, isTagLoading, handleUpdate, handleTagSelection]
);
})}
</tbody>
</table>
const columns = useMemo(
() => [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
accessor: 'name',
ellipsis: true,
width: 180,
render: (
_: Array<unknown>,
record: ModifiedTableColumn,
index: number
) => renderCell(TABLE_HEADERS_V1.name, record, index),
},
{
title: 'Type',
dataIndex: 'dataTypeDisplay',
key: 'dataTypeDisplay',
accessor: 'dataTypeDisplay',
ellipsis: true,
width: 200,
render: (
_: Array<unknown>,
record: ModifiedTableColumn,
index: number
) => {
return renderCell(TABLE_HEADERS_V1.dataTypeDisplay, record, index);
},
},
{
title: 'Data Quality',
dataIndex: 'columnTests',
key: 'columnTests',
accessor: 'columnTests',
width: 200,
render: (
_: Array<unknown>,
record: ModifiedTableColumn,
index: number
) => {
return renderCell(TABLE_HEADERS_V1.columnTests, record, index);
},
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
accessor: 'description',
render: (
_: Array<unknown>,
record: ModifiedTableColumn,
index: number
) => renderCell(TABLE_HEADERS_V1.description, record, index),
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
accessor: 'tags',
width: 272,
render: (
_: Array<unknown>,
record: ModifiedTableColumn | Column,
index: number
) => renderCell(TABLE_HEADERS_V1.tags, record, index),
},
],
[editColumnTag, isTagLoading, renderCell]
);
useEffect(() => {
if (!searchText) {
setSearchedColumns(tableColumns);
} else {
const searchCols = searchInColumns(tableColumns, searchText);
setSearchedColumns(searchCols);
}
}, [searchText, tableColumns]);
return (
<>
<Table
columns={columns}
data-testid="entity-table"
dataSource={data}
expandable={{
defaultExpandedRowKeys: [],
expandIcon: ({ expanded, onExpand, record }) =>
record.children ? (
<FontAwesomeIcon
className="tw-mr-2 tw-cursor-pointer"
icon={expanded ? faCaretDown : faCaretRight}
onClick={(e) =>
onExpand(
record,
e as unknown as React.MouseEvent<HTMLElement, MouseEvent>
)
}
/>
) : null,
}}
pagination={false}
size="small"
/>
{editColumn && (
<ModalWithMarkdownEditor
header={`Edit column: "${editColumn.column.name}"`}
@ -919,9 +940,10 @@ const EntityTable = ({
value={editColumn.column.description as string}
onCancel={closeEditColumnModal}
onSave={handleEditColumnChange}
// expandable={}
/>
)}
</div>
</>
);
};

View File

@ -1,35 +0,0 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const TABLE_HEADERS = [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Type',
accessor: 'dataTypeDisplay',
},
{
Header: 'Data Quality',
accessor: 'columnTests',
},
{
Header: 'Description',
accessor: 'description',
},
{
Header: 'Tags',
accessor: 'tags',
},
];

View File

@ -0,0 +1,7 @@
export const TABLE_HEADERS_V1 = {
name: 'name',
dataTypeDisplay: 'dataTypeDisplay',
columnTests: 'columnTests',
description: 'description',
tags: 'tags',
};

View File

@ -0,0 +1,10 @@
.hover-icon-group {
.hover-cell-icon {
opacity: 0;
}
&:hover {
.hover-cell-icon {
opacity: 100;
}
}
}

View File

@ -11,55 +11,17 @@
* limitations under the License.
*/
import {
findAllByTestId,
findByTestId,
fireEvent,
queryByTestId,
render,
} from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { flatten } from 'lodash';
import { FormattedGlossaryTermData, TagOption } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Column } from '../../generated/api/data/createTable';
import { Table } from '../../generated/entity/data/table';
import { TagCategory, TagClass } from '../../generated/entity/tags/tagCategory';
import { ModifiedTableColumn } from '../../interface/dataQuality.interface';
import { fetchGlossaryTerms } from '../../utils/GlossaryUtils';
import { getTagCategories } from '../../utils/TagsUtils';
import EntityTable from './EntityTable.component';
const mockTableheader = [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Type',
accessor: 'dataTypeDisplay',
},
{
Header: 'Data Quality',
accessor: 'columnTests',
},
{
Header: 'Description',
accessor: 'description',
},
{
Header: 'Tags',
accessor: 'tags',
},
];
const mockEntityFieldThreads = [
{
entityLink:
'<#E::table::bigquery_gcp.ecommerce.shopify.raw_product_catalog::columns::products::description>',
count: 1,
entityField: 'columns::products::description',
},
];
import EntityTableV1 from './EntityTable.component';
import type { ColumnsType } from 'antd/es/table';
const onEntityFieldSelect = jest.fn();
const onThreadLinkSelect = jest.fn();
@ -71,6 +33,15 @@ const mockTableConstraints = [
},
] as Table['tableConstraints'];
type ColumnDataType = {
key: string;
name: string;
dataTypeDisplay: string;
columnTests: string;
description: string;
tags: string;
};
const mockEntityTableProp = {
tableColumns: [
{
@ -230,6 +201,11 @@ jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: jest.fn().mockReturnValue(<i>Icon</i>),
}));
jest.mock('@fortawesome/free-solid-svg-icons', () => ({
faCaretDown: jest.fn().mockReturnValue(<i>faCaretDown</i>),
faCaretRight: jest.fn().mockReturnValue(<i>faCaretRight</i>),
}));
jest.mock('../common/non-admin-action/NonAdminAction', () => {
return jest
.fn()
@ -241,9 +217,11 @@ jest.mock('../common/non-admin-action/NonAdminAction', () => {
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
jest.mock('../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor', () => ({
ModalWithMarkdownEditor: jest.fn().mockReturnValue(<p>EditorModal</p>),
}));
jest.mock('../tags-container/tags-container', () => {
return jest.fn().mockImplementation(({ tagList }) => {
return (
@ -255,9 +233,11 @@ jest.mock('../tags-container/tags-container', () => {
);
});
});
jest.mock('../tags-viewer/tags-viewer', () => {
return jest.fn().mockReturnValue(<p>TagViewer</p>);
});
jest.mock('../tags/tags', () => {
return jest.fn().mockReturnValue(<p>Tag</p>);
});
@ -286,258 +266,82 @@ jest.mock('../../utils/TagsUtils', () => ({
}),
}));
jest.mock('./EntityTable.constant', () => {
return {
TABLE_HEADERS: [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Type',
accessor: 'dataTypeDisplay',
},
{
Header: 'Data Quality',
accessor: 'columnTests',
},
{
Header: 'Description',
accessor: 'description',
},
{
Header: 'Tags',
accessor: 'tags',
},
],
};
});
jest.mock('antd', () => ({
Popover: jest
.fn()
.mockImplementation(({ children }) => <div>{children}</div>),
Table: jest.fn().mockImplementation(({ columns, dataSource }) => (
<table data-testid="entity-table">
<thead>
<tr>
{(columns as ColumnsType<ColumnDataType>).map((col) => (
<th key={col.key}>{col.title}</th>
))}
</tr>
</thead>
<tbody key="tbody">
{dataSource.map((row: ModifiedTableColumn | Column, i: number) => (
<tr key={i}>
{(columns as ColumnsType<ColumnDataType>).map((col, index) => (
<td key={col.key}>
{col.render ? col.render(row, dataSource, index) : 'alt'}
</td>
))}
</tr>
))}
</tbody>
</table>
)),
}));
describe('Test EntityTable Component', () => {
it('Check if it has all child elements', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
it('Initially, Table should load', async () => {
render(<EntityTableV1 {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
const entityTable = await screen.findByTestId('entity-table');
expect(entityTable).toBeInTheDocument();
const tableHeader = await findByTestId(container, 'table-header');
expect(tableHeader).toBeInTheDocument();
for (let index = 0; index < mockTableheader.length; index++) {
const headerValue = mockTableheader[index];
const header = await findByTestId(tableHeader, `${headerValue.accessor}`);
expect(header).toBeInTheDocument();
}
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
expect(tableRows).toHaveLength(mockEntityTableProp.tableColumns.length);
});
it('should render request description button', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
render(<EntityTableV1 {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
const entityTable = await screen.findByTestId('entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const requestDescriptionButton = await findByTestId(
tableRows[0],
const requestDescriptionButton = await screen.findAllByTestId(
'request-description'
);
expect(requestDescriptionButton).toBeInTheDocument();
const descriptionThread = queryByTestId(tableRows[0], 'field-thread');
const startDescriptionThread = queryByTestId(
tableRows[0],
'start-field-thread'
);
// should not be in the document, as request description button is present
expect(descriptionThread).not.toBeInTheDocument();
expect(startDescriptionThread).not.toBeInTheDocument();
expect(requestDescriptionButton[0]).toBeInTheDocument();
});
it('Should render start thread button', async () => {
const { container } = render(<EntityTable {...mockEntityTableProp} />, {
render(<EntityTableV1 {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await findByTestId(container, 'entity-table');
const entityTable = await screen.findByTestId('entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const startThreadButton = await findByTestId(
tableRows[4],
const startThreadButton = await screen.findAllByTestId(
'start-field-thread'
);
expect(startThreadButton).toBeInTheDocument();
expect(startThreadButton[0]).toBeInTheDocument();
fireEvent.click(
startThreadButton,
startThreadButton[0],
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
});
it('Should render thread button with count', async () => {
const { container } = render(
<EntityTable
{...mockEntityTableProp}
entityFieldThreads={mockEntityFieldThreads}
/>,
{
wrapper: MemoryRouter,
}
);
const entityTable = await findByTestId(container, 'entity-table');
expect(entityTable).toBeInTheDocument();
const tableBody = await findByTestId(container, 'table-body');
expect(tableBody).toBeInTheDocument();
const tableRows = await findAllByTestId(tableBody, 'row');
const threadButton = await findByTestId(tableRows[1], 'field-thread');
expect(threadButton).toBeInTheDocument();
fireEvent.click(
threadButton,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toBeCalled();
const threadCount = await findByTestId(threadButton, 'field-thread-count');
expect(threadCount).toBeInTheDocument();
expect(threadCount).toHaveTextContent(
String(mockEntityFieldThreads[0].count)
);
});
it('Check if tags and glossary-terms are present', async () => {
const { getAllByTestId, findAllByText } = render(
<EntityTable {...mockEntityTableProp} />,
{
wrapper: MemoryRouter,
}
);
const tagWrapper = getAllByTestId('tags-wrapper')[0];
fireEvent.click(tagWrapper);
const tag1 = await findAllByText('TagCat1.Tag1');
const glossaryTerm1 = await findAllByText('Glossary.Tag1');
expect(tag1).toHaveLength(mockEntityTableProp.tableColumns.length);
expect(glossaryTerm1).toHaveLength(mockEntityTableProp.tableColumns.length);
});
it('Check if only tags are present', async () => {
(fetchGlossaryTerms as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
const { getAllByTestId, findAllByText, queryAllByText } = render(
<EntityTable {...mockEntityTableProp} />,
{
wrapper: MemoryRouter,
}
);
const tagWrapper = getAllByTestId('tags-wrapper')[0];
fireEvent.click(
tagWrapper,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
const tag1 = await findAllByText('TagCat1.Tag1');
const glossaryTerm1 = queryAllByText('Glossary.Tag1');
expect(tag1).toHaveLength(mockEntityTableProp.tableColumns.length);
expect(glossaryTerm1).toHaveLength(0);
});
it('Check if only glossary terms are present', async () => {
(getTagCategories as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
const { getAllByTestId, findAllByText, queryAllByText } = render(
<EntityTable {...mockEntityTableProp} />,
{
wrapper: MemoryRouter,
}
);
const tagWrapper = getAllByTestId('tags-wrapper')[0];
fireEvent.click(
tagWrapper,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
const tag1 = queryAllByText('TagCat1.Tag1');
const glossaryTerm1 = await findAllByText('Glossary.Tag1');
expect(tag1).toHaveLength(0);
expect(glossaryTerm1).toHaveLength(mockEntityTableProp.tableColumns.length);
});
it('Check that tags and glossary terms are not present', async () => {
(getTagCategories as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
(fetchGlossaryTerms as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
const { getAllByTestId, queryAllByText } = render(
<EntityTable {...mockEntityTableProp} />,
{
wrapper: MemoryRouter,
}
);
const tagWrapper = getAllByTestId('tags-wrapper')[0];
fireEvent.click(
tagWrapper,
new MouseEvent('click', { bubbles: true, cancelable: true })
);
const tag1 = queryAllByText('TagCat1.Tag1');
const glossaryTerm1 = queryAllByText('Glossary.Tag1');
expect(tag1).toHaveLength(0);
expect(glossaryTerm1).toHaveLength(0);
});
});

View File

@ -21,7 +21,7 @@ import {
} from '../../generated/entity/data/table';
import { ThreadType } from '../../generated/entity/feed/thread';
import Searchbar from '../common/searchbar/Searchbar';
import EntityTable from '../EntityTable/EntityTable.component';
import EntityTableV1 from '../EntityTable/EntityTable.component';
type Props = {
owner?: Table['owner'];
@ -76,7 +76,7 @@ const SchemaTab: FunctionComponent<Props> = ({
<div className="row">
{columns?.length > 0 ? (
<div className="col-sm-12">
<EntityTable
<EntityTableV1
columnName={columnName}
entityFieldTasks={entityFieldTasks}
entityFieldThreads={entityFieldThreads}

View File

@ -76,7 +76,7 @@ jest.mock('../SampleDataTable/SampleDataTable.component', () => {
});
jest.mock('../EntityTable/EntityTable.component', () => {
return jest.fn().mockReturnValue(<p>EntityTable</p>);
return jest.fn().mockReturnValue(<p>EntityTableV1</p>);
});
const mockTableConstraints = [

View File

@ -140,7 +140,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
<div
className={classNames('tw-cursor-pointer', containerClass)}
data-testid="tag-container">
<div>
<div className="tw-flex tw-flex-wrap">
{showTags && !editable && (
<Fragment>
{showAddTagButton && (

View File

@ -45,7 +45,7 @@ export const getFieldThreadElement = (
return !isEmpty(threadValue) ? (
<button
className="link-text tw-self-start tw-w-8 tw-h-8 tw-flex-none tw-mx-1 tw-opacity-0 group-hover:tw-opacity-100"
className="link-text tw-self-start tw-w-8 tw-h-8 tw-flex-none tw-mx-1 hover-cell-icon"
data-testid="field-thread"
onClick={(e) => {
e.preventDefault();
@ -70,7 +70,7 @@ export const getFieldThreadElement = (
<Fragment>
{entityType && entityFqn && entityField && flag && !isTaskType ? (
<button
className="link-text tw-self-start tw-w-8 tw-h-8 tw-flex-none tw-mx-1 tw-opacity-0 group-hover:tw-opacity-100"
className="link-text tw-self-start tw-w-8 tw-h-8 tw-flex-none tw-mx-1 hover-cell-icon"
data-testid="start-field-thread"
onClick={(e) => {
e.preventDefault();