From 1df322a704aae868c20fcbd3a95ff85bd4bdab1e Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Tue, 16 May 2023 19:05:22 +0530 Subject: [PATCH] feat(ui): supported separate column for Pipeline entity (#11600) * Supported separate column for Pipline entity * fix code smell and bug * fix code bug * fix code bug --- .../constants/tagsAddRemove.constants.js | 1 + .../DashboardDetails.component.tsx | 4 +- .../DashboardDetails.interface.ts | 5 +- .../EntityTable/EntityTable.component.tsx | 13 +- .../EntityTable/EntityTable.interface.ts | 2 +- .../EntityTable/EntityTable.test.tsx | 2 + .../PipelineDetails.component.tsx | 219 ++++++++++-------- .../PipelineDetails/PipelineDetails.test.tsx | 61 +---- .../SchemaTab/SchemaTab.interfaces.ts | 2 +- .../TableTags/TableTags.component.tsx | 4 +- .../TableTags/TableTags.interface.ts | 7 +- 11 files changed, 159 insertions(+), 161 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js b/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js index f0e0694cf59..bcdab1118ce 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js @@ -47,6 +47,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ serviceName: 'sample_airflow', fieldName: 'dim_address_task', tags: ['PersonalData.Personal', 'PII.Sensitive'], + separate: true, }, // Todo: need to investigate on below test // more details:- https://cloud.cypress.io/projects/a9yxci/runs/18306/test-results/abe5ab43-84c9-49da-b50f-4936bbcfdd3d diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx index c248aa36f1c..8ef9c1c1864 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx @@ -474,7 +474,7 @@ const DashboardDetails = ({ } }; - const handleChartTagSelection = ( + const handleChartTagSelection = async ( selectedTags: Array, editColumnTag: ChartType, otherTags: TagLabel[] @@ -505,7 +505,7 @@ const DashboardDetails = ({ tags: [...(prevTags as TagLabel[]), ...newTags], }; const jsonPatch = compare(editColumnTag, updatedChart); - chartTagUpdateHandler(editColumnTag.id, jsonPatch); + await chartTagUpdateHandler(editColumnTag.id, jsonPatch); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts index 911eb9fd60f..1f0c811a1ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts @@ -59,7 +59,10 @@ export interface DashboardDetailsProps { chartId: string, patch: Array ) => Promise; - chartTagUpdateHandler: (chartId: string, patch: Array) => void; + chartTagUpdateHandler: ( + chartId: string, + patch: Array + ) => Promise; versionHandler: () => void; postFeedHandler: (value: string, id: string) => void; deletePostHandler: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx index 57da64ef6c1..a156e1a4ff3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx @@ -105,11 +105,6 @@ const EntityTable = ({ index: number; }>(); - const [editColumnTag, setEditColumnTag] = useState<{ - column: Column; - index: number; - }>(); - const [isTagLoading, setIsTagLoading] = useState(false); const [tagFetchFailed, setTagFetchFailed] = useState(false); @@ -228,14 +223,14 @@ const EntityTable = ({ editColumn.column.fullyQualifiedName, columnDescription ); - await onUpdate?.(tableCols); + await onUpdate(tableCols); setEditColumn(undefined); } else { setEditColumn(undefined); } }; - const handleTagSelection = ( + const handleTagSelection = async ( selectedTags: EntityTags[], editColumnTag: Column, otherTags: TagLabel[] @@ -247,9 +242,8 @@ const EntityTable = ({ if (newSelectedTags && editColumnTag) { const tableCols = cloneDeep(tableColumns); updateColumnTags(tableCols, editColumnTag.name, newSelectedTags); - onUpdate?.(tableCols); + await onUpdate(tableCols); } - setEditColumnTag(undefined); }; const searchInColumns = (table: Column[], searchText: string): Column[] => { @@ -590,7 +584,6 @@ const EntityTable = ({ entityFieldThreads, entityFqn, tableConstraints, - editColumnTag, isTagLoading, handleUpdate, handleTagSelection, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.interface.ts index a63c6b92103..6e051867e78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.interface.ts @@ -28,7 +28,7 @@ export interface EntityTableProps { entityFqn?: string; entityFieldThreads?: EntityFieldThreads[]; entityFieldTasks?: EntityFieldThreads[]; - onUpdate?: (columns: Column[]) => Promise; + onUpdate: (columns: Column[]) => Promise; onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; onEntityFieldSelect?: (value: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.test.tsx index 64385a94460..0a0ef99f832 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.test.tsx @@ -22,6 +22,7 @@ import EntityTableV1 from './EntityTable.component'; const onEntityFieldSelect = jest.fn(); const onThreadLinkSelect = jest.fn(); +const onUpdate = jest.fn(); const mockTableConstraints = [ { @@ -114,6 +115,7 @@ const mockEntityTableProp = { tableConstraints: mockTableConstraints, onEntityFieldSelect, onThreadLinkSelect, + onUpdate, }; const mockTagList = [ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx index 9a3ff42fb7d..abd84f60214 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx @@ -27,8 +27,10 @@ import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; import { AxiosError } from 'axios'; import { ActivityFilters } from 'components/ActivityFeed/ActivityFeedList/ActivityFeedList.interface'; import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; +import TableTags from 'components/TableTags/TableTags.component'; import { compare, Operation } from 'fast-json-patch'; -import { isEmpty, isUndefined } from 'lodash'; +import { TagSource } from 'generated/type/schema'; +import { isEmpty, isUndefined, map } from 'lodash'; import { EntityTags, ExtraInfo, TagOption } from 'Models'; import React, { RefObject, @@ -41,6 +43,8 @@ import { useTranslation } from 'react-i18next'; import { Link, Redirect, useHistory, useParams } from 'react-router-dom'; import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI'; import { restorePipeline } from 'rest/pipelineAPI'; +import { fetchGlossaryTerms, getGlossaryTermlist } from 'utils/GlossaryUtils'; +import { getFilterTags } from 'utils/TableTags/TableTags.utils'; import AppState from '../../AppState'; import { ReactComponent as ExternalLinkIcon } from '../../assets/svg/external-link.svg'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -87,7 +91,7 @@ import { } from '../../utils/FeedUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { fetchTagsAndGlossaryTerms } from '../../utils/TagsUtils'; +import { getClassifications, getTaglist } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList'; import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; @@ -103,8 +107,6 @@ import Loader from '../Loader/Loader'; import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import { usePermissionProvider } from '../PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface'; -import TagsContainer from '../Tag/TagsContainer/tags-container'; -import TagsViewer from '../Tag/TagsViewer/tags-viewer'; import TasksDAGView from '../TasksDAGView/TasksDAGView'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; @@ -150,10 +152,7 @@ const PipelineDetails = ({ }, [pipelineDetails]); // local state variables - const [editTaskTags, setEditTaskTags] = useState<{ - task: Task; - index: number; - }>(); + const [isEdit, setIsEdit] = useState(false); const [followersCount, setFollowersCount] = useState(0); const [isFollowing, setIsFollowing] = useState(false); @@ -175,8 +174,6 @@ const PipelineDetails = ({ EntityFieldThreadCount[] >([]); - const [tagList, setTagList] = useState(); - const [threadLink, setThreadLink] = useState(''); const [elementRef, isInView] = useElementInView(observerOptions); @@ -195,6 +192,11 @@ const PipelineDetails = ({ const [activityFilter, setActivityFilter] = useState(); + const [isTagLoading, setIsTagLoading] = useState(false); + const [tagFetchFailed, setTagFetchFailed] = useState(false); + const [glossaryTags, setGlossaryTags] = useState([]); + const [classificationTags, setClassificationTags] = useState([]); + // local state ends const USERId = getCurrentUserId(); @@ -213,6 +215,11 @@ const PipelineDetails = ({ [entityThreadLoading] ); + const hasTagEditAccess = useMemo( + () => pipelinePermissions.EditAll || pipelinePermissions.EditTags, + [pipelinePermissions] + ); + const getEntityFeedCount = () => { getFeedCounts( EntityType.PIPELINE, @@ -239,6 +246,41 @@ const PipelineDetails = ({ } }, [pipelineDetails.id, getEntityPermission, setPipelinePermissions]); + const fetchGlossaryTags = async () => { + setIsTagLoading(true); + try { + const res = await fetchGlossaryTerms(); + + const glossaryTerms: TagOption[] = getGlossaryTermlist(res).map( + (tag) => ({ fqn: tag, source: TagSource.Glossary }) + ); + setGlossaryTags(glossaryTerms); + } catch { + setTagFetchFailed(true); + } finally { + setIsTagLoading(false); + } + }; + + const fetchClassificationTags = async () => { + setIsTagLoading(true); + try { + const res = await getClassifications(); + const tagList = await getTaglist(res.data); + + const classificationTag: TagOption[] = map(tagList, (tag) => ({ + fqn: tag, + source: TagSource.Classification, + })); + + setClassificationTags(classificationTag); + } catch { + setTagFetchFailed(true); + } finally { + setIsTagLoading(false); + } + }; + useEffect(() => { if (pipelineDetails.id) { fetchResourcePermission(); @@ -497,105 +539,47 @@ const PipelineDetails = ({ getFeedData(undefined, feedFilter, threadType); }, []); - const handleEditTaskTag = (task: Task, index: number): void => { - setEditTaskTags({ task: { ...task, tags: [] }, index }); - }; - - const handleTableTagSelection = ( - selectedTags: Array = [], - task: { - task: Task; - index: number; - } + const handleTableTagSelection = async ( + selectedTags: EntityTags[], + editColumnTag: Task, + otherTags: TagLabel[] ) => { - const selectedTask = isUndefined(editTask) ? task : editTask; - const prevTags = selectedTask.task.tags?.filter((tag) => - selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN) + const newSelectedTags: TagOption[] = map( + [...selectedTags, ...otherTags], + (tag) => ({ fqn: tag.tagFQN, source: tag.source }) ); - const newTags = selectedTags + const prevTags = editColumnTag.tags?.filter((tag) => + newSelectedTags.some((selectedTag) => selectedTag.fqn === tag.tagFQN) + ); + + const newTags = newSelectedTags .filter( (selectedTag) => - !selectedTask.task.tags?.some( - (tag) => tag.tagFQN === selectedTag.tagFQN - ) + !editColumnTag.tags?.some((tag) => tag.tagFQN === selectedTag.fqn) ) .map((tag) => ({ labelType: 'Manual', state: 'Confirmed', source: tag.source, - tagFQN: tag.tagFQN, + tagFQN: tag.fqn, })); - const updatedTasks: Task[] = [...(pipelineDetails.tasks || [])]; - const updatedTask = { - ...selectedTask.task, + ...editColumnTag, tags: [...(prevTags as TagLabel[]), ...newTags], } as Task; - updatedTasks[selectedTask.index] = updatedTask; + const updatedTasks: Task[] = [...(pipelineDetails.tasks ?? [])].map( + (task) => (task.name === editColumnTag.name ? updatedTask : task) + ); const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks }; const jsonPatch = compare(pipelineDetails, updatedPipeline); - taskUpdateHandler(jsonPatch); - setEditTaskTags(undefined); + await taskUpdateHandler(jsonPatch); }; - useMemo(() => { - fetchTagsAndGlossaryTerms().then((response) => { - setTagList(response); - }); - }, [setTagList]); - - const addButtonHandler = useCallback((record, index) => { - handleEditTaskTag(record, index); - }, []); - - const renderTags = useCallback( - (tags, record, index) => ( -
- {deleted ? ( - - ) : ( - addButtonHandler(record, index)} - onCancel={() => { - setEditTask(undefined); - }} - onEditButtonClick={() => addButtonHandler(record, index)} - onSelectionChange={(tags) => { - handleTableTagSelection(tags, { - task: record, - index: index, - }); - }} - /> - )} -
- ), - [ - tagList, - editTaskTags, - pipelinePermissions.EditAll, - pipelinePermissions.EditTags, - deleted, - ] - ); - const taskColumns: ColumnsType = useMemo( () => [ { @@ -669,14 +653,65 @@ const PipelineDetails = ({ ), }, { - key: t('label.tag-plural'), - dataIndex: 'tags', title: t('label.tag-plural'), - width: 350, - render: renderTags, + dataIndex: 'tags', + key: 'tags', + accessor: 'tags', + width: 300, + render: (tags, record, index) => ( + + dataTestId="classification-tags" + fetchTags={fetchClassificationTags} + handleTagSelection={handleTableTagSelection} + hasTagEditAccess={hasTagEditAccess} + index={index} + isReadOnly={deleted} + isTagLoading={isTagLoading} + record={record} + tagFetchFailed={tagFetchFailed} + tagList={classificationTags} + tags={getFilterTags(tags)} + type={TagSource.Classification} + /> + ), + }, + { + title: t('label.glossary-term-plural'), + dataIndex: 'tags', + key: 'tags', + accessor: 'tags', + width: 300, + render: (tags, record, index) => ( + + dataTestId="glossary-tags" + fetchTags={fetchGlossaryTags} + handleTagSelection={handleTableTagSelection} + hasTagEditAccess={hasTagEditAccess} + index={index} + isReadOnly={deleted} + isTagLoading={isTagLoading} + record={record} + tagFetchFailed={tagFetchFailed} + tagList={glossaryTags} + tags={getFilterTags(tags)} + type={TagSource.Glossary} + /> + ), }, ], - [pipelinePermissions, editTask, editTaskTags, tagList, deleted] + [ + fetchGlossaryTags, + fetchClassificationTags, + handleTableTagSelection, + classificationTags, + hasTagEditAccess, + pipelinePermissions, + editTask, + deleted, + isTagLoading, + tagFetchFailed, + glossaryTags, + ] ); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.test.tsx index eaf11cff986..311ed996a37 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.test.tsx @@ -12,6 +12,7 @@ */ import { + findAllByTestId, findByTestId, findByText, fireEvent, @@ -19,7 +20,6 @@ import { render, screen, } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { act } from 'react-test-renderer'; @@ -81,17 +81,6 @@ const mockTasks = [ }, ]; -const mockTags = [ - { - tagFQN: 'PII.Sensitive', - source: 'Tag', - }, - { - tagFQN: 'PersonalData.Personal', - source: 'Tag', - }, -]; - const mockTaskUpdateHandler = jest.fn(); const PipelineDetailsProps = { @@ -147,14 +136,6 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => { return jest.fn().mockReturnValue(

RichTextEditorPreviwer

); }); -jest.mock('components/Tag/TagsContainer/tags-container', () => { - return jest.fn().mockReturnValue(

Tag Container

); -}); - -jest.mock('components/Tag/Tags/tags', () => { - return jest.fn().mockReturnValue(

Tags

); -}); - jest.mock('../EntityLineage/EntityLineage.component', () => { return jest.fn().mockReturnValue(

EntityLineage

); }); @@ -201,17 +182,12 @@ jest.mock('../../utils/CommonUtils', () => ({ jest.mock('../Execution/Execution.component', () => { return jest.fn().mockImplementation(() =>

Executions

); }); - -jest.mock('../Tag/TagsContainer/tags-container', () => - jest.fn().mockImplementation(({ onSelectionChange }) => ( -
-
onSelectionChange(mockTags)}> - onSelectionChange -
-
- )) +jest.mock('components/TableTags/TableTags.component', () => + jest + .fn() + .mockImplementation(() => ( +
Table Tag Container
+ )) ); describe('Test PipelineDetails component', () => { @@ -235,6 +211,10 @@ describe('Test PipelineDetails component', () => { container, 'label.custom-property-plural' ); + const tagsContainer = await findAllByTestId( + container, + 'table-tag-container' + ); expect(EntityPageInfo).toBeInTheDocument(); expect(description).toBeInTheDocument(); @@ -243,6 +223,7 @@ describe('Test PipelineDetails component', () => { expect(lineageTab).toBeInTheDocument(); expect(executionsTab).toBeInTheDocument(); expect(customPropertiesTab).toBeInTheDocument(); + expect(tagsContainer).toHaveLength(4); }); it('Check if active tab is tasks', async () => { @@ -381,22 +362,4 @@ describe('Test PipelineDetails component', () => { expect(obServerElement).toBeInTheDocument(); }); - - it('taskUpdateHandler should be called after the tags are added or removed to a task', async () => { - render(, { - wrapper: MemoryRouter, - }); - - const tagsContainer = screen.getAllByTestId('tags-container'); - - expect(tagsContainer).toHaveLength(2); - - const onSelectionChange = screen.getAllByTestId('onSelectionChange'); - - expect(onSelectionChange).toHaveLength(2); - - await act(async () => userEvent.click(onSelectionChange[0])); - - expect(mockTaskUpdateHandler).toHaveBeenCalledTimes(1); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.interfaces.ts index beff5fb7695..3254ce78c88 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.interfaces.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.interfaces.ts @@ -33,5 +33,5 @@ export type Props = { entityFieldTasks?: EntityFieldThreads[]; onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; onEntityFieldSelect?: (value: string) => void; - onUpdate?: (columns: Table['columns']) => Promise; + onUpdate: (columns: Table['columns']) => Promise; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.component.tsx index 5f1d8445f12..603f4c58b57 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.component.tsx @@ -140,8 +140,8 @@ const TableTags = ({ type="label" onAddButtonClick={addButtonHandler} onCancel={() => setIsEdit(false)} - onSelectionChange={(selectedTags) => { - handleTagSelection(selectedTags, record, otherTags); + onSelectionChange={async (selectedTags) => { + await handleTagSelection(selectedTags, record, otherTags); setIsEdit(false); }} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts index e86c630a2f9..a3e7310e4f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Task } from 'generated/entity/data/pipeline'; import { Field } from 'generated/entity/data/topic'; import { TagLabel, TagSource } from 'generated/type/tagLabel'; import { EntityTags, TagOption } from 'Models'; @@ -33,7 +34,7 @@ export interface TableTagsComponentProps { selectedTags: Array, editColumnTag: T, otherTags: TagLabel[] - ) => void; + ) => Promise; onRequestTagsHandler?: (cell: T) => void; getColumnName?: (cell: T) => string; getColumnFieldFQN?: string; @@ -42,7 +43,7 @@ export interface TableTagsComponentProps { entityFieldThreads?: EntityFieldThreads[]; tagFetchFailed: boolean; type: TagSource; - fetchTags: () => void; + fetchTags: () => Promise; dataTestId: string; } @@ -56,4 +57,4 @@ export interface TableTagsProps { Glossary: TagLabel[]; } -export type TableUnion = Column | Field | ChartType; +export type TableUnion = Column | Field | Task | ChartType;