fix(ui): show full content for ellipsis column names (#8443)

* fix(ui): show full content for ellipsis column names

* fix unit tests and mock i18next

* address comments

* fix cypress failure

* address review comments
improve unit tests console errors
This commit is contained in:
Chirag Madlani 2022-11-01 16:26:40 +05:30 committed by GitHub
parent 5819c45937
commit ca91bafa39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 549 additions and 344 deletions

View File

@ -95,7 +95,7 @@ describe('Tags page should work', () => {
});
});
it('Add new tag flow shoud work properly', () => {
it('Add new tag flow should work properly', () => {
cy.get('[data-testid="side-panel-category"]')
.contains(NEW_TAG_CATEGORY.name)
.should('be.visible')

View File

@ -75,6 +75,7 @@ const DashboardDetailsProps = {
chartUrl: 'http://localhost',
chartType: 'Area',
displayName: 'Test chart',
id: '1',
},
] as ChartType[],
serviceType: '',

View File

@ -38,7 +38,7 @@ export const dashboardVersionProp = {
deleted: false,
},
{
id: '0698ab5d-a122-4b86-a6e5-d10bf3550bd7',
id: '0698ab5d-a122-4b86-a6e5-d10bf3550bd6',
type: 'chart',
name: 'without_description',
description: '',

View File

@ -40,7 +40,7 @@ import TabsPane from '../common/TabsPane/TabsPane';
import PageContainer from '../containers/PageContainer';
import EntityVersionTimeLine from '../EntityVersionTimeLine/EntityVersionTimeLine';
import Loader from '../Loader/Loader';
import SchemaTab from '../SchemaTab/SchemaTab.component';
import VersionTable from '../VersionTable/VersionTable.component';
import { DatasetVersionProp } from './DatasetVersion.interface';
const DatasetVersion: React.FC<DatasetVersionProp> = ({
@ -406,8 +406,7 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
</div>
<div className="tw-col-span-full">
<SchemaTab
isReadOnly
<VersionTable
columnName={getPartialNameFromTableFQN(
datasetFQN,
[FqnPart.Column],
@ -418,7 +417,6 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
// TODO: Below we should have separate type for Dataset instead casting it to `Table`
(currentVersionData as Table).joins as ColumnJoins[]
}
tableConstraints={[]}
/>
</div>
</div>

View File

@ -13,7 +13,8 @@
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Popover, Table } from 'antd';
import { Popover, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import classNames from 'classnames';
import { cloneDeep, isEmpty, isUndefined, lowerCase } from 'lodash';
import { EntityFieldThreads, EntityTags, TagOption } from 'Models';
@ -25,20 +26,19 @@ import React, {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useHistory } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTableDetailsPath } from '../../constants/constants';
import { EntityField } from '../../constants/feed.constants';
import { SettledStatus } from '../../enums/axios.enum';
import { EntityType, FqnPart } from '../../enums/entity.enum';
import { Column, JoinedWith } from '../../generated/entity/data/table';
import { Column } from '../../generated/entity/data/table';
import { ThreadType } from '../../generated/entity/feed/thread';
import { LabelType, State, TagLabel } from '../../generated/type/tagLabel';
import { getPartialNameFromTableFQN } from '../../utils/CommonUtils';
import {
getPartialNameFromTableFQN,
getTableFQNFromColumnFQN,
} from '../../utils/CommonUtils';
import { ENTITY_LINK_SEPARATOR } from '../../utils/EntityUtils';
ENTITY_LINK_SEPARATOR,
getFrequentlyJoinedColumns,
} from '../../utils/EntityUtils';
import { getFieldThreadElement } from '../../utils/FeedElementUtils';
import {
fetchGlossaryTerms,
@ -62,8 +62,7 @@ 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_V1 } from './EntityTable.constants';
import { EntityTableProps } from './EntityTable.interface';
import { EntityTableProps, TableCellRendered } from './EntityTable.interface';
import './EntityTable.style.less';
const EntityTable = ({
@ -252,21 +251,6 @@ const EntityTable = ({
setEditColumnTag(undefined);
};
const getFrequentlyJoinedWithColumns = (
columnName: string
): Array<JoinedWith> => {
return (
joins.find((join) => join.columnName === columnName)?.joinedWith || []
);
};
const checkIfJoinsAvailable = (columnName: string): boolean => {
return (
joins &&
Boolean(joins.length) &&
Boolean(joins.find((join) => join.columnName === columnName))
);
};
const searchInColumns = (table: Column[], searchText: string): Column[] => {
const searchedValue: Column[] = table.reduce((searchedCols, column) => {
const isContainData =
@ -438,46 +422,30 @@ const EntityTable = ({
);
};
const getDataTypeDisplayCell = (record: Column | Column) => {
const renderDataTypeDisplay: TableCellRendered<Column, 'dataTypeDisplay'> = (
dataTypeDisplay
) => {
return (
<>
{record.dataTypeDisplay ? (
<>
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap tw-w-60 tw-overflow-x-auto">
<RichTextEditorPreviewer
markdown={record.dataTypeDisplay.toLowerCase()}
/>
</div>
) : (
<>
{record.dataTypeDisplay.length > 25 ? (
<span>
<PopOver
html={
<div className="tw-break-words">
<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={`${record.dataTypeDisplay
.slice(0, 20)
.toLowerCase()}...`}
/>
</div>
</PopOver>
</span>
) : (
record.dataTypeDisplay.toLowerCase()
)}
</>
)}
</>
{dataTypeDisplay ? (
isReadOnly || (dataTypeDisplay.length < 25 && !isReadOnly) ? (
lowerCase(dataTypeDisplay)
) : (
<PopOver
html={
<div className="break-word">
<span>{lowerCase(dataTypeDisplay)}</span>
</div>
}
key="pop-over"
position="bottom"
theme="light"
trigger="click">
<Typography.Text ellipsis className="cursor-pointer">
{dataTypeDisplay}
</Typography.Text>
</PopOver>
)
) : (
'--'
)}
@ -485,24 +453,28 @@ const EntityTable = ({
);
};
const getDescriptionCell = (index: number, record: Column | Column) => {
const renderDescription: TableCellRendered<Column, 'description'> = (
description,
record,
index
) => {
return (
<div className="hover-icon-group">
<div className="tw-inline-block">
<div className="d-inline-block">
<div
className="tw-flex"
className="d-flex"
data-testid="description"
id={`column-description-${index}`}>
<div>
{record?.description ? (
<RichTextEditorPreviewer markdown={record?.description} />
{description ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="tw-no-description">
{t('label.no-description')}
</span>
)}
</div>
<div className="tw-flex tw--mt-1.5">
<div className="d-flex tw--mt-1.5">
{!isReadOnly ? (
<Fragment>
{hasDescriptionEditAccess && (
@ -550,189 +522,97 @@ const EntityTable = ({
</div>
</div>
</div>
{checkIfJoinsAvailable(record?.name) && (
<div className="tw-mt-3" data-testid="frequently-joined-columns">
<span className="tw-text-grey-muted tw-mr-1">
{t('label.frequently-joined-columns')}:
</span>
<span>
{getFrequentlyJoinedWithColumns(record?.name)
.slice(0, 3)
.map((columnJoin, index) => (
<Fragment key={index}>
{index > 0 && <span className="tw-mr-1">,</span>}
<Link
className="link-text"
to={getTableDetailsPath(
getTableFQNFromColumnFQN(columnJoin.fullyQualifiedName),
getPartialNameFromTableFQN(
columnJoin.fullyQualifiedName,
[FqnPart.Column]
)
)}>
{getPartialNameFromTableFQN(
columnJoin.fullyQualifiedName,
[FqnPart.Database, FqnPart.Table, FqnPart.Column],
FQN_SEPARATOR_CHAR
)}
</Link>
</Fragment>
))}
{getFrequentlyJoinedWithColumns(record?.name).length > 3 && (
<PopOver
html={
<div className="tw-text-left">
{getFrequentlyJoinedWithColumns(record?.name)
?.slice(3)
.map((columnJoin, index) => (
<Fragment key={index}>
<a
className="link-text tw-block tw-py-1"
href={getTableDetailsPath(
getTableFQNFromColumnFQN(
columnJoin?.fullyQualifiedName
),
getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName,
[FqnPart.Column]
)
)}>
{getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName,
[
FqnPart.Database,
FqnPart.Table,
FqnPart.Column,
]
)}
</a>
</Fragment>
))}
</div>
}
position="bottom"
theme="light"
trigger="click">
<span className="show-more tw-ml-1 tw-underline">...</span>
</PopOver>
)}
</span>
</div>
{getFrequentlyJoinedColumns(
record?.name,
joins,
t('label.frequently-joined-columns')
)}
</div>
);
};
const getTagsCell = (index: number, record: Column | 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'
)}
data-testid="tags-wrapper"
onClick={() => {
if (!editColumnTag) {
handleEditColumnTag(record, index);
// Fetch tags and terms only once
if (allTags.length === 0 || tagFetchFailed) {
fetchTagsAndGlossaryTerms();
}
}
}}>
<TagsContainer
className="w-max-256"
editable={editColumnTag?.index === index}
isLoading={isTagLoading && editColumnTag?.index === index}
selectedTags={record?.tags || []}
showAddTagButton={hasTagEditAccess}
size="small"
tagList={allTags}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags, record?.name);
}}
/>
<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
)}
const renderTags: TableCellRendered<Column, 'tags'> = useCallback(
(tags, record: Column, index: number) => {
return (
<div className="hover-icon-group">
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap">
<TagsViewer sizeCap={-1} tags={tags || []} />
</div>
</div>
)}
</div>
);
};
const renderCell = useCallback(
(key: string, record: Column | Column, index: number) => {
switch (key) {
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={record.name} />
</div>
) : (
<span>
{prepareConstraintIcon(record.name, record.constraint)}
<span>{record.name}</span>
</span>
) : (
<div
className={classNames(
`tw-flex tw-justify-content`,
editColumnTag?.index === index || !isEmpty(tags)
? 'tw-flex-col tw-items-start'
: 'tw-items-center'
)}
</Fragment>
);
}
data-testid="tags-wrapper"
onClick={() => {
if (!editColumnTag) {
handleEditColumnTag(record, index);
// Fetch tags and terms only once
if (allTags.length === 0 || tagFetchFailed) {
fetchTagsAndGlossaryTerms();
}
}
}}>
<TagsContainer
className="w-max-256"
editable={editColumnTag?.index === index}
isLoading={isTagLoading && editColumnTag?.index === index}
selectedTags={tags || []}
showAddTagButton={hasTagEditAccess}
size="small"
tagList={allTags}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(selectedTags) => {
handleTagSelection(selectedTags, record?.name);
}}
/>
<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>
);
},
[editColumnTag, isTagLoading, handleUpdate, handleTagSelection]
[isReadOnly, editColumnTag, hasTagEditAccess, isTagLoading]
);
const columns = useMemo(
const columns: ColumnsType<Column> = useMemo(
() => [
{
title: t('label.name'),
@ -741,8 +621,12 @@ const EntityTable = ({
accessor: 'name',
ellipsis: true,
width: 180,
render: (_: Array<unknown>, record: Column, index: number) =>
renderCell(TABLE_HEADERS_V1.name, record, index),
render: (name: Column['name'], record: Column) => (
<Popover destroyTooltipOnHide content={name} trigger="hover">
{prepareConstraintIcon(name, record.constraint)}
<span>{name}</span>
</Popover>
),
},
{
title: t('label.type'),
@ -751,17 +635,14 @@ const EntityTable = ({
accessor: 'dataTypeDisplay',
ellipsis: true,
width: 200,
render: (_: Array<unknown>, record: Column, index: number) => {
return renderCell(TABLE_HEADERS_V1.dataTypeDisplay, record, index);
},
render: renderDataTypeDisplay,
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
accessor: 'description',
render: (_: Array<unknown>, record: Column, index: number) =>
renderCell(TABLE_HEADERS_V1.description, record, index),
render: renderDescription,
},
{
title: t('label.tags'),
@ -769,11 +650,10 @@ const EntityTable = ({
key: 'tags',
accessor: 'tags',
width: 272,
render: (_: Array<unknown>, record: Column | Column, index: number) =>
renderCell(TABLE_HEADERS_V1.tags, record, index),
render: renderTags,
},
],
[editColumnTag, isTagLoading, renderCell]
[editColumnTag, isTagLoading, handleUpdate, handleTagSelection]
);
useEffect(() => {

View File

@ -12,6 +12,7 @@
*/
import { EntityFieldThreads } from 'Models';
import { ReactNode } from 'react';
import { ThreadType } from '../../generated/api/feed/createThread';
import { Column, ColumnJoins, Table } from '../../generated/entity/data/table';
@ -31,3 +32,13 @@ export interface EntityTableProps {
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
}
export type TableCellRendered<T, K extends keyof T> = (
value: T[K],
record: T,
index: number
) => ReactNode;
export interface DataTypeDisplayCellProps {
dataTypeDisplay: Column['dataTypeDisplay'];
}

View File

@ -20,7 +20,6 @@ import { Column } from '../../generated/api/data/createTable';
import { Table } from '../../generated/entity/data/table';
import { TagCategory, TagClass } from '../../generated/entity/tags/tagCategory';
import EntityTableV1 from './EntityTable.component';
import type { ColumnsType } from 'antd/es/table';
const onEntityFieldSelect = jest.fn();
const onThreadLinkSelect = jest.fn();
@ -32,15 +31,6 @@ const mockTableConstraints = [
},
] as Table['tableConstraints'];
type ColumnDataType = {
key: string;
name: string;
dataTypeDisplay: string;
columnTests: string;
description: string;
tags: string;
};
const mockEntityTableProp = {
tableColumns: [
{
@ -257,35 +247,6 @@ jest.mock('../../utils/TagsUtils', () => ({
}),
}));
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: 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('Initially, Table should load', async () => {
render(<EntityTableV1 {...mockEntityTableProp} />, {

View File

@ -12,33 +12,10 @@
*/
import { lowerCase } from 'lodash';
import { EntityFieldThreads } from 'Models';
import React, { Fragment, FunctionComponent, useState } from 'react';
import {
ColumnJoins,
Table,
TableData,
} from '../../generated/entity/data/table';
import { ThreadType } from '../../generated/entity/feed/thread';
import Searchbar from '../common/searchbar/Searchbar';
import EntityTableV1 from '../EntityTable/EntityTable.component';
type Props = {
columns: Table['columns'];
joins: Array<ColumnJoins>;
columnName: string;
tableConstraints: Table['tableConstraints'];
sampleData?: TableData;
hasDescriptionEditAccess?: boolean;
hasTagEditAccess?: boolean;
isReadOnly?: boolean;
entityFqn?: string;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
onUpdate?: (columns: Table['columns']) => Promise<void>;
};
import { Props } from './SchemaTab.interfaces';
const SchemaTab: FunctionComponent<Props> = ({
columns,

View File

@ -0,0 +1,37 @@
/*
* Copyright 2022 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.
*/
import { EntityFieldThreads } from 'Models';
import { ThreadType } from '../../generated/api/feed/createThread';
import {
ColumnJoins,
Table,
TableData,
} from '../../generated/entity/data/table';
export type Props = {
columns: Table['columns'];
joins: Array<ColumnJoins>;
columnName: string;
tableConstraints: Table['tableConstraints'];
sampleData?: TableData;
hasDescriptionEditAccess?: boolean;
hasTagEditAccess?: boolean;
isReadOnly?: boolean;
entityFqn?: string;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
onUpdate?: (columns: Table['columns']) => Promise<void>;
};

View File

@ -0,0 +1,155 @@
/*
* Copyright 2022 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.
*/
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Col, Row, Table } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Column } from '../../generated/entity/data/table';
import {
getFrequentlyJoinedColumns,
searchInColumns,
} from '../../utils/EntityUtils';
import { makeData } from '../../utils/TableUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import Searchbar from '../common/searchbar/Searchbar';
import TagsViewer from '../tags-viewer/tags-viewer';
import { VersionTableProps } from './VersionTable.interfaces';
const VersionTable = ({ columnName, columns, joins }: VersionTableProps) => {
const [searchedColumns, setSearchedColumns] = useState<Column[]>([]);
const { t } = useTranslation();
const [searchText, setSearchText] = useState('');
const data = useMemo(() => makeData(searchedColumns), [searchedColumns]);
const versionTableColumns = useMemo(
() => [
{
title: t('label.name'),
dataIndex: 'name',
key: 'name',
accessor: 'name',
ellipsis: true,
width: 180,
render: (name: Column['name']) => (
<div className="d-inline-block">
<RichTextEditorPreviewer markdown={name} />
</div>
),
},
{
title: t('label.type'),
dataIndex: 'dataTypeDisplay',
key: 'dataTypeDisplay',
accessor: 'dataTypeDisplay',
ellipsis: true,
width: 200,
render: (dataTypeDisplay: Column['dataTypeDisplay']) =>
dataTypeDisplay ? (
<RichTextEditorPreviewer markdown={dataTypeDisplay.toLowerCase()} />
) : (
'--'
),
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
accessor: 'description',
render: (description: Column['description']) =>
description ? (
<>
<RichTextEditorPreviewer markdown={description} />
{getFrequentlyJoinedColumns(
columnName,
joins,
t('label.frequently-joined-columns')
)}
</>
) : (
<span className="tw-no-description">
{t('label.no-description')}
</span>
),
},
{
title: t('label.tags'),
dataIndex: 'tags',
key: 'tags',
accessor: 'tags',
width: 272,
render: (tags: Column['tags']) => (
<div className="flex flex-wrap">
<TagsViewer sizeCap={-1} tags={tags || []} />
</div>
),
},
],
[]
);
const handleSearchAction = (searchValue: string) => {
setSearchText(searchValue);
};
useEffect(() => {
if (!searchText) {
setSearchedColumns(columns);
} else {
const searchCols = searchInColumns(columns, searchText);
setSearchedColumns(searchCols);
}
}, [searchText, columns]);
return (
<Row>
<Col>
<Searchbar
placeholder={`${t('label.find-in-table')}...`}
searchValue={searchText}
typingInterval={500}
onSearch={handleSearchAction}
/>
</Col>
<Col>
<Table
columns={versionTableColumns}
data-testid="entity-table"
dataSource={data}
expandable={{
defaultExpandedRowKeys: [],
expandIcon: ({ expanded, onExpand, record }) =>
record.children ? (
<FontAwesomeIcon
className="m-r-xs cursor-pointer"
icon={expanded ? faCaretDown : faCaretRight}
onClick={(e) =>
onExpand(
record,
e as unknown as React.MouseEvent<HTMLElement>
)
}
/>
) : null,
}}
pagination={false}
size="small"
/>
</Col>
</Row>
);
};
export default VersionTable;

View File

@ -0,0 +1,20 @@
/*
* Copyright 2022 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.
*/
import { ColumnJoins, Table } from '../../generated/entity/data/table';
export interface VersionTableProps {
columnName: string;
columns: Table['columns'];
joins: Array<ColumnJoins>;
}

View File

@ -129,12 +129,12 @@ describe('Test CronEditor component', () => {
render(<CronEditor {...mockProps} />);
const cronType = await screen.findByTestId('cron-type');
act(async () => {
await userEvent.selectOptions(cronType, '');
expect(
await screen.findByTestId('manual-segment-container')
).toBeInTheDocument();
await act(async () => {
userEvent.selectOptions(cronType, '');
});
expect(
await screen.findByTestId('manual-segment-container')
).toBeInTheDocument();
});
});

View File

@ -149,7 +149,6 @@
"bot": "bot",
"for": "for",
"are-you-sure": "Are you sure?",
"email": "Email",
"select-token-expiration": "Select Token Expiration",
"token-expiration": "Token Expiration",
"select-auth-mechanism": "Select Auth Mechanism",
@ -172,6 +171,7 @@
"sql-query": "SQL Query",
"sql-query-tooltip": "Queries returning 1 or more rows will result in the test failing.",
"scopes-comma-separated": "Scopes value comma separated",
"find-in-table": "Find in table",
"data-insight-summary": "OpenMetadata health at a glance",
"data-insight-description-summary": "Percentage of Entities With Description",
"data-insight-owner-summary": "Percentage of Entities With Owners",

View File

@ -32,12 +32,6 @@ jest.mock('react-lazylog', () => ({
LazyLog: jest.fn().mockImplementation(() => <div>LazyLog</div>),
}));
jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockReturnValue({
t: (key: string) => key,
}),
}));
jest.mock('../../axiosAPIs/ingestionPipelineAPI', () => ({
getIngestionPipelineLogById: jest
.fn()

View File

@ -197,7 +197,7 @@ const AddUsersModalV1 = ({
className="user-list"
data={uniqueUser}
height={ADD_USER_CONTAINER_HEIGHT}
itemKey="user"
itemKey="id"
onScroll={onScroll}>
{(User) => (
<UserCard

View File

@ -53,3 +53,12 @@ window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
}));
/**
* mock react-i18next
*/
jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockReturnValue({
t: (key) => key,
}),
}));

View File

@ -62,6 +62,9 @@
text-align: right;
}
.text-underline {
text-decoration: underline;
}
// Width
.w-8 {
width: 32px;
@ -177,6 +180,9 @@
align-items: center;
justify-content: center;
}
.flex-wrap {
flex-wrap: wrap;
}
.break-word {
word-break: break-word;
@ -252,10 +258,6 @@
}
}
.underline {
text-decoration: underline;
}
.no-underline {
text-decoration: none;
}

View File

@ -32,7 +32,12 @@
.d-flex {
display: flex;
}
.d-inline-block {
display: inline-block;
}
.d-block {
display: block;
}
.flex-1 {
flex: 1;
}

View File

@ -11,15 +11,18 @@
* limitations under the License.
*/
import { isEmpty, isNil, isUndefined, startCase } from 'lodash';
import { isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash';
import { Bucket, LeafNodes, LineagePos } from 'Models';
import React from 'react';
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import PopOver from '../components/common/popover/PopOver';
import { EntityData } from '../components/common/PopOverCard/EntityPopOverCard';
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
getDatabaseDetailsPath,
getServiceDetailsPath,
getTableDetailsPath,
getTeamAndUserDetailsPath,
} from '../constants/constants';
import { AssetsType, EntityType, FqnPart } from '../enums/entity.enum';
@ -28,12 +31,21 @@ import { ServiceCategory } from '../enums/service.enum';
import { PrimaryTableDataTypes } from '../enums/table.enum';
import { Dashboard } from '../generated/entity/data/dashboard';
import { Pipeline } from '../generated/entity/data/pipeline';
import { Table } from '../generated/entity/data/table';
import {
Column,
ColumnJoins,
JoinedWith,
Table,
} from '../generated/entity/data/table';
import { Topic } from '../generated/entity/data/topic';
import { Edge, EntityLineage } from '../generated/type/entityLineage';
import { EntityReference } from '../generated/type/entityUsage';
import { TagLabel } from '../generated/type/tagLabel';
import { getEntityName, getPartialNameFromTableFQN } from './CommonUtils';
import {
getEntityName,
getPartialNameFromTableFQN,
getTableFQNFromColumnFQN,
} from './CommonUtils';
import {
getDataTypeString,
getTierFromTableTags,
@ -400,3 +412,146 @@ export const getResourceEntityFromEntityType = (entityType: string) => {
return ResourceEntity.ALL;
};
/**
* It searches for a given text in a given table and returns a new table with only the columns that
* contain the given text
* @param {Column[]} table - Column[] - the table to search in
* @param {string} searchText - The text to search for.
* @returns An array of columns that have been searched for a specific string.
*/
export const searchInColumns = (
table: Column[],
searchText: string
): Column[] => {
const searchedValue: Column[] = table.reduce((searchedCols, column) => {
const searchLowerCase = lowerCase(searchText);
const isContainData =
lowerCase(column.name).includes(searchLowerCase) ||
lowerCase(column.description).includes(searchLowerCase) ||
lowerCase(getDataTypeString(column.dataType)).includes(searchLowerCase);
if (isContainData) {
return [...searchedCols, column];
} else if (!isUndefined(column.children)) {
const searchedChildren = searchInColumns(column.children, searchText);
if (searchedChildren.length > 0) {
return [
...searchedCols,
{
...column,
children: searchedChildren,
},
];
}
}
return searchedCols;
}, [] as Column[]);
return searchedValue;
};
/**
* It checks if a column has a join
* @param {string} columnName - The name of the column you want to check if joins are available for.
* @param joins - Array<ColumnJoins>
* @returns A boolean value.
*/
export const checkIfJoinsAvailable = (
columnName: string,
joins: Array<ColumnJoins>
): boolean => {
return (
joins &&
Boolean(joins.length) &&
Boolean(joins.find((join) => join.columnName === columnName))
);
};
/**
* It takes a column name and a list of joins and returns the list of joinedWith for the column name
* @param {string} columnName - The name of the column you want to get the frequently joined with
* columns for.
* @param joins - Array<ColumnJoins>
* @returns An array of joinedWith objects
*/
export const getFrequentlyJoinedWithColumns = (
columnName: string,
joins: Array<ColumnJoins>
): Array<JoinedWith> => {
return joins.find((join) => join.columnName === columnName)?.joinedWith || [];
};
export const getFrequentlyJoinedColumns = (
columnName: string,
joins: Array<ColumnJoins>,
columnLabel: string
) => {
const frequentlyJoinedWithColumns = getFrequentlyJoinedWithColumns(
columnName,
joins
);
return checkIfJoinsAvailable(columnName, joins) ? (
<div className="m-t-sm" data-testid="frequently-joined-columns">
<span className="tw-text-grey-muted m-r-xss">{columnLabel}:</span>
<span>
{frequentlyJoinedWithColumns.slice(0, 3).map((columnJoin, index) => (
<Fragment key={index}>
{index > 0 && <span className="m-r-xss">,</span>}
<Link
className="link-text"
to={getTableDetailsPath(
getTableFQNFromColumnFQN(columnJoin.fullyQualifiedName),
getPartialNameFromTableFQN(columnJoin.fullyQualifiedName, [
FqnPart.Column,
])
)}>
{getPartialNameFromTableFQN(
columnJoin.fullyQualifiedName,
[FqnPart.Database, FqnPart.Table, FqnPart.Column],
FQN_SEPARATOR_CHAR
)}
</Link>
</Fragment>
))}
{frequentlyJoinedWithColumns.length > 3 && (
<PopOver
html={
<div className="text-left">
{frequentlyJoinedWithColumns
?.slice(3)
.map((columnJoin, index) => (
<Fragment key={index}>
<a
className="link-text d-block p-y-xss"
href={getTableDetailsPath(
getTableFQNFromColumnFQN(
columnJoin?.fullyQualifiedName
),
getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName,
[FqnPart.Column]
)
)}>
{getPartialNameFromTableFQN(
columnJoin?.fullyQualifiedName,
[FqnPart.Database, FqnPart.Table, FqnPart.Column]
)}
</a>
</Fragment>
))}
</div>
}
position="bottom"
theme="light"
trigger="click">
<span className="show-more m-l-xss text-underline">...</span>
</PopOver>
)}
</span>
</div>
) : null;
};