mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-28 02:46:09 +00:00
* Supported DataModel in Dashboard Page * url fqn changes * changes as per comments
This commit is contained in:
parent
62b88b0404
commit
4001aa0ccc
@ -56,6 +56,6 @@
|
|||||||
"default": null
|
"default": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "service", "modelType", "columns"],
|
"required": ["name", "service", "dataModelType", "columns"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"modelType",
|
"dataModelType",
|
||||||
"columns"
|
"columns"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { Table } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
|
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
|
||||||
|
import Loader from 'components/Loader/Loader';
|
||||||
|
import { getDataModelDetailsPath } from 'constants/constants';
|
||||||
|
import { CONNECTORS_DOCS } from 'constants/docs.constants';
|
||||||
|
import { servicesDisplayName } from 'constants/Services.constant';
|
||||||
|
import { isEmpty, isUndefined } from 'lodash';
|
||||||
|
import { DataModelTableProps } from 'pages/DataModelPage/DataModelsInterface';
|
||||||
|
import { ServicePageData } from 'pages/service';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { getEntityName } from 'utils/EntityUtils';
|
||||||
|
|
||||||
|
const DataModelTable = ({ data, isLoading }: DataModelTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const tableColumn: ColumnsType<ServicePageData> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: t('label.name'),
|
||||||
|
dataIndex: 'displayName',
|
||||||
|
key: 'displayName',
|
||||||
|
render: (_, record: ServicePageData) => {
|
||||||
|
return (
|
||||||
|
<Link to={getDataModelDetailsPath(record.fullyQualifiedName || '')}>
|
||||||
|
{getEntityName(record)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('label.description'),
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
render: (description: ServicePageData['description']) =>
|
||||||
|
!isUndefined(description) && description.trim() ? (
|
||||||
|
<RichTextEditorPreviewer markdown={description} />
|
||||||
|
) : (
|
||||||
|
<span className="text-grey-muted">
|
||||||
|
{t('label.no-entity', {
|
||||||
|
entity: t('label.description'),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return isEmpty(data) ? (
|
||||||
|
<ErrorPlaceHolder
|
||||||
|
doc={CONNECTORS_DOCS}
|
||||||
|
heading={servicesDisplayName.dashboardDataModel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div data-testid="table-container">
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
className="mt-4 table-shadow"
|
||||||
|
columns={tableColumn}
|
||||||
|
data-testid="data-models-table"
|
||||||
|
dataSource={data}
|
||||||
|
loading={{
|
||||||
|
spinning: isLoading,
|
||||||
|
indicator: <Loader size="small" />,
|
||||||
|
}}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataModelTable;
|
@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { Button, Space, Table, Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
|
||||||
|
import { CellRendered } from 'components/ContainerDetail/ContainerDataModel/ContainerDataModel.interface';
|
||||||
|
import { ModalWithMarkdownEditor } from 'components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||||
|
import TagsContainer from 'components/Tag/TagsContainer/tags-container';
|
||||||
|
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
||||||
|
import { Column } from 'generated/entity/data/dashboardDataModel';
|
||||||
|
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
||||||
|
import { EntityTags, TagOption } from 'Models';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
updateDataModelColumnDescription,
|
||||||
|
updateDataModelColumnTags,
|
||||||
|
} from 'utils/DataModelsUtils';
|
||||||
|
import { fetchTagsAndGlossaryTerms } from 'utils/TagsUtils';
|
||||||
|
import { ReactComponent as EditIcon } from '../../../assets/svg/ic-edit.svg';
|
||||||
|
import { ModelTabProps } from './ModelTab.interface';
|
||||||
|
|
||||||
|
const ModelTab = ({
|
||||||
|
data,
|
||||||
|
isReadOnly,
|
||||||
|
hasEditDescriptionPermission,
|
||||||
|
hasEditTagsPermission,
|
||||||
|
onUpdate,
|
||||||
|
}: ModelTabProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [editColumnDescription, setEditColumnDescription] = useState<Column>();
|
||||||
|
const [editContainerColumnTags, setEditContainerColumnTags] =
|
||||||
|
useState<Column>();
|
||||||
|
|
||||||
|
const [tagList, setTagList] = useState<TagOption[]>([]);
|
||||||
|
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
|
||||||
|
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fetchTags = useCallback(async () => {
|
||||||
|
setIsTagLoading(true);
|
||||||
|
try {
|
||||||
|
const tagsAndTerms = await fetchTagsAndGlossaryTerms();
|
||||||
|
setTagList(tagsAndTerms);
|
||||||
|
} catch (error) {
|
||||||
|
setTagList([]);
|
||||||
|
setTagFetchFailed(true);
|
||||||
|
} finally {
|
||||||
|
setIsTagLoading(false);
|
||||||
|
}
|
||||||
|
}, [fetchTagsAndGlossaryTerms]);
|
||||||
|
|
||||||
|
const handleFieldTagsChange = useCallback(
|
||||||
|
async (selectedTags: EntityTags[] = [], selectedColumn: Column) => {
|
||||||
|
const newSelectedTags: TagOption[] = selectedTags.map((tag) => ({
|
||||||
|
fqn: tag.tagFQN,
|
||||||
|
source: tag.source,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dataModelData = cloneDeep(data);
|
||||||
|
|
||||||
|
updateDataModelColumnTags(
|
||||||
|
dataModelData,
|
||||||
|
editContainerColumnTags?.name ?? selectedColumn.name,
|
||||||
|
newSelectedTags
|
||||||
|
);
|
||||||
|
|
||||||
|
await onUpdate(dataModelData);
|
||||||
|
|
||||||
|
setEditContainerColumnTags(undefined);
|
||||||
|
},
|
||||||
|
[data, updateDataModelColumnTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnDescriptionChange = useCallback(
|
||||||
|
async (updatedDescription: string) => {
|
||||||
|
if (!isUndefined(editColumnDescription)) {
|
||||||
|
const dataModelColumns = cloneDeep(data);
|
||||||
|
updateDataModelColumnDescription(
|
||||||
|
dataModelColumns,
|
||||||
|
editColumnDescription?.name,
|
||||||
|
updatedDescription
|
||||||
|
);
|
||||||
|
await onUpdate(dataModelColumns);
|
||||||
|
}
|
||||||
|
setEditColumnDescription(undefined);
|
||||||
|
},
|
||||||
|
[editColumnDescription, data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddTagClick = useCallback(
|
||||||
|
(record: Column) => {
|
||||||
|
if (isUndefined(editContainerColumnTags)) {
|
||||||
|
setEditContainerColumnTags(record);
|
||||||
|
// Fetch tags and terms only once
|
||||||
|
if (tagList.length === 0 || tagFetchFailed) {
|
||||||
|
fetchTags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editContainerColumnTags, tagList, tagFetchFailed]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderColumnDescription: CellRendered<Column, 'description'> =
|
||||||
|
useCallback(
|
||||||
|
(description, record, index) => {
|
||||||
|
return (
|
||||||
|
<Space
|
||||||
|
className="custom-group w-full"
|
||||||
|
data-testid="description"
|
||||||
|
id={`field-description-${index}`}
|
||||||
|
size={4}>
|
||||||
|
<>
|
||||||
|
{description ? (
|
||||||
|
<RichTextEditorPreviewer markdown={description} />
|
||||||
|
) : (
|
||||||
|
<Typography.Text className="tw-no-description">
|
||||||
|
{t('label.no-entity', {
|
||||||
|
entity: t('label.description'),
|
||||||
|
})}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
{isReadOnly && !hasEditDescriptionPermission ? null : (
|
||||||
|
<Button
|
||||||
|
className="p-0 opacity-0 group-hover-opacity-100"
|
||||||
|
data-testid="edit-button"
|
||||||
|
icon={<EditIcon width="16px" />}
|
||||||
|
type="text"
|
||||||
|
onClick={() => setEditColumnDescription(record)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isReadOnly, hasEditDescriptionPermission]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderColumnTags: CellRendered<Column, 'tags'> = useCallback(
|
||||||
|
(tags, record: Column) => {
|
||||||
|
const isSelectedField = editContainerColumnTags?.name === record.name;
|
||||||
|
const isUpdatingTags = isSelectedField || !isEmpty(tags);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isReadOnly ? (
|
||||||
|
<TagsViewer sizeCap={-1} tags={tags || []} />
|
||||||
|
) : (
|
||||||
|
<Space
|
||||||
|
align={isUpdatingTags ? 'start' : 'center'}
|
||||||
|
className="justify-between"
|
||||||
|
data-testid="tags-wrapper"
|
||||||
|
direction={isUpdatingTags ? 'vertical' : 'horizontal'}
|
||||||
|
onClick={() => handleAddTagClick(record)}>
|
||||||
|
<TagsContainer
|
||||||
|
editable={isSelectedField}
|
||||||
|
isLoading={isTagLoading && isSelectedField}
|
||||||
|
selectedTags={tags || []}
|
||||||
|
showAddTagButton={hasEditTagsPermission}
|
||||||
|
size="small"
|
||||||
|
tagList={tagList}
|
||||||
|
type="label"
|
||||||
|
onCancel={() => setEditContainerColumnTags(undefined)}
|
||||||
|
onSelectionChange={(tags) =>
|
||||||
|
handleFieldTagsChange(tags, record)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
editContainerColumnTags,
|
||||||
|
isReadOnly,
|
||||||
|
isTagLoading,
|
||||||
|
tagList,
|
||||||
|
hasEditTagsPermission,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableColumn: ColumnsType<Column> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: t('label.name'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('label.type'),
|
||||||
|
dataIndex: 'dataTypeDisplay',
|
||||||
|
key: 'dataTypeDisplay',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('label.description'),
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
accessor: 'description',
|
||||||
|
render: renderColumnDescription,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('label.tag-plural'),
|
||||||
|
dataIndex: 'tags',
|
||||||
|
key: 'tags',
|
||||||
|
accessor: 'tags',
|
||||||
|
width: 350,
|
||||||
|
render: renderColumnTags,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
editColumnDescription,
|
||||||
|
hasEditDescriptionPermission,
|
||||||
|
hasEditTagsPermission,
|
||||||
|
editContainerColumnTags,
|
||||||
|
isReadOnly,
|
||||||
|
isTagLoading,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
className="p-t-xs"
|
||||||
|
columns={tableColumn}
|
||||||
|
data-testid="charts-table"
|
||||||
|
dataSource={data}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="name"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{editColumnDescription && (
|
||||||
|
<ModalWithMarkdownEditor
|
||||||
|
header={`${t('label.edit-entity', {
|
||||||
|
entity: t('label.column'),
|
||||||
|
})}: "${editColumnDescription.name}"`}
|
||||||
|
placeholder={t('label.enter-field-description', {
|
||||||
|
field: t('label.column'),
|
||||||
|
})}
|
||||||
|
value={editColumnDescription.description || ''}
|
||||||
|
visible={Boolean(editColumnDescription)}
|
||||||
|
onCancel={() => setEditColumnDescription(undefined)}
|
||||||
|
onSave={handleColumnDescriptionChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelTab;
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { Column } from 'generated/entity/data/dashboardDataModel';
|
||||||
|
|
||||||
|
export interface ModelTabProps {
|
||||||
|
data: Column[];
|
||||||
|
isReadOnly: boolean;
|
||||||
|
hasEditTagsPermission: boolean;
|
||||||
|
hasEditDescriptionPermission: boolean;
|
||||||
|
onUpdate: (updatedDataModel: Column[]) => Promise<void>;
|
||||||
|
}
|
@ -126,6 +126,11 @@ const DatabaseSchemaPageComponent = withSuspenseFallback(
|
|||||||
() => import('pages/DatabaseSchemaPage/DatabaseSchemaPage.component')
|
() => import('pages/DatabaseSchemaPage/DatabaseSchemaPage.component')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const DataModelDetailsPage = withSuspenseFallback(
|
||||||
|
React.lazy(() => import('pages/DataModelPage/DataModelPage.component'))
|
||||||
|
);
|
||||||
|
|
||||||
const DatasetDetailsPage = withSuspenseFallback(
|
const DatasetDetailsPage = withSuspenseFallback(
|
||||||
React.lazy(
|
React.lazy(
|
||||||
() => import('pages/DatasetDetailsPage/DatasetDetailsPage.component')
|
() => import('pages/DatasetDetailsPage/DatasetDetailsPage.component')
|
||||||
@ -350,6 +355,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
component={DashboardDetailsPage}
|
component={DashboardDetailsPage}
|
||||||
path={ROUTES.DASHBOARD_DETAILS_WITH_TAB}
|
path={ROUTES.DASHBOARD_DETAILS_WITH_TAB}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
component={DataModelDetailsPage}
|
||||||
|
path={ROUTES.DATA_MODEL_DETAILS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
component={DataModelDetailsPage}
|
||||||
|
path={ROUTES.DATA_MODEL_DETAILS_WITH_TAB}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
component={PipelineDetailsPage}
|
component={PipelineDetailsPage}
|
||||||
|
@ -245,6 +245,9 @@ export const servicesDisplayName: { [key: string]: string } = {
|
|||||||
objectStoreServices: i18n.t('label.entity-service', {
|
objectStoreServices: i18n.t('label.entity-service', {
|
||||||
entity: i18n.t('label.object-store'),
|
entity: i18n.t('label.object-store'),
|
||||||
}),
|
}),
|
||||||
|
dashboardDataModel: i18n.t('label.entity-service', {
|
||||||
|
entity: i18n.t('label.data-model'),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEF_UI_SCHEMA = {
|
export const DEF_UI_SCHEMA = {
|
||||||
|
@ -124,6 +124,7 @@ export const INGESTION_NAME = ':ingestionName';
|
|||||||
export const LOG_ENTITY_NAME = ':logEntityName';
|
export const LOG_ENTITY_NAME = ':logEntityName';
|
||||||
export const KPI_NAME = ':kpiName';
|
export const KPI_NAME = ':kpiName';
|
||||||
export const PLACEHOLDER_ACTION = ':action';
|
export const PLACEHOLDER_ACTION = ':action';
|
||||||
|
export const PLACEHOLDER_ROUTE_DATA_MODEL_FQN = ':dashboardDataModelFQN';
|
||||||
|
|
||||||
export const pagingObject = { after: '', before: '', total: 0 };
|
export const pagingObject = { after: '', before: '', total: 0 };
|
||||||
|
|
||||||
@ -212,6 +213,8 @@ export const ROUTES = {
|
|||||||
TOPIC_DETAILS_WITH_TAB: `/topic/${PLACEHOLDER_ROUTE_TOPIC_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
TOPIC_DETAILS_WITH_TAB: `/topic/${PLACEHOLDER_ROUTE_TOPIC_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
DASHBOARD_DETAILS: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}`,
|
DASHBOARD_DETAILS: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}`,
|
||||||
DASHBOARD_DETAILS_WITH_TAB: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
DASHBOARD_DETAILS_WITH_TAB: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
|
DATA_MODEL_DETAILS: `/dashboardDataModel/${PLACEHOLDER_ROUTE_DATA_MODEL_FQN}`,
|
||||||
|
DATA_MODEL_DETAILS_WITH_TAB: `/dashboardDataModel/${PLACEHOLDER_ROUTE_DATA_MODEL_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`,
|
DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`,
|
||||||
SCHEMA_DETAILS: `/databaseSchema/${PLACEHOLDER_ROUTE_DATABASE_SCHEMA_FQN}`,
|
SCHEMA_DETAILS: `/databaseSchema/${PLACEHOLDER_ROUTE_DATABASE_SCHEMA_FQN}`,
|
||||||
DATABASE_DETAILS_WITH_TAB: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
DATABASE_DETAILS_WITH_TAB: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
@ -438,6 +441,19 @@ export const getDashboardDetailsPath = (dashboardFQN: string, tab?: string) => {
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataModelDetailsPath = (dataModelFQN: string, tab?: string) => {
|
||||||
|
let path = tab
|
||||||
|
? ROUTES.DATA_MODEL_DETAILS_WITH_TAB
|
||||||
|
: ROUTES.DATA_MODEL_DETAILS;
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_DATA_MODEL_FQN, dataModelFQN);
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_TAB, tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPipelineDetailsPath = (pipelineFQN: string, tab?: string) => {
|
export const getPipelineDetailsPath = (pipelineFQN: string, tab?: string) => {
|
||||||
let path = tab ? ROUTES.PIPELINE_DETAILS_WITH_TAB : ROUTES.PIPELINE_DETAILS;
|
let path = tab ? ROUTES.PIPELINE_DETAILS_WITH_TAB : ROUTES.PIPELINE_DETAILS;
|
||||||
path = path.replace(PLACEHOLDER_ROUTE_PIPELINE_FQN, pipelineFQN);
|
path = path.replace(PLACEHOLDER_ROUTE_PIPELINE_FQN, pipelineFQN);
|
||||||
|
@ -42,6 +42,7 @@ export enum EntityType {
|
|||||||
ALERT = 'alert',
|
ALERT = 'alert',
|
||||||
CONTAINER = 'container',
|
CONTAINER = 'container',
|
||||||
TAG = 'tag',
|
TAG = 'tag',
|
||||||
|
DASHBOARD_DATA_MODEL = 'dashboardDataModel',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AssetsType {
|
export enum AssetsType {
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "Total Data Assets by Tier",
|
"data-insight-tier-summary": "Total Data Assets by Tier",
|
||||||
"data-insight-top-viewed-entity-summary": "Most Viewed Data Assets",
|
"data-insight-top-viewed-entity-summary": "Most Viewed Data Assets",
|
||||||
"data-insight-total-entity-summary": "Total Data Assets",
|
"data-insight-total-entity-summary": "Total Data Assets",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "Data Quality Test",
|
"data-quality-test": "Data Quality Test",
|
||||||
"data-type": "Data Type",
|
"data-type": "Data Type",
|
||||||
"database": "Database",
|
"database": "Database",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "ML models",
|
"ml-model-lowercase-plural": "ML models",
|
||||||
"ml-model-plural": "ML Models",
|
"ml-model-plural": "ML Models",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "Model Name",
|
"model-name": "Model Name",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "Model Store",
|
"model-store": "Model Store",
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "Total de activos de datos por nivel",
|
"data-insight-tier-summary": "Total de activos de datos por nivel",
|
||||||
"data-insight-top-viewed-entity-summary": "Activos de datos más vistos",
|
"data-insight-top-viewed-entity-summary": "Activos de datos más vistos",
|
||||||
"data-insight-total-entity-summary": "Total de activos de datos",
|
"data-insight-total-entity-summary": "Total de activos de datos",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "Prueba de calidad de datos",
|
"data-quality-test": "Prueba de calidad de datos",
|
||||||
"data-type": "Tipo de datos",
|
"data-type": "Tipo de datos",
|
||||||
"database": "Base de datos",
|
"database": "Base de datos",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "modelos de ML",
|
"ml-model-lowercase-plural": "modelos de ML",
|
||||||
"ml-model-plural": "Modelos de ML",
|
"ml-model-plural": "Modelos de ML",
|
||||||
"mode": "Moda",
|
"mode": "Moda",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "Nombre del Modelo",
|
"model-name": "Nombre del Modelo",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "Almacenamiento de Modelos",
|
"model-store": "Almacenamiento de Modelos",
|
||||||
"monday": "Lunes",
|
"monday": "Lunes",
|
||||||
"month": "Mes",
|
"month": "Mes",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "Total des Resources de Données par Rang",
|
"data-insight-tier-summary": "Total des Resources de Données par Rang",
|
||||||
"data-insight-top-viewed-entity-summary": "Resources de Données les plus Visitées",
|
"data-insight-top-viewed-entity-summary": "Resources de Données les plus Visitées",
|
||||||
"data-insight-total-entity-summary": "Total Resources de Données",
|
"data-insight-total-entity-summary": "Total Resources de Données",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "Data Quality Test",
|
"data-quality-test": "Data Quality Test",
|
||||||
"data-type": "Type de donnée",
|
"data-type": "Type de donnée",
|
||||||
"database": "Base de Données",
|
"database": "Base de Données",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "ML models",
|
"ml-model-lowercase-plural": "ML models",
|
||||||
"ml-model-plural": "Modéles d'IA",
|
"ml-model-plural": "Modéles d'IA",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "Nom du Modèle",
|
"model-name": "Nom du Modèle",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "Magasin de Modèles",
|
"model-store": "Magasin de Modèles",
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "ティアごとの全データアセット",
|
"data-insight-tier-summary": "ティアごとの全データアセット",
|
||||||
"data-insight-top-viewed-entity-summary": "最も閲覧されたデータアセット",
|
"data-insight-top-viewed-entity-summary": "最も閲覧されたデータアセット",
|
||||||
"data-insight-total-entity-summary": "全てのデータアセット",
|
"data-insight-total-entity-summary": "全てのデータアセット",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "データ品質テスト",
|
"data-quality-test": "データ品質テスト",
|
||||||
"data-type": "データ型",
|
"data-type": "データ型",
|
||||||
"database": "データベース",
|
"database": "データベース",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "ML models",
|
"ml-model-lowercase-plural": "ML models",
|
||||||
"ml-model-plural": "MLモデル",
|
"ml-model-plural": "MLモデル",
|
||||||
"mode": "モード",
|
"mode": "モード",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "モデル名",
|
"model-name": "モデル名",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "モデルストア",
|
"model-store": "モデルストア",
|
||||||
"monday": "月曜日",
|
"monday": "月曜日",
|
||||||
"month": "月",
|
"month": "月",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "Recursos de dados totais por nível",
|
"data-insight-tier-summary": "Recursos de dados totais por nível",
|
||||||
"data-insight-top-viewed-entity-summary": "Recursos de dados mais visualizados",
|
"data-insight-top-viewed-entity-summary": "Recursos de dados mais visualizados",
|
||||||
"data-insight-total-entity-summary": "Total de recursos de dados",
|
"data-insight-total-entity-summary": "Total de recursos de dados",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "Teste de qualidade de dados",
|
"data-quality-test": "Teste de qualidade de dados",
|
||||||
"data-type": "Tipo de dado",
|
"data-type": "Tipo de dado",
|
||||||
"database": "Banco de dados",
|
"database": "Banco de dados",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "ML models",
|
"ml-model-lowercase-plural": "ML models",
|
||||||
"ml-model-plural": "Modelos de ML",
|
"ml-model-plural": "Modelos de ML",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "Nome do modelo",
|
"model-name": "Nome do modelo",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "Estoque modelo",
|
"model-store": "Estoque modelo",
|
||||||
"monday": "Segunda-feira",
|
"monday": "Segunda-feira",
|
||||||
"month": "Mês",
|
"month": "Mês",
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"data-insight-tier-summary": "分层的总数据资产",
|
"data-insight-tier-summary": "分层的总数据资产",
|
||||||
"data-insight-top-viewed-entity-summary": "查看次数最多的数据资产",
|
"data-insight-top-viewed-entity-summary": "查看次数最多的数据资产",
|
||||||
"data-insight-total-entity-summary": "所有数据资产",
|
"data-insight-total-entity-summary": "所有数据资产",
|
||||||
|
"data-model": "Data Model",
|
||||||
"data-quality-test": "Data Quality Test",
|
"data-quality-test": "Data Quality Test",
|
||||||
"data-type": "Data Type",
|
"data-type": "Data Type",
|
||||||
"database": "数据库",
|
"database": "数据库",
|
||||||
@ -470,7 +471,9 @@
|
|||||||
"ml-model-lowercase-plural": "ML models",
|
"ml-model-lowercase-plural": "ML models",
|
||||||
"ml-model-plural": "机器学习模型",
|
"ml-model-plural": "机器学习模型",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"model": "Model",
|
||||||
"model-name": "模型名",
|
"model-name": "模型名",
|
||||||
|
"model-plural": "Models",
|
||||||
"model-store": "模型存储",
|
"model-store": "模型存储",
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
|
@ -0,0 +1,464 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { Card, Space, Tabs } from 'antd';
|
||||||
|
import AppState from 'AppState';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import Description from 'components/common/description/Description';
|
||||||
|
import EntityPageInfo from 'components/common/entityPageInfo/EntityPageInfo';
|
||||||
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
|
import PageContainerV1 from 'components/containers/PageContainerV1';
|
||||||
|
import ModelTab from 'components/DataModels/ModelTab/ModelTab.component';
|
||||||
|
import Loader from 'components/Loader/Loader';
|
||||||
|
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
|
||||||
|
import {
|
||||||
|
OperationPermission,
|
||||||
|
ResourceEntity,
|
||||||
|
} from 'components/PermissionProvider/PermissionProvider.interface';
|
||||||
|
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
|
||||||
|
import { getServiceDetailsPath } from 'constants/constants';
|
||||||
|
import { ENTITY_CARD_CLASS } from 'constants/entity.constants';
|
||||||
|
import { NO_PERMISSION_TO_VIEW } from 'constants/HelperTextUtil';
|
||||||
|
import { EntityInfo, EntityType } from 'enums/entity.enum';
|
||||||
|
import { ServiceCategory } from 'enums/service.enum';
|
||||||
|
import { OwnerType } from 'enums/user.enum';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
|
||||||
|
import { LabelType, State, TagSource } from 'generated/type/tagLabel';
|
||||||
|
import { isUndefined, omitBy } from 'lodash';
|
||||||
|
import { EntityTags, ExtraInfo } from 'Models';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
addDataModelFollower,
|
||||||
|
getDataModelsByName,
|
||||||
|
patchDataModelDetails,
|
||||||
|
removeDataModelFollower,
|
||||||
|
} from 'rest/dataModelsAPI';
|
||||||
|
import {
|
||||||
|
getCurrentUserId,
|
||||||
|
getEntityMissingError,
|
||||||
|
getEntityPlaceHolder,
|
||||||
|
getOwnerValue,
|
||||||
|
} from 'utils/CommonUtils';
|
||||||
|
import { getDataModelsDetailPath } from 'utils/DataModelsUtils';
|
||||||
|
import { getEntityName } from 'utils/EntityUtils';
|
||||||
|
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
|
||||||
|
import { serviceTypeLogo } from 'utils/ServiceUtils';
|
||||||
|
import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils';
|
||||||
|
import { showErrorToast } from 'utils/ToastUtils';
|
||||||
|
import { DATA_MODELS_DETAILS_TABS } from './DataModelsInterface';
|
||||||
|
|
||||||
|
const DataModelsPage = () => {
|
||||||
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { getEntityPermissionByFqn } = usePermissionProvider();
|
||||||
|
const { dashboardDataModelFQN, tab } = useParams() as Record<string, string>;
|
||||||
|
|
||||||
|
const [isEditDescription, setIsEditDescription] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [hasError, setHasError] = useState<boolean>(false);
|
||||||
|
const [dataModelPermissions, setDataModelPermissions] =
|
||||||
|
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
|
||||||
|
const [dataModelData, setDataModelData] = useState<DashboardDataModel>();
|
||||||
|
|
||||||
|
// get current user details
|
||||||
|
const currentUser = useMemo(
|
||||||
|
() => AppState.getCurrentUserDetails(),
|
||||||
|
[AppState.userDetails, AppState.nonSecureUserDetails]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasViewPermission,
|
||||||
|
hasEditDescriptionPermission,
|
||||||
|
hasEditOwnerPermission,
|
||||||
|
hasEditTagsPermission,
|
||||||
|
hasEditTierPermission,
|
||||||
|
} = useMemo(() => {
|
||||||
|
return {
|
||||||
|
hasViewPermission:
|
||||||
|
dataModelPermissions.ViewAll || dataModelPermissions.ViewBasic,
|
||||||
|
hasEditDescriptionPermission:
|
||||||
|
dataModelPermissions.EditAll || dataModelPermissions.EditDescription,
|
||||||
|
hasEditOwnerPermission:
|
||||||
|
dataModelPermissions.EditAll || dataModelPermissions.EditOwner,
|
||||||
|
hasEditTagsPermission:
|
||||||
|
dataModelPermissions.EditAll || dataModelPermissions.EditTags,
|
||||||
|
hasEditTierPermission:
|
||||||
|
dataModelPermissions.EditAll || dataModelPermissions.EditTier,
|
||||||
|
hasEditLineagePermission:
|
||||||
|
dataModelPermissions.EditAll || dataModelPermissions.EditLineage,
|
||||||
|
};
|
||||||
|
}, [dataModelPermissions]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
tier,
|
||||||
|
deleted,
|
||||||
|
owner,
|
||||||
|
description,
|
||||||
|
version,
|
||||||
|
tags,
|
||||||
|
entityName,
|
||||||
|
entityId,
|
||||||
|
followers,
|
||||||
|
isUserFollowing,
|
||||||
|
} = useMemo(() => {
|
||||||
|
return {
|
||||||
|
deleted: dataModelData?.deleted,
|
||||||
|
owner: dataModelData?.owner,
|
||||||
|
description: dataModelData?.description,
|
||||||
|
version: dataModelData?.version,
|
||||||
|
tier: getTierTags(dataModelData?.tags ?? []),
|
||||||
|
tags: getTagsWithoutTier(dataModelData?.tags ?? []),
|
||||||
|
entityId: dataModelData?.id,
|
||||||
|
entityName: getEntityName(dataModelData),
|
||||||
|
isUserFollowing: dataModelData?.followers?.some(
|
||||||
|
({ id }: { id: string }) => id === getCurrentUserId()
|
||||||
|
),
|
||||||
|
followers: dataModelData?.followers ?? [],
|
||||||
|
};
|
||||||
|
}, [dataModelData]);
|
||||||
|
|
||||||
|
const breadcrumbTitles = useMemo(() => {
|
||||||
|
const serviceType = dataModelData?.serviceType;
|
||||||
|
const service = dataModelData?.service;
|
||||||
|
const serviceName = service?.name;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: serviceName || '',
|
||||||
|
url: serviceName
|
||||||
|
? getServiceDetailsPath(
|
||||||
|
serviceName,
|
||||||
|
ServiceCategory.DASHBOARD_SERVICES
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: entityName,
|
||||||
|
url: '',
|
||||||
|
activeTitle: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [dataModelData, dashboardDataModelFQN, entityName]);
|
||||||
|
|
||||||
|
const fetchResourcePermission = async (dashboardDataModelFQN: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const entityPermission = await getEntityPermissionByFqn(
|
||||||
|
ResourceEntity.CONTAINER,
|
||||||
|
dashboardDataModelFQN
|
||||||
|
);
|
||||||
|
setDataModelPermissions(entityPermission);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
t('server.fetch-entity-permissions-error', {
|
||||||
|
entity: t('label.asset-lowercase'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateDataModelData = (updatedData: DashboardDataModel) => {
|
||||||
|
const jsonPatch = compare(omitBy(dataModelData, isUndefined), updatedData);
|
||||||
|
|
||||||
|
return patchDataModelDetails(dataModelData?.id ?? '', jsonPatch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (tabValue: string) => {
|
||||||
|
if (tabValue !== tab) {
|
||||||
|
history.push({
|
||||||
|
pathname: getDataModelsDetailPath(dashboardDataModelFQN, tabValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateDescription = async (updatedDescription: string) => {
|
||||||
|
try {
|
||||||
|
const { description: newDescription, version } =
|
||||||
|
await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
description: updatedDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
description: newDescription,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataModelDetails = async (dashboardDataModelFQN: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getDataModelsByName(
|
||||||
|
dashboardDataModelFQN,
|
||||||
|
'owner,tags,followers'
|
||||||
|
);
|
||||||
|
setDataModelData(response);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
setHasError(true);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFollowDataModel = async () => {
|
||||||
|
const followerId = currentUser?.id ?? '';
|
||||||
|
const dataModelId = dataModelData?.id ?? '';
|
||||||
|
try {
|
||||||
|
if (isUserFollowing) {
|
||||||
|
const response = await removeDataModelFollower(dataModelId, followerId);
|
||||||
|
const { oldValue } = response.changeDescription.fieldsDeleted[0];
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
followers: (dataModelData?.followers || []).filter(
|
||||||
|
(follower) => follower.id !== oldValue[0].id
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const response = await addDataModelFollower(dataModelId, followerId);
|
||||||
|
const { newValue } = response.changeDescription.fieldsAdded[0];
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
followers: [...(dataModelData?.followers ?? []), ...newValue],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extraInfo: Array<ExtraInfo> = [
|
||||||
|
{
|
||||||
|
key: EntityInfo.OWNER,
|
||||||
|
value: owner && getOwnerValue(owner),
|
||||||
|
placeholderText: getEntityPlaceHolder(
|
||||||
|
getEntityName(owner),
|
||||||
|
owner?.deleted
|
||||||
|
),
|
||||||
|
isLink: true,
|
||||||
|
openInNewTab: false,
|
||||||
|
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EntityInfo.TIER,
|
||||||
|
value: tier?.tagFQN ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleRemoveTier = async () => {
|
||||||
|
try {
|
||||||
|
const { tags: newTags, version } = await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
tags: getTagsWithoutTier(dataModelData?.tags ?? []),
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
tags: newTags,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateTags = async (selectedTags: Array<EntityTags> = []) => {
|
||||||
|
try {
|
||||||
|
const { tags: newTags, version } = await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
tags: [...(tier ? [tier] : []), ...selectedTags],
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
tags: newTags,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateOwner = useCallback(
|
||||||
|
async (updatedOwner?: DashboardDataModel['owner']) => {
|
||||||
|
try {
|
||||||
|
const { owner: newOwner, version } = await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
owner: updatedOwner ? updatedOwner : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
owner: newOwner,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dataModelData, dataModelData?.owner]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateTier = async (updatedTier?: string) => {
|
||||||
|
try {
|
||||||
|
if (updatedTier) {
|
||||||
|
const { tags: newTags, version } = await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
tags: [
|
||||||
|
...getTagsWithoutTier(dataModelData?.tags ?? []),
|
||||||
|
{
|
||||||
|
tagFQN: updatedTier,
|
||||||
|
labelType: LabelType.Manual,
|
||||||
|
state: State.Confirmed,
|
||||||
|
source: TagSource.Classification,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
tags: newTags,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateDataModel = async (
|
||||||
|
updatedDataModel: DashboardDataModel['columns']
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { columns: newColumns, version } = await handleUpdateDataModelData({
|
||||||
|
...(dataModelData as DashboardDataModel),
|
||||||
|
columns: updatedDataModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDataModelData((prev) => ({
|
||||||
|
...(prev as DashboardDataModel),
|
||||||
|
columns: newColumns,
|
||||||
|
version,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasViewPermission) {
|
||||||
|
fetchDataModelDetails(dashboardDataModelFQN);
|
||||||
|
}
|
||||||
|
}, [dashboardDataModelFQN, dataModelPermissions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchResourcePermission(dashboardDataModelFQN);
|
||||||
|
}, [dashboardDataModelFQN]);
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return (
|
||||||
|
<ErrorPlaceHolder>
|
||||||
|
{getEntityMissingError(t('label.data-model'), dashboardDataModelFQN)}
|
||||||
|
</ErrorPlaceHolder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasViewPermission && !isLoading) {
|
||||||
|
return <ErrorPlaceHolder>{NO_PERMISSION_TO_VIEW}</ErrorPlaceHolder>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainerV1>
|
||||||
|
<div className="entity-details-container">
|
||||||
|
<EntityPageInfo
|
||||||
|
canDelete={dataModelPermissions.Delete}
|
||||||
|
currentOwner={owner}
|
||||||
|
deleted={deleted}
|
||||||
|
entityFqn={dashboardDataModelFQN}
|
||||||
|
entityId={entityId}
|
||||||
|
entityName={entityName || ''}
|
||||||
|
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
||||||
|
extraInfo={extraInfo}
|
||||||
|
followHandler={handleFollowDataModel}
|
||||||
|
followers={followers.length}
|
||||||
|
followersList={followers}
|
||||||
|
isFollowing={isUserFollowing}
|
||||||
|
isTagEditable={hasEditTagsPermission}
|
||||||
|
removeTier={hasEditTierPermission ? handleRemoveTier : undefined}
|
||||||
|
tags={tags}
|
||||||
|
tagsHandler={handleUpdateTags}
|
||||||
|
tier={tier}
|
||||||
|
titleLinks={breadcrumbTitles}
|
||||||
|
updateOwner={hasEditOwnerPermission ? handleUpdateOwner : undefined}
|
||||||
|
updateTier={hasEditTierPermission ? handleUpdateTier : undefined}
|
||||||
|
version={version + ''}
|
||||||
|
/>
|
||||||
|
<Tabs activeKey={tab} className="h-full" onChange={handleTabChange}>
|
||||||
|
<Tabs.TabPane
|
||||||
|
key={DATA_MODELS_DETAILS_TABS.MODEL}
|
||||||
|
tab={
|
||||||
|
<span data-testid={DATA_MODELS_DETAILS_TABS.MODEL}>
|
||||||
|
{t('label.model')}
|
||||||
|
</span>
|
||||||
|
}>
|
||||||
|
<Card className={ENTITY_CARD_CLASS}>
|
||||||
|
<Space className="w-full" direction="vertical" size={8}>
|
||||||
|
<Description
|
||||||
|
description={description}
|
||||||
|
entityFqn={dashboardDataModelFQN}
|
||||||
|
entityName={entityName}
|
||||||
|
entityType={EntityType.CONTAINER}
|
||||||
|
hasEditAccess={hasEditDescriptionPermission}
|
||||||
|
isEdit={isEditDescription}
|
||||||
|
isReadOnly={deleted}
|
||||||
|
owner={owner}
|
||||||
|
onCancel={() => setIsEditDescription(false)}
|
||||||
|
onDescriptionEdit={() => setIsEditDescription(true)}
|
||||||
|
onDescriptionUpdate={handleUpdateDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelTab
|
||||||
|
data={dataModelData?.columns || []}
|
||||||
|
hasEditDescriptionPermission={hasEditDescriptionPermission}
|
||||||
|
hasEditTagsPermission={hasEditTagsPermission}
|
||||||
|
isReadOnly={Boolean(deleted)}
|
||||||
|
onUpdate={handleUpdateDataModel}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</PageContainerV1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataModelsPage;
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { ServicePageData } from 'pages/service';
|
||||||
|
|
||||||
|
export interface DataModelTableProps {
|
||||||
|
data: Array<ServicePageData>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DATA_MODELS_DETAILS_TABS {
|
||||||
|
MODEL = 'model',
|
||||||
|
ACTIVITY = 'activityFeed',
|
||||||
|
SQL = 'sql',
|
||||||
|
LINEAGE = 'lineage',
|
||||||
|
}
|
@ -27,6 +27,7 @@ import TestConnection from 'components/common/TestConnection/TestConnection';
|
|||||||
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
|
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
|
||||||
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
|
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||||
import PageContainerV1 from 'components/containers/PageContainerV1';
|
import PageContainerV1 from 'components/containers/PageContainerV1';
|
||||||
|
import DataModelTable from 'components/DataModels/DataModelsTable';
|
||||||
import Ingestion from 'components/Ingestion/Ingestion.component';
|
import Ingestion from 'components/Ingestion/Ingestion.component';
|
||||||
import Loader from 'components/Loader/Loader';
|
import Loader from 'components/Loader/Loader';
|
||||||
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
|
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
|
||||||
@ -36,6 +37,7 @@ import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
|||||||
import { EntityType } from 'enums/entity.enum';
|
import { EntityType } from 'enums/entity.enum';
|
||||||
import { compare } from 'fast-json-patch';
|
import { compare } from 'fast-json-patch';
|
||||||
import { Container } from 'generated/entity/data/container';
|
import { Container } from 'generated/entity/data/container';
|
||||||
|
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
|
||||||
import { isEmpty, isNil, isUndefined, startCase, toLower } from 'lodash';
|
import { isEmpty, isNil, isUndefined, startCase, toLower } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ExtraInfo,
|
ExtraInfo,
|
||||||
@ -46,7 +48,7 @@ import {
|
|||||||
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||||
import { getDashboards } from 'rest/dashboardAPI';
|
import { getDashboards, getDataModels } from 'rest/dashboardAPI';
|
||||||
import { getDatabases } from 'rest/databaseAPI';
|
import { getDatabases } from 'rest/databaseAPI';
|
||||||
import {
|
import {
|
||||||
deleteIngestionPipelineById,
|
deleteIngestionPipelineById,
|
||||||
@ -121,7 +123,8 @@ export type ServicePageData =
|
|||||||
| Dashboard
|
| Dashboard
|
||||||
| Mlmodel
|
| Mlmodel
|
||||||
| Pipeline
|
| Pipeline
|
||||||
| Container;
|
| Container
|
||||||
|
| DashboardDataModel;
|
||||||
|
|
||||||
const ServicePage: FunctionComponent = () => {
|
const ServicePage: FunctionComponent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -140,6 +143,8 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [serviceDetails, setServiceDetails] = useState<ServicesType>();
|
const [serviceDetails, setServiceDetails] = useState<ServicesType>();
|
||||||
const [data, setData] = useState<Array<ServicePageData>>([]);
|
const [data, setData] = useState<Array<ServicePageData>>([]);
|
||||||
|
const [dataModel, setDataModel] = useState<Array<ServicePageData>>([]);
|
||||||
|
const [dataModelPaging, setDataModelPaging] = useState<Paging>(pagingObject);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [paging, setPaging] = useState<Paging>(pagingObject);
|
const [paging, setPaging] = useState<Paging>(pagingObject);
|
||||||
const [activeTab, setActiveTab] = useState(
|
const [activeTab, setActiveTab] = useState(
|
||||||
@ -187,7 +192,8 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
serviceName,
|
serviceName,
|
||||||
paging.total,
|
paging.total,
|
||||||
ingestions,
|
ingestions,
|
||||||
servicePermission
|
servicePermission,
|
||||||
|
dataModelPaging.total
|
||||||
),
|
),
|
||||||
[serviceName, paging, ingestions, servicePermission]
|
[serviceName, paging, ingestions, servicePermission]
|
||||||
);
|
);
|
||||||
@ -449,6 +455,23 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDashboardsDataModel = async (paging?: PagingWithoutTotal) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { data, paging: resPaging } = await getDataModels(
|
||||||
|
serviceFQN,
|
||||||
|
'owner,tags,followers',
|
||||||
|
paging
|
||||||
|
);
|
||||||
|
setDataModel(data);
|
||||||
|
setDataModelPaging(resPaging);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchPipeLines = async (paging?: PagingWithoutTotal) => {
|
const fetchPipeLines = async (paging?: PagingWithoutTotal) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -517,6 +540,7 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
case ServiceCategory.DASHBOARD_SERVICES: {
|
case ServiceCategory.DASHBOARD_SERVICES: {
|
||||||
fetchDashboards(paging);
|
fetchDashboards(paging);
|
||||||
|
fetchDashboardsDataModel(paging);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -877,6 +901,9 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDataModalTab = () => (
|
||||||
|
<DataModelTable data={dataModel} isLoading={isLoading} />
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
servicePageTabs(getCountLabel(serviceName))[activeTab - 1].path !== tab
|
servicePageTabs(getCountLabel(serviceName))[activeTab - 1].path !== tab
|
||||||
@ -1138,6 +1165,7 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{activeTab === 4 && getDataModalTab()}
|
||||||
{activeTab === 2 && getIngestionTab()}
|
{activeTab === 2 && getIngestionTab()}
|
||||||
|
|
||||||
{activeTab === 3 && (
|
{activeTab === 3 && (
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { PagingWithoutTotal, RestoreRequestType } from 'Models';
|
import { PagingResponse, PagingWithoutTotal, RestoreRequestType } from 'Models';
|
||||||
import { ServicePageData } from 'pages/service';
|
import { ServicePageData } from 'pages/service';
|
||||||
import { Dashboard } from '../generated/entity/data/dashboard';
|
import { Dashboard } from '../generated/entity/data/dashboard';
|
||||||
import { EntityHistory } from '../generated/type/entityHistory';
|
import { EntityHistory } from '../generated/type/entityHistory';
|
||||||
@ -129,3 +129,22 @@ export const restoreDashboard = async (id: string) => {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataModels = async (
|
||||||
|
service: string,
|
||||||
|
fields: string,
|
||||||
|
paging?: PagingWithoutTotal
|
||||||
|
) => {
|
||||||
|
const response = await APIClient.get<PagingResponse<ServicePageData[]>>(
|
||||||
|
`/dashboard/datamodels`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
service,
|
||||||
|
fields,
|
||||||
|
...paging,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 { AxiosResponse } from 'axios';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
|
||||||
|
import { EntityReference } from 'generated/type/entityReference';
|
||||||
|
import APIClient from './index';
|
||||||
|
|
||||||
|
const URL = '/dashboard/datamodels';
|
||||||
|
|
||||||
|
const configOptionsForPatch = {
|
||||||
|
headers: { 'Content-type': 'application/json-patch+json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDataModelsByName = async (
|
||||||
|
name: string,
|
||||||
|
fields: string | string[]
|
||||||
|
) => {
|
||||||
|
const response = await APIClient.get<DashboardDataModel>(
|
||||||
|
`${URL}/name/${name}?fields=${fields}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const patchDataModelDetails = async (id: string, data: Operation[]) => {
|
||||||
|
const response = await APIClient.patch<
|
||||||
|
Operation[],
|
||||||
|
AxiosResponse<DashboardDataModel>
|
||||||
|
>(`${URL}/${id}`, data, configOptionsForPatch);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addDataModelFollower = async (id: string, userId: string) => {
|
||||||
|
const response = await APIClient.put<
|
||||||
|
string,
|
||||||
|
AxiosResponse<{
|
||||||
|
changeDescription: { fieldsAdded: { newValue: EntityReference[] }[] };
|
||||||
|
}>
|
||||||
|
>(`${URL}/${id}/followers`, userId, configOptions);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeDataModelFollower = async (id: string, userId: string) => {
|
||||||
|
const response = await APIClient.delete<
|
||||||
|
string,
|
||||||
|
AxiosResponse<{
|
||||||
|
changeDescription: { fieldsDeleted: { oldValue: EntityReference[] }[] };
|
||||||
|
}>
|
||||||
|
>(`${URL}/${id}/followers/${userId}`, configOptions);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 {
|
||||||
|
PLACEHOLDER_ROUTE_DATA_MODEL_FQN,
|
||||||
|
PLACEHOLDER_ROUTE_TAB,
|
||||||
|
ROUTES,
|
||||||
|
} from 'constants/constants';
|
||||||
|
import { Column } from 'generated/entity/data/dashboardDataModel';
|
||||||
|
import { LabelType, State, TagLabel } from 'generated/type/tagLabel';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { EntityTags, TagOption } from 'Models';
|
||||||
|
|
||||||
|
export const getDataModelsDetailPath = (dataModelFQN: string, tab?: string) => {
|
||||||
|
let path = tab
|
||||||
|
? ROUTES.DATA_MODEL_DETAILS_WITH_TAB
|
||||||
|
: ROUTES.DATA_MODEL_DETAILS;
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_DATA_MODEL_FQN, dataModelFQN);
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_TAB, tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDataModelColumnDescription = (
|
||||||
|
containerColumns: Column[] = [],
|
||||||
|
changedColumnName: string,
|
||||||
|
description: string
|
||||||
|
) => {
|
||||||
|
containerColumns.forEach((containerColumn) => {
|
||||||
|
if (containerColumn.name === changedColumnName) {
|
||||||
|
containerColumn.description = description;
|
||||||
|
} else {
|
||||||
|
const hasChildren = !isEmpty(containerColumn.children);
|
||||||
|
|
||||||
|
// stop condition
|
||||||
|
if (hasChildren) {
|
||||||
|
updateDataModelColumnDescription(
|
||||||
|
containerColumn.children,
|
||||||
|
changedColumnName,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatedDataModelColumnTags = (
|
||||||
|
containerColumn: Column,
|
||||||
|
newContainerColumnTags: TagOption[] = []
|
||||||
|
) => {
|
||||||
|
const newTagsFqnList = newContainerColumnTags.map((newTag) => newTag.fqn);
|
||||||
|
|
||||||
|
const prevTags = containerColumn?.tags?.filter((tag) =>
|
||||||
|
newTagsFqnList.includes(tag.tagFQN)
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevTagsFqnList = prevTags?.map((prevTag) => prevTag.tagFQN);
|
||||||
|
|
||||||
|
const newTags: EntityTags[] = newContainerColumnTags.reduce((prev, curr) => {
|
||||||
|
const isExistingTag = prevTagsFqnList?.includes(curr.fqn);
|
||||||
|
|
||||||
|
return isExistingTag
|
||||||
|
? prev
|
||||||
|
: [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
labelType: LabelType.Manual,
|
||||||
|
state: State.Confirmed,
|
||||||
|
source: curr.source,
|
||||||
|
tagFQN: curr.fqn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [] as EntityTags[]);
|
||||||
|
|
||||||
|
return [...(prevTags as TagLabel[]), ...newTags];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDataModelColumnTags = (
|
||||||
|
containerColumns: Column[] = [],
|
||||||
|
changedColumnName: string,
|
||||||
|
newColumnTags: TagOption[] = []
|
||||||
|
) => {
|
||||||
|
containerColumns.forEach((containerColumn) => {
|
||||||
|
if (containerColumn.name === changedColumnName) {
|
||||||
|
containerColumn.tags = getUpdatedDataModelColumnTags(
|
||||||
|
containerColumn,
|
||||||
|
newColumnTags
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const hasChildren = !isEmpty(containerColumn.children);
|
||||||
|
|
||||||
|
// stop condition
|
||||||
|
if (hasChildren) {
|
||||||
|
updateDataModelColumnTags(
|
||||||
|
containerColumn.children,
|
||||||
|
changedColumnName,
|
||||||
|
newColumnTags
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -893,7 +893,8 @@ export const getServicePageTabs = (
|
|||||||
serviceName: ServiceTypes,
|
serviceName: ServiceTypes,
|
||||||
instanceCount: number,
|
instanceCount: number,
|
||||||
ingestions: IngestionPipeline[],
|
ingestions: IngestionPipeline[],
|
||||||
servicePermission: OperationPermission
|
servicePermission: OperationPermission,
|
||||||
|
dataModelCount: number
|
||||||
) => {
|
) => {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
@ -906,6 +907,15 @@ export const getServicePageTabs = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serviceName === ServiceCategory.DASHBOARD_SERVICES) {
|
||||||
|
tabs.push({
|
||||||
|
name: t('label.data-model'),
|
||||||
|
isProtected: false,
|
||||||
|
position: 4,
|
||||||
|
count: dataModelCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tabs.push(
|
tabs.push(
|
||||||
{
|
{
|
||||||
name: t('label.ingestion-plural'),
|
name: t('label.ingestion-plural'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user