mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26: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
|
||||
}
|
||||
},
|
||||
"required": ["name", "service", "modelType", "columns"],
|
||||
"required": ["name", "service", "dataModelType", "columns"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
@ -125,7 +125,7 @@
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"modelType",
|
||||
"dataModelType",
|
||||
"columns"
|
||||
],
|
||||
"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')
|
||||
)
|
||||
);
|
||||
|
||||
const DataModelDetailsPage = withSuspenseFallback(
|
||||
React.lazy(() => import('pages/DataModelPage/DataModelPage.component'))
|
||||
);
|
||||
|
||||
const DatasetDetailsPage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() => import('pages/DatasetDetailsPage/DatasetDetailsPage.component')
|
||||
@ -350,6 +355,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
component={DashboardDetailsPage}
|
||||
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
|
||||
exact
|
||||
component={PipelineDetailsPage}
|
||||
|
@ -245,6 +245,9 @@ export const servicesDisplayName: { [key: string]: string } = {
|
||||
objectStoreServices: i18n.t('label.entity-service', {
|
||||
entity: i18n.t('label.object-store'),
|
||||
}),
|
||||
dashboardDataModel: i18n.t('label.entity-service', {
|
||||
entity: i18n.t('label.data-model'),
|
||||
}),
|
||||
};
|
||||
|
||||
export const DEF_UI_SCHEMA = {
|
||||
|
@ -124,6 +124,7 @@ export const INGESTION_NAME = ':ingestionName';
|
||||
export const LOG_ENTITY_NAME = ':logEntityName';
|
||||
export const KPI_NAME = ':kpiName';
|
||||
export const PLACEHOLDER_ACTION = ':action';
|
||||
export const PLACEHOLDER_ROUTE_DATA_MODEL_FQN = ':dashboardDataModelFQN';
|
||||
|
||||
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}`,
|
||||
DASHBOARD_DETAILS: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}`,
|
||||
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}`,
|
||||
SCHEMA_DETAILS: `/databaseSchema/${PLACEHOLDER_ROUTE_DATABASE_SCHEMA_FQN}`,
|
||||
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;
|
||||
};
|
||||
|
||||
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) => {
|
||||
let path = tab ? ROUTES.PIPELINE_DETAILS_WITH_TAB : ROUTES.PIPELINE_DETAILS;
|
||||
path = path.replace(PLACEHOLDER_ROUTE_PIPELINE_FQN, pipelineFQN);
|
||||
|
@ -42,6 +42,7 @@ export enum EntityType {
|
||||
ALERT = 'alert',
|
||||
CONTAINER = 'container',
|
||||
TAG = 'tag',
|
||||
DASHBOARD_DATA_MODEL = 'dashboardDataModel',
|
||||
}
|
||||
|
||||
export enum AssetsType {
|
||||
|
@ -181,6 +181,7 @@
|
||||
"data-insight-tier-summary": "Total Data Assets by Tier",
|
||||
"data-insight-top-viewed-entity-summary": "Most Viewed Data Assets",
|
||||
"data-insight-total-entity-summary": "Total Data Assets",
|
||||
"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-plural": "ML Models",
|
||||
"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": "Total de activos de datos por nivel",
|
||||
"data-insight-top-viewed-entity-summary": "Activos de datos más vistos",
|
||||
"data-insight-total-entity-summary": "Total de activos de datos",
|
||||
"data-model": "Data Model",
|
||||
"data-quality-test": "Prueba de calidad de datos",
|
||||
"data-type": "Tipo de datos",
|
||||
"database": "Base de datos",
|
||||
@ -470,7 +471,9 @@
|
||||
"ml-model-lowercase-plural": "modelos de ML",
|
||||
"ml-model-plural": "Modelos de ML",
|
||||
"mode": "Moda",
|
||||
"model": "Model",
|
||||
"model-name": "Nombre del Modelo",
|
||||
"model-plural": "Models",
|
||||
"model-store": "Almacenamiento de Modelos",
|
||||
"monday": "Lunes",
|
||||
"month": "Mes",
|
||||
|
@ -181,6 +181,7 @@
|
||||
"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-total-entity-summary": "Total Resources de Données",
|
||||
"data-model": "Data Model",
|
||||
"data-quality-test": "Data Quality Test",
|
||||
"data-type": "Type de donnée",
|
||||
"database": "Base de Données",
|
||||
@ -470,7 +471,9 @@
|
||||
"ml-model-lowercase-plural": "ML models",
|
||||
"ml-model-plural": "Modéles d'IA",
|
||||
"mode": "Mode",
|
||||
"model": "Model",
|
||||
"model-name": "Nom du Modèle",
|
||||
"model-plural": "Models",
|
||||
"model-store": "Magasin de Modèles",
|
||||
"monday": "Monday",
|
||||
"month": "Month",
|
||||
|
@ -181,6 +181,7 @@
|
||||
"data-insight-tier-summary": "ティアごとの全データアセット",
|
||||
"data-insight-top-viewed-entity-summary": "最も閲覧されたデータアセット",
|
||||
"data-insight-total-entity-summary": "全てのデータアセット",
|
||||
"data-model": "Data Model",
|
||||
"data-quality-test": "データ品質テスト",
|
||||
"data-type": "データ型",
|
||||
"database": "データベース",
|
||||
@ -470,7 +471,9 @@
|
||||
"ml-model-lowercase-plural": "ML models",
|
||||
"ml-model-plural": "MLモデル",
|
||||
"mode": "モード",
|
||||
"model": "Model",
|
||||
"model-name": "モデル名",
|
||||
"model-plural": "Models",
|
||||
"model-store": "モデルストア",
|
||||
"monday": "月曜日",
|
||||
"month": "月",
|
||||
|
@ -181,6 +181,7 @@
|
||||
"data-insight-tier-summary": "Recursos de dados totais por nível",
|
||||
"data-insight-top-viewed-entity-summary": "Recursos de dados mais visualizados",
|
||||
"data-insight-total-entity-summary": "Total de recursos de dados",
|
||||
"data-model": "Data Model",
|
||||
"data-quality-test": "Teste de qualidade de dados",
|
||||
"data-type": "Tipo de dado",
|
||||
"database": "Banco de dados",
|
||||
@ -470,7 +471,9 @@
|
||||
"ml-model-lowercase-plural": "ML models",
|
||||
"ml-model-plural": "Modelos de ML",
|
||||
"mode": "Modo",
|
||||
"model": "Model",
|
||||
"model-name": "Nome do modelo",
|
||||
"model-plural": "Models",
|
||||
"model-store": "Estoque modelo",
|
||||
"monday": "Segunda-feira",
|
||||
"month": "Mês",
|
||||
|
@ -181,6 +181,7 @@
|
||||
"data-insight-tier-summary": "分层的总数据资产",
|
||||
"data-insight-top-viewed-entity-summary": "查看次数最多的数据资产",
|
||||
"data-insight-total-entity-summary": "所有数据资产",
|
||||
"data-model": "Data Model",
|
||||
"data-quality-test": "Data Quality Test",
|
||||
"data-type": "Data Type",
|
||||
"database": "数据库",
|
||||
@ -470,7 +471,9 @@
|
||||
"ml-model-lowercase-plural": "ML models",
|
||||
"ml-model-plural": "机器学习模型",
|
||||
"mode": "Mode",
|
||||
"model": "Model",
|
||||
"model-name": "模型名",
|
||||
"model-plural": "Models",
|
||||
"model-store": "模型存储",
|
||||
"monday": "Monday",
|
||||
"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 { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import PageContainerV1 from 'components/containers/PageContainerV1';
|
||||
import DataModelTable from 'components/DataModels/DataModelsTable';
|
||||
import Ingestion from 'components/Ingestion/Ingestion.component';
|
||||
import Loader from 'components/Loader/Loader';
|
||||
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 { compare } from 'fast-json-patch';
|
||||
import { Container } from 'generated/entity/data/container';
|
||||
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
|
||||
import { isEmpty, isNil, isUndefined, startCase, toLower } from 'lodash';
|
||||
import {
|
||||
ExtraInfo,
|
||||
@ -46,7 +48,7 @@ import {
|
||||
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 {
|
||||
deleteIngestionPipelineById,
|
||||
@ -121,7 +123,8 @@ export type ServicePageData =
|
||||
| Dashboard
|
||||
| Mlmodel
|
||||
| Pipeline
|
||||
| Container;
|
||||
| Container
|
||||
| DashboardDataModel;
|
||||
|
||||
const ServicePage: FunctionComponent = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -140,6 +143,8 @@ const ServicePage: FunctionComponent = () => {
|
||||
const [description, setDescription] = useState('');
|
||||
const [serviceDetails, setServiceDetails] = useState<ServicesType>();
|
||||
const [data, setData] = useState<Array<ServicePageData>>([]);
|
||||
const [dataModel, setDataModel] = useState<Array<ServicePageData>>([]);
|
||||
const [dataModelPaging, setDataModelPaging] = useState<Paging>(pagingObject);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [paging, setPaging] = useState<Paging>(pagingObject);
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
@ -187,7 +192,8 @@ const ServicePage: FunctionComponent = () => {
|
||||
serviceName,
|
||||
paging.total,
|
||||
ingestions,
|
||||
servicePermission
|
||||
servicePermission,
|
||||
dataModelPaging.total
|
||||
),
|
||||
[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) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -517,6 +540,7 @@ const ServicePage: FunctionComponent = () => {
|
||||
}
|
||||
case ServiceCategory.DASHBOARD_SERVICES: {
|
||||
fetchDashboards(paging);
|
||||
fetchDashboardsDataModel(paging);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -877,6 +901,9 @@ const ServicePage: FunctionComponent = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDataModalTab = () => (
|
||||
<DataModelTable data={dataModel} isLoading={isLoading} />
|
||||
);
|
||||
useEffect(() => {
|
||||
if (
|
||||
servicePageTabs(getCountLabel(serviceName))[activeTab - 1].path !== tab
|
||||
@ -1138,6 +1165,7 @@ const ServicePage: FunctionComponent = () => {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeTab === 4 && getDataModalTab()}
|
||||
{activeTab === 2 && getIngestionTab()}
|
||||
|
||||
{activeTab === 3 && (
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { PagingWithoutTotal, RestoreRequestType } from 'Models';
|
||||
import { PagingResponse, PagingWithoutTotal, RestoreRequestType } from 'Models';
|
||||
import { ServicePageData } from 'pages/service';
|
||||
import { Dashboard } from '../generated/entity/data/dashboard';
|
||||
import { EntityHistory } from '../generated/type/entityHistory';
|
||||
@ -129,3 +129,22 @@ export const restoreDashboard = async (id: string) => {
|
||||
|
||||
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,
|
||||
instanceCount: number,
|
||||
ingestions: IngestionPipeline[],
|
||||
servicePermission: OperationPermission
|
||||
servicePermission: OperationPermission,
|
||||
dataModelCount: number
|
||||
) => {
|
||||
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(
|
||||
{
|
||||
name: t('label.ingestion-plural'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user