mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-09 07:23:39 +00:00
Feat: Added DBT Model Details page and Explore page tab (#1394)
* Feat: Added DBT Model tab to Explore page * Feat: Added DBTModel Details page * Changed dataset to dbtModel * Added support to add tier tag * Adding suggestions support for DBT Models. * Added ViewDefinition Tab UI Co-authored-by: Sachin-chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
parent
fdd6ca14b7
commit
dc1576cd91
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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 { Dbtmodel } from '../generated/entity/data/dbtmodel';
|
||||||
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
|
import APIClient from './index';
|
||||||
|
|
||||||
|
export const getDBTModelDetails: Function = (
|
||||||
|
id: string,
|
||||||
|
arrQueryFields: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const url = getURLWithQueryFields(`/dbtmodels/${id}`, arrQueryFields);
|
||||||
|
|
||||||
|
return APIClient.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDBTModelDetailsByFQN: Function = (
|
||||||
|
fqn: string,
|
||||||
|
arrQueryFields: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const url = getURLWithQueryFields(`/dbtmodels/name/${fqn}`, arrQueryFields);
|
||||||
|
|
||||||
|
return APIClient.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDatabaseDBTModels: Function = (
|
||||||
|
databaseName: string,
|
||||||
|
paging: string,
|
||||||
|
arrQueryFields?: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const url = `${getURLWithQueryFields(
|
||||||
|
`/tables`,
|
||||||
|
arrQueryFields
|
||||||
|
)}&database=${databaseName}${paging ? paging : ''}`;
|
||||||
|
|
||||||
|
return APIClient.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const patchDBTModelDetails: Function = (
|
||||||
|
id: string,
|
||||||
|
data: Dbtmodel
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json-patch+json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.patch(`/dbtmodels/${id}`, data, configOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addFollower: Function = (
|
||||||
|
dbtModelId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.put(
|
||||||
|
`/dbtmodels/${dbtModelId}/followers`,
|
||||||
|
userId,
|
||||||
|
configOptions
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFollower: Function = (
|
||||||
|
dbtModelId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.delete(
|
||||||
|
`/dbtmodels/${dbtModelId}/followers/${userId}`,
|
||||||
|
configOptions
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,284 @@
|
|||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { EntityTags } from 'Models';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getTeamDetailsPath } from '../../constants/constants';
|
||||||
|
import { CSMode } from '../../enums/codemirror.enum';
|
||||||
|
import { Dbtmodel } from '../../generated/entity/data/dbtmodel';
|
||||||
|
import { User } from '../../generated/entity/teams/user';
|
||||||
|
import { LabelType, State } from '../../generated/type/tagLabel';
|
||||||
|
import { useAuth } from '../../hooks/authHooks';
|
||||||
|
import {
|
||||||
|
getCurrentUserId,
|
||||||
|
getPartialNameFromFQN,
|
||||||
|
getUserTeams,
|
||||||
|
} from '../../utils/CommonUtils';
|
||||||
|
import { getTagsWithoutTier } from '../../utils/TableUtils';
|
||||||
|
import Description from '../common/description/Description';
|
||||||
|
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||||
|
import TabsPane from '../common/TabsPane/TabsPane';
|
||||||
|
import PageContainer from '../containers/PageContainer';
|
||||||
|
import ManageTab from '../ManageTab/ManageTab.component';
|
||||||
|
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||||
|
import SchemaTab from '../SchemaTab/SchemaTab.component';
|
||||||
|
import { DBTModelDetailsProps } from './DBTModelDetails.interface';
|
||||||
|
|
||||||
|
const DBTModelDetails: React.FC<DBTModelDetailsProps> = ({
|
||||||
|
dbtModelDetails,
|
||||||
|
entityName,
|
||||||
|
dbtModelFQN,
|
||||||
|
activeTab,
|
||||||
|
setActiveTabHandler,
|
||||||
|
owner,
|
||||||
|
description,
|
||||||
|
columns,
|
||||||
|
followDBTModelHandler,
|
||||||
|
unfollowDBTModelHandler,
|
||||||
|
followers,
|
||||||
|
dbtModelTags,
|
||||||
|
slashedDBTModelName,
|
||||||
|
descriptionUpdateHandler,
|
||||||
|
columnsUpdateHandler,
|
||||||
|
settingsUpdateHandler,
|
||||||
|
users,
|
||||||
|
version,
|
||||||
|
viewDefinition = '',
|
||||||
|
tier,
|
||||||
|
}: DBTModelDetailsProps) => {
|
||||||
|
const { isAuthDisabled } = useAuth();
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const [followersCount, setFollowersCount] = useState(0);
|
||||||
|
const [isFollowing, setIsFollowing] = useState(false);
|
||||||
|
|
||||||
|
const hasEditAccess = () => {
|
||||||
|
if (owner?.type === 'user') {
|
||||||
|
return owner.id === getCurrentUserId();
|
||||||
|
} else {
|
||||||
|
return getUserTeams().some((team) => team.id === owner?.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const setFollowersData = (followers: Array<User>) => {
|
||||||
|
setIsFollowing(
|
||||||
|
followers.some(({ id }: { id: string }) => id === getCurrentUserId())
|
||||||
|
);
|
||||||
|
setFollowersCount(followers?.length);
|
||||||
|
};
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: 'Schema',
|
||||||
|
icon: {
|
||||||
|
alt: 'schema',
|
||||||
|
name: 'icon-schema',
|
||||||
|
title: 'Schema',
|
||||||
|
},
|
||||||
|
isProtected: false,
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View Definition',
|
||||||
|
icon: {
|
||||||
|
alt: 'view_definition',
|
||||||
|
name: 'icon-profiler',
|
||||||
|
title: 'View Definition',
|
||||||
|
},
|
||||||
|
isProtected: false,
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manage',
|
||||||
|
icon: {
|
||||||
|
alt: 'manage',
|
||||||
|
name: 'icon-manage',
|
||||||
|
title: 'Manage',
|
||||||
|
},
|
||||||
|
isProtected: true,
|
||||||
|
protectedState: !owner || hasEditAccess(),
|
||||||
|
position: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const extraInfo: Array<{
|
||||||
|
key?: string;
|
||||||
|
value: string | number | React.ReactNode;
|
||||||
|
isLink?: boolean;
|
||||||
|
placeholderText?: string;
|
||||||
|
openInNewTab?: boolean;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: 'Owner',
|
||||||
|
value:
|
||||||
|
owner?.type === 'team'
|
||||||
|
? getTeamDetailsPath(owner?.name || '')
|
||||||
|
: owner?.name || '',
|
||||||
|
placeholderText: owner?.displayName || '',
|
||||||
|
isLink: owner?.type === 'team',
|
||||||
|
openInNewTab: false,
|
||||||
|
},
|
||||||
|
{ key: 'Tier', value: tier ? tier.split('.')[1] : '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onDescriptionEdit = (): void => {
|
||||||
|
setIsEdit(true);
|
||||||
|
};
|
||||||
|
const onCancel = () => {
|
||||||
|
setIsEdit(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDescriptionUpdate = (updatedHTML: string) => {
|
||||||
|
if (description !== updatedHTML) {
|
||||||
|
const updatedDBTModelDetails = {
|
||||||
|
...dbtModelDetails,
|
||||||
|
description: updatedHTML,
|
||||||
|
};
|
||||||
|
descriptionUpdateHandler(updatedDBTModelDetails);
|
||||||
|
setIsEdit(false);
|
||||||
|
} else {
|
||||||
|
setIsEdit(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onColumnsUpdate = (updateColumns: Dbtmodel['columns']) => {
|
||||||
|
if (!isEqual(columns, updateColumns)) {
|
||||||
|
const updatedDBTModelDetails = {
|
||||||
|
...dbtModelDetails,
|
||||||
|
columns: updateColumns,
|
||||||
|
};
|
||||||
|
columnsUpdateHandler(updatedDBTModelDetails);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSettingsUpdate = (newOwner?: Dbtmodel['owner'], newTier?: string) => {
|
||||||
|
if (newOwner || newTier) {
|
||||||
|
const tierTag: Dbtmodel['tags'] = newTier
|
||||||
|
? [
|
||||||
|
...getTagsWithoutTier(dbtModelDetails.tags as Array<EntityTags>),
|
||||||
|
{
|
||||||
|
tagFQN: newTier,
|
||||||
|
labelType: LabelType.Manual,
|
||||||
|
state: State.Confirmed,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: dbtModelDetails.tags;
|
||||||
|
const updatedDBTModelDetails = {
|
||||||
|
...dbtModelDetails,
|
||||||
|
owner: newOwner
|
||||||
|
? {
|
||||||
|
...dbtModelDetails.owner,
|
||||||
|
...newOwner,
|
||||||
|
}
|
||||||
|
: dbtModelDetails.owner,
|
||||||
|
tags: tierTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
return settingsUpdateHandler(updatedDBTModelDetails);
|
||||||
|
} else {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const followDBTModel = () => {
|
||||||
|
if (isFollowing) {
|
||||||
|
setFollowersCount((preValu) => preValu - 1);
|
||||||
|
setIsFollowing(false);
|
||||||
|
unfollowDBTModelHandler();
|
||||||
|
} else {
|
||||||
|
setFollowersCount((preValu) => preValu + 1);
|
||||||
|
setIsFollowing(true);
|
||||||
|
followDBTModelHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthDisabled && users.length && followers.length) {
|
||||||
|
setFollowersData(followers);
|
||||||
|
}
|
||||||
|
}, [users, followers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFollowersData(followers);
|
||||||
|
}, [followers]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div className="tw-px-4 tw-w-full tw-h-full tw-flex tw-flex-col">
|
||||||
|
<EntityPageInfo
|
||||||
|
entityName={entityName}
|
||||||
|
extraInfo={extraInfo}
|
||||||
|
followers={followersCount}
|
||||||
|
followersList={followers}
|
||||||
|
followHandler={followDBTModel}
|
||||||
|
isFollowing={isFollowing}
|
||||||
|
tags={dbtModelTags}
|
||||||
|
tier={tier}
|
||||||
|
titleLinks={slashedDBTModelName}
|
||||||
|
version={version}
|
||||||
|
versionHandler={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="tw-mt-1 tw-flex tw-flex-col tw-flex-grow">
|
||||||
|
<TabsPane
|
||||||
|
activeTab={activeTab}
|
||||||
|
className="tw-flex-initial"
|
||||||
|
setActiveTab={setActiveTabHandler}
|
||||||
|
tabs={tabs}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="tw-bg-white tw-flex-grow">
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<div className="tw-grid tw-grid-cols-4 tw-gap-4 tw-w-full tw-mt-4 ">
|
||||||
|
<div className="tw-col-span-4">
|
||||||
|
<Description
|
||||||
|
description={description}
|
||||||
|
entityName={entityName}
|
||||||
|
hasEditAccess={hasEditAccess()}
|
||||||
|
isEdit={isEdit}
|
||||||
|
owner={owner}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onDescriptionEdit={onDescriptionEdit}
|
||||||
|
onDescriptionUpdate={onDescriptionUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tw-col-span-full">
|
||||||
|
<SchemaTab
|
||||||
|
columnName={getPartialNameFromFQN(
|
||||||
|
dbtModelFQN,
|
||||||
|
['column'],
|
||||||
|
'.'
|
||||||
|
)}
|
||||||
|
columns={columns}
|
||||||
|
hasEditAccess={hasEditAccess()}
|
||||||
|
joins={[]}
|
||||||
|
owner={owner}
|
||||||
|
onUpdate={onColumnsUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeTab === 2 && (
|
||||||
|
<div className="tw-my-4 tw-border tw-border-main tw-rounded-md tw-py-4 tw-h-full cm-h-full">
|
||||||
|
<SchemaEditor
|
||||||
|
className="tw-h-full"
|
||||||
|
mode={{ name: CSMode.SQL }}
|
||||||
|
value={viewDefinition}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeTab === 3 && (
|
||||||
|
<div className="tw-mt-4">
|
||||||
|
<ManageTab
|
||||||
|
currentTier={tier}
|
||||||
|
currentUser={owner?.id}
|
||||||
|
hasEditAccess={hasEditAccess()}
|
||||||
|
onSave={onSettingsUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DBTModelDetails;
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { EntityTags } from 'Models';
|
||||||
|
import { Dbtmodel } from '../../generated/entity/data/dbtmodel';
|
||||||
|
import { EntityReference } from '../../generated/entity/data/table';
|
||||||
|
import { User } from '../../generated/entity/teams/user';
|
||||||
|
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
||||||
|
|
||||||
|
export interface DatasetOwner extends EntityReference {
|
||||||
|
displayName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBTModelDetailsProps {
|
||||||
|
version?: string;
|
||||||
|
users: Array<User>;
|
||||||
|
dbtModelDetails: Dbtmodel;
|
||||||
|
dbtModelFQN: string;
|
||||||
|
entityName: string;
|
||||||
|
activeTab: number;
|
||||||
|
owner: DatasetOwner;
|
||||||
|
description: string;
|
||||||
|
tier: string;
|
||||||
|
columns: Dbtmodel['columns'];
|
||||||
|
followers: Array<User>;
|
||||||
|
dbtModelTags: Array<EntityTags>;
|
||||||
|
slashedDBTModelName: TitleBreadcrumbProps['titleLinks'];
|
||||||
|
viewDefinition: Dbtmodel['viewDefinition'];
|
||||||
|
setActiveTabHandler: (value: number) => void;
|
||||||
|
followDBTModelHandler: () => void;
|
||||||
|
unfollowDBTModelHandler: () => void;
|
||||||
|
settingsUpdateHandler: (updatedDBTModel: Dbtmodel) => Promise<void>;
|
||||||
|
columnsUpdateHandler: (updatedDBTModel: Dbtmodel) => void;
|
||||||
|
descriptionUpdateHandler: (updatedDBTModel: Dbtmodel) => void;
|
||||||
|
}
|
||||||
@ -73,6 +73,7 @@ const Explore: React.FC<ExploreProps> = ({
|
|||||||
updateTableCount,
|
updateTableCount,
|
||||||
updateTopicCount,
|
updateTopicCount,
|
||||||
updateDashboardCount,
|
updateDashboardCount,
|
||||||
|
updateDbtModelCount,
|
||||||
updatePipelineCount,
|
updatePipelineCount,
|
||||||
}: ExploreProps) => {
|
}: ExploreProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -197,6 +198,10 @@ const Explore: React.FC<ExploreProps> = ({
|
|||||||
case SearchIndex.PIPELINE:
|
case SearchIndex.PIPELINE:
|
||||||
updatePipelineCount(count);
|
updatePipelineCount(count);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SearchIndex.DBT_MODEL:
|
||||||
|
updateDbtModelCount(count);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -339,6 +344,8 @@ const Explore: React.FC<ExploreProps> = ({
|
|||||||
return getCountBadge(tabCounts.dashboard);
|
return getCountBadge(tabCounts.dashboard);
|
||||||
case SearchIndex.PIPELINE:
|
case SearchIndex.PIPELINE:
|
||||||
return getCountBadge(tabCounts.pipeline);
|
return getCountBadge(tabCounts.pipeline);
|
||||||
|
case SearchIndex.DBT_MODEL:
|
||||||
|
return getCountBadge(tabCounts.dbtModel);
|
||||||
default:
|
default:
|
||||||
return getCountBadge();
|
return getCountBadge();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,8 +68,10 @@ describe('Test Explore component', () => {
|
|||||||
topic: 2,
|
topic: 2,
|
||||||
dashboard: 8,
|
dashboard: 8,
|
||||||
pipeline: 5,
|
pipeline: 5,
|
||||||
|
dbtModel: 7,
|
||||||
}}
|
}}
|
||||||
updateDashboardCount={mockFunction}
|
updateDashboardCount={mockFunction}
|
||||||
|
updateDbtModelCount={mockFunction}
|
||||||
updatePipelineCount={mockFunction}
|
updatePipelineCount={mockFunction}
|
||||||
updateTableCount={mockFunction}
|
updateTableCount={mockFunction}
|
||||||
updateTopicCount={mockFunction}
|
updateTopicCount={mockFunction}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export interface ExploreProps {
|
|||||||
topic: number;
|
topic: number;
|
||||||
dashboard: number;
|
dashboard: number;
|
||||||
pipeline: number;
|
pipeline: number;
|
||||||
|
dbtModel: number;
|
||||||
};
|
};
|
||||||
searchText: string;
|
searchText: string;
|
||||||
sortValue: string;
|
sortValue: string;
|
||||||
@ -48,6 +49,7 @@ export interface ExploreProps {
|
|||||||
updateTopicCount: (count: number) => void;
|
updateTopicCount: (count: number) => void;
|
||||||
updateDashboardCount: (count: number) => void;
|
updateDashboardCount: (count: number) => void;
|
||||||
updatePipelineCount: (count: number) => void;
|
updatePipelineCount: (count: number) => void;
|
||||||
|
updateDbtModelCount: (count: number) => void;
|
||||||
fetchData: (value: SearchDataFunctionType[]) => void;
|
fetchData: (value: SearchDataFunctionType[]) => void;
|
||||||
searchResult: ExploreSearchData | undefined;
|
searchResult: ExploreSearchData | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isUndefined, lowerCase } from 'lodash';
|
import { isNil, isUndefined, lowerCase } from 'lodash';
|
||||||
import { DatasetSchemaTableTab } from 'Models';
|
import { DatasetSchemaTableTab } from 'Models';
|
||||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router';
|
import { useHistory, useParams } from 'react-router';
|
||||||
@ -121,7 +121,7 @@ const SchemaTab: FunctionComponent<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isReadOnly ? (
|
{!isReadOnly && !isNil(sampleData) ? (
|
||||||
<div className="tw-col-span-2 tw-text-right tw-mb-4">
|
<div className="tw-col-span-2 tw-text-right tw-mb-4">
|
||||||
<div
|
<div
|
||||||
className="tw-w-60 tw-inline-flex tw-border tw-border-main
|
className="tw-w-60 tw-inline-flex tw-border tw-border-main
|
||||||
|
|||||||
@ -55,9 +55,18 @@ type PipelineSource = {
|
|||||||
pipeline_name: string;
|
pipeline_name: string;
|
||||||
} & CommonSource;
|
} & CommonSource;
|
||||||
|
|
||||||
|
type DBTModelSource = {
|
||||||
|
dbt_model_id: string;
|
||||||
|
dbt_model_name: string;
|
||||||
|
} & CommonSource;
|
||||||
|
|
||||||
type Option = {
|
type Option = {
|
||||||
_index: string;
|
_index: string;
|
||||||
_source: TableSource & DashboardSource & TopicSource & PipelineSource;
|
_source: TableSource &
|
||||||
|
DashboardSource &
|
||||||
|
TopicSource &
|
||||||
|
PipelineSource &
|
||||||
|
DBTModelSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
||||||
@ -67,10 +76,13 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
const [dashboardSuggestions, setDashboardSuggestions] = useState<
|
const [dashboardSuggestions, setDashboardSuggestions] = useState<
|
||||||
DashboardSource[]
|
DashboardSource[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const [pipelineSuggestions, setPipelineSuggestions] = useState<
|
const [pipelineSuggestions, setPipelineSuggestions] = useState<
|
||||||
PipelineSource[]
|
PipelineSource[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [DBTModelSuggestions, setDBTModelSuggestions] = useState<
|
||||||
|
DBTModelSource[]
|
||||||
|
>([]);
|
||||||
const isMounting = useRef(true);
|
const isMounting = useRef(true);
|
||||||
|
|
||||||
const setSuggestions = (options: Array<Option>) => {
|
const setSuggestions = (options: Array<Option>) => {
|
||||||
@ -94,6 +106,11 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
.filter((option) => option._index === SearchIndex.PIPELINE)
|
.filter((option) => option._index === SearchIndex.PIPELINE)
|
||||||
.map((option) => option._source)
|
.map((option) => option._source)
|
||||||
);
|
);
|
||||||
|
setDBTModelSuggestions(
|
||||||
|
options
|
||||||
|
.filter((option) => option._index === SearchIndex.DBT_MODEL)
|
||||||
|
.map((option) => option._source)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGroupLabel = (index: string) => {
|
const getGroupLabel = (index: string) => {
|
||||||
@ -114,6 +131,11 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
label = 'Pipelines';
|
label = 'Pipelines';
|
||||||
icon = Icons.PIPELINE_GREY;
|
icon = Icons.PIPELINE_GREY;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SearchIndex.DBT_MODEL:
|
||||||
|
label = 'DBT Models';
|
||||||
|
icon = Icons.TABLE_GREY;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SearchIndex.TABLE:
|
case SearchIndex.TABLE:
|
||||||
default:
|
default:
|
||||||
@ -233,6 +255,24 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{DBTModelSuggestions.length > 0 && (
|
||||||
|
<>
|
||||||
|
{getGroupLabel(SearchIndex.DBT_MODEL)}
|
||||||
|
|
||||||
|
{DBTModelSuggestions.map((suggestion: DBTModelSource) => {
|
||||||
|
const fqdn = suggestion.fqdn;
|
||||||
|
const name = suggestion.dbt_model_name;
|
||||||
|
const serviceType = suggestion.service_type;
|
||||||
|
|
||||||
|
return getSuggestionElement(
|
||||||
|
fqdn,
|
||||||
|
serviceType,
|
||||||
|
name,
|
||||||
|
SearchIndex.DBT_MODEL
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,12 +7,31 @@ import 'codemirror/addon/fold/foldgutter.js';
|
|||||||
import 'codemirror/addon/selection/active-line';
|
import 'codemirror/addon/selection/active-line';
|
||||||
import 'codemirror/lib/codemirror.css';
|
import 'codemirror/lib/codemirror.css';
|
||||||
import 'codemirror/mode/javascript/javascript';
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/sql/sql';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
import { JSON_TAB_SIZE } from '../../constants/constants';
|
import { JSON_TAB_SIZE } from '../../constants/constants';
|
||||||
|
import { CSMode } from '../../enums/codemirror.enum';
|
||||||
import { getSchemaEditorValue } from './SchemaEditor.utils';
|
import { getSchemaEditorValue } from './SchemaEditor.utils';
|
||||||
|
|
||||||
const options = {
|
type Mode = {
|
||||||
|
name: CSMode;
|
||||||
|
json?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SchemaEditor = ({
|
||||||
|
value,
|
||||||
|
className = '',
|
||||||
|
mode = {
|
||||||
|
name: CSMode.JAVASCRIPT,
|
||||||
|
json: true,
|
||||||
|
},
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
mode?: Mode;
|
||||||
|
}) => {
|
||||||
|
const options = {
|
||||||
tabSize: JSON_TAB_SIZE,
|
tabSize: JSON_TAB_SIZE,
|
||||||
indentUnit: JSON_TAB_SIZE,
|
indentUnit: JSON_TAB_SIZE,
|
||||||
indentWithTabs: false,
|
indentWithTabs: false,
|
||||||
@ -23,14 +42,9 @@ const options = {
|
|||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
mode: {
|
mode,
|
||||||
name: 'javascript',
|
|
||||||
json: true,
|
|
||||||
},
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SchemaEditor = ({ value }: { value: string }) => {
|
|
||||||
const [internalValue, setInternalValue] = useState(
|
const [internalValue, setInternalValue] = useState(
|
||||||
getSchemaEditorValue(value)
|
getSchemaEditorValue(value)
|
||||||
);
|
);
|
||||||
@ -43,7 +57,7 @@ const SchemaEditor = ({ value }: { value: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
options={options}
|
options={options}
|
||||||
value={internalValue}
|
value={internalValue}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ const ASSETS_NAME = [
|
|||||||
'topic_name',
|
'topic_name',
|
||||||
'dashboard_name',
|
'dashboard_name',
|
||||||
'pipeline_name',
|
'pipeline_name',
|
||||||
|
'dbt_model_name',
|
||||||
];
|
];
|
||||||
|
|
||||||
const SearchedData: React.FC<SearchedDataProp> = ({
|
const SearchedData: React.FC<SearchedDataProp> = ({
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const ERROR500 = 'Something went wrong';
|
|||||||
const PLACEHOLDER_ROUTE_DATASET_FQN = ':datasetFQN';
|
const PLACEHOLDER_ROUTE_DATASET_FQN = ':datasetFQN';
|
||||||
const PLACEHOLDER_ROUTE_TOPIC_FQN = ':topicFQN';
|
const PLACEHOLDER_ROUTE_TOPIC_FQN = ':topicFQN';
|
||||||
const PLACEHOLDER_ROUTE_PIPELINE_FQN = ':pipelineFQN';
|
const PLACEHOLDER_ROUTE_PIPELINE_FQN = ':pipelineFQN';
|
||||||
|
const PLACEHOLDER_ROUTE_DBT_MODEL_FQN = ':dbtModelFQN';
|
||||||
const PLACEHOLDER_ROUTE_DASHBOARD_FQN = ':dashboardFQN';
|
const PLACEHOLDER_ROUTE_DASHBOARD_FQN = ':dashboardFQN';
|
||||||
const PLACEHOLDER_ROUTE_DATABASE_FQN = ':databaseFQN';
|
const PLACEHOLDER_ROUTE_DATABASE_FQN = ':databaseFQN';
|
||||||
const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN';
|
const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN';
|
||||||
@ -139,6 +140,8 @@ export const ROUTES = {
|
|||||||
DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`,
|
DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`,
|
||||||
PIPELINE_DETAILS: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}`,
|
PIPELINE_DETAILS: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}`,
|
||||||
PIPELINE_DETAILS_WITH_TAB: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
PIPELINE_DETAILS_WITH_TAB: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
|
DBT_MODEL_DETAILS: `/dbtmodel/${PLACEHOLDER_ROUTE_DBT_MODEL_FQN}`,
|
||||||
|
DBT_MODEL_DETAILS_WITH_TAB: `/dbtmodel/${PLACEHOLDER_ROUTE_DBT_MODEL_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||||
ONBOARDING: '/onboarding',
|
ONBOARDING: '/onboarding',
|
||||||
INGESTION: '/ingestion',
|
INGESTION: '/ingestion',
|
||||||
};
|
};
|
||||||
@ -238,6 +241,17 @@ export const getPipelineDetailsPath = (pipelineFQN: string, tab?: string) => {
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDBTModelDetailsPath = (dbtModelFQN: string, tab?: string) => {
|
||||||
|
let path = tab ? ROUTES.DBT_MODEL_DETAILS_WITH_TAB : ROUTES.DBT_MODEL_DETAILS;
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_DBT_MODEL_FQN, dbtModelFQN);
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
path = path.replace(PLACEHOLDER_ROUTE_TAB, tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
export const getTeamDetailsPath = (teamName: string) => {
|
export const getTeamDetailsPath = (teamName: string) => {
|
||||||
let path = ROUTES.TEAM_DETAILS;
|
let path = ROUTES.TEAM_DETAILS;
|
||||||
path = path.replace(PLACEHOLDER_ROUTE_TEAM, teamName);
|
path = path.replace(PLACEHOLDER_ROUTE_TEAM, teamName);
|
||||||
|
|||||||
@ -104,6 +104,10 @@ export const getCurrentIndex = (tab: string) => {
|
|||||||
case 'pipelines':
|
case 'pipelines':
|
||||||
currentIndex = SearchIndex.PIPELINE;
|
currentIndex = SearchIndex.PIPELINE;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'dbt_model':
|
||||||
|
currentIndex = SearchIndex.DBT_MODEL;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tables':
|
case 'tables':
|
||||||
@ -132,6 +136,11 @@ export const getCurrentTab = (tab: string) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'dbt_model':
|
||||||
|
currentTab = 5;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case 'tables':
|
case 'tables':
|
||||||
default:
|
default:
|
||||||
currentTab = 1;
|
currentTab = 1;
|
||||||
@ -179,4 +188,13 @@ export const tabsInfo = [
|
|||||||
path: 'pipelines',
|
path: 'pipelines',
|
||||||
icon: Icons.PIPELINE_GREY,
|
icon: Icons.PIPELINE_GREY,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'DBT Model',
|
||||||
|
index: SearchIndex.DBT_MODEL,
|
||||||
|
sortingFields: topicSortingFields,
|
||||||
|
sortField: '',
|
||||||
|
tab: 5,
|
||||||
|
path: 'dbt_model',
|
||||||
|
icon: Icons.PIPELINE_GREY,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum CSMode {
|
||||||
|
JAVASCRIPT = 'javascript',
|
||||||
|
SQL = 'sql',
|
||||||
|
}
|
||||||
@ -21,4 +21,5 @@ export enum EntityType {
|
|||||||
TOPIC = 'topic',
|
TOPIC = 'topic',
|
||||||
DASHBOARD = 'dashboard',
|
DASHBOARD = 'dashboard',
|
||||||
PIPELINE = 'pipeline',
|
PIPELINE = 'pipeline',
|
||||||
|
DBT_MODEL = 'dbt_model',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,4 +26,5 @@ export enum SearchIndex {
|
|||||||
TOPIC = 'topic_search_index',
|
TOPIC = 'topic_search_index',
|
||||||
DASHBOARD = 'dashboard_search_index',
|
DASHBOARD = 'dashboard_search_index',
|
||||||
PIPELINE = 'pipeline_search_index',
|
PIPELINE = 'pipeline_search_index',
|
||||||
|
DBT_MODEL = 'dbt_model_search_index',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -388,7 +388,7 @@ declare module 'Models' {
|
|||||||
// topic interface end
|
// topic interface end
|
||||||
|
|
||||||
interface RecentlyViewedData {
|
interface RecentlyViewedData {
|
||||||
entityType: 'dataset' | 'topic' | 'dashboard' | 'pipeline';
|
entityType: 'dataset' | 'topic' | 'dashboard' | 'pipeline' | 'dbt_model';
|
||||||
fqn: string;
|
fqn: string;
|
||||||
serviceType?: string;
|
serviceType?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|||||||
@ -0,0 +1,259 @@
|
|||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { EntityTags } from 'Models';
|
||||||
|
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import AppState from '../../AppState';
|
||||||
|
import { getDatabase } from '../../axiosAPIs/databaseAPI';
|
||||||
|
import {
|
||||||
|
addFollower,
|
||||||
|
getDBTModelDetailsByFQN,
|
||||||
|
patchDBTModelDetails,
|
||||||
|
removeFollower,
|
||||||
|
} from '../../axiosAPIs/dbtModelAPI';
|
||||||
|
import { getServiceById } from '../../axiosAPIs/serviceAPI';
|
||||||
|
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||||
|
import DBTModelDetails from '../../components/DBTModelDetails/DBTModelDetails.component';
|
||||||
|
import Loader from '../../components/Loader/Loader';
|
||||||
|
import {
|
||||||
|
getDatabaseDetailsPath,
|
||||||
|
getDBTModelDetailsPath,
|
||||||
|
getServiceDetailsPath,
|
||||||
|
} from '../../constants/constants';
|
||||||
|
import { EntityType } from '../../enums/entity.enum';
|
||||||
|
import { ServiceCategory } from '../../enums/service.enum';
|
||||||
|
import { Dbtmodel } from '../../generated/entity/data/dbtmodel';
|
||||||
|
import { User } from '../../generated/entity/teams/user';
|
||||||
|
import { addToRecentViewed, getCurrentUserId } from '../../utils/CommonUtils';
|
||||||
|
import {
|
||||||
|
dbtModelTabs,
|
||||||
|
getCurrentDBTModelTab,
|
||||||
|
} from '../../utils/DBTModelDetailsUtils';
|
||||||
|
import { serviceTypeLogo } from '../../utils/ServiceUtils';
|
||||||
|
import { getOwnerFromId, getTierFromTableTags } from '../../utils/TableUtils';
|
||||||
|
import { getTableTags } from '../../utils/TagsUtils';
|
||||||
|
|
||||||
|
const DBTModelDetailsPage: FunctionComponent = () => {
|
||||||
|
const history = useHistory();
|
||||||
|
const USERId = getCurrentUserId();
|
||||||
|
const { dbtModelFQN: dbtModelFQN, tab } = useParams() as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [activeTab, setActiveTab] = useState<number>(
|
||||||
|
getCurrentDBTModelTab(tab)
|
||||||
|
);
|
||||||
|
const [dbtModelDetails, setDbtModelDetails] = useState<Dbtmodel>(
|
||||||
|
{} as Dbtmodel
|
||||||
|
);
|
||||||
|
const [, setCurrentVersion] = useState<string>();
|
||||||
|
|
||||||
|
const [dbtModelId, setDbtModelId] = useState('');
|
||||||
|
const [tier, setTier] = useState<string>();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [followers, setFollowers] = useState<Array<User>>([]);
|
||||||
|
const [slashedDBTModelName, setSlashedDBTModelName] = useState<
|
||||||
|
TitleBreadcrumbProps['titleLinks']
|
||||||
|
>([]);
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [columns, setColumns] = useState<Dbtmodel['columns']>([]);
|
||||||
|
const [dbtModelTags, setDBTModelTags] = useState<Array<EntityTags>>([]);
|
||||||
|
const [dbtViewDefinition, setDbtViewDefinition] =
|
||||||
|
useState<Dbtmodel['viewDefinition']>('');
|
||||||
|
const [owner, setOwner] = useState<
|
||||||
|
Dbtmodel['owner'] & { displayName?: string }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const activeTabHandler = (tabValue: number) => {
|
||||||
|
const currentTabIndex = tabValue - 1;
|
||||||
|
if (dbtModelTabs[currentTabIndex].path !== tab) {
|
||||||
|
setActiveTab(getCurrentDBTModelTab(dbtModelTabs[currentTabIndex].path));
|
||||||
|
history.push({
|
||||||
|
pathname: getDBTModelDetailsPath(
|
||||||
|
dbtModelFQN,
|
||||||
|
dbtModelTabs[currentTabIndex].path
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveUpdatedDBTModelData = (
|
||||||
|
updatedData: Dbtmodel
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const jsonPatch = compare(dbtModelDetails, updatedData);
|
||||||
|
|
||||||
|
return patchDBTModelDetails(
|
||||||
|
dbtModelId,
|
||||||
|
jsonPatch
|
||||||
|
) as unknown as Promise<AxiosResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const descriptionUpdateHandler = (updatedDBTModel: Dbtmodel) => {
|
||||||
|
saveUpdatedDBTModelData(updatedDBTModel).then((res: AxiosResponse) => {
|
||||||
|
const { description, version } = res.data;
|
||||||
|
setCurrentVersion(version);
|
||||||
|
setDbtModelDetails(res.data);
|
||||||
|
setDescription(description);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnsUpdateHandler = (updatedDBTModel: Dbtmodel) => {
|
||||||
|
saveUpdatedDBTModelData(updatedDBTModel).then((res: AxiosResponse) => {
|
||||||
|
const { columns, version } = res.data;
|
||||||
|
setCurrentVersion(version);
|
||||||
|
setDbtModelDetails(res.data);
|
||||||
|
setColumns(columns);
|
||||||
|
setDBTModelTags(getTableTags(columns || []));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsUpdateHandler = (updatedDBTModel: Dbtmodel): Promise<void> => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
saveUpdatedDBTModelData(updatedDBTModel)
|
||||||
|
.then((res) => {
|
||||||
|
const { version, owner, tags } = res.data;
|
||||||
|
setCurrentVersion(version);
|
||||||
|
setDbtModelDetails(res.data);
|
||||||
|
setOwner(getOwnerFromId(owner?.id));
|
||||||
|
setTier(getTierFromTableTags(tags));
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(() => reject());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const followDBTModel = () => {
|
||||||
|
addFollower(dbtModelId, USERId).then((res: AxiosResponse) => {
|
||||||
|
const { newValue } = res.data.changeDescription.fieldsAdded[0];
|
||||||
|
|
||||||
|
setFollowers([...followers, ...newValue]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const unfollowDBTModel = () => {
|
||||||
|
removeFollower(dbtModelId, USERId).then((res: AxiosResponse) => {
|
||||||
|
const { oldValue } = res.data.changeDescription.fieldsDeleted[0];
|
||||||
|
|
||||||
|
setFollowers(
|
||||||
|
followers.filter((follower) => follower.id !== oldValue[0].id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dbtModelTabs[activeTab - 1].path !== tab) {
|
||||||
|
setActiveTab(getCurrentDBTModelTab(tab));
|
||||||
|
}
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
getDBTModelDetailsByFQN(
|
||||||
|
dbtModelFQN,
|
||||||
|
'columns,owner,database,tags,followers,viewDefinition'
|
||||||
|
)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
const {
|
||||||
|
description,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
columns,
|
||||||
|
database,
|
||||||
|
owner,
|
||||||
|
followers,
|
||||||
|
fullyQualifiedName,
|
||||||
|
version,
|
||||||
|
viewDefinition,
|
||||||
|
tags,
|
||||||
|
} = res.data;
|
||||||
|
setDbtModelDetails(res.data);
|
||||||
|
setDbtModelId(id);
|
||||||
|
setCurrentVersion(version);
|
||||||
|
setOwner(getOwnerFromId(owner?.id));
|
||||||
|
setTier(getTierFromTableTags(tags));
|
||||||
|
setFollowers(followers);
|
||||||
|
getDatabase(database.id, 'service').then((resDB: AxiosResponse) => {
|
||||||
|
getServiceById('databaseServices', resDB.data.service?.id).then(
|
||||||
|
(resService: AxiosResponse) => {
|
||||||
|
setSlashedDBTModelName([
|
||||||
|
{
|
||||||
|
name: resService.data.name,
|
||||||
|
url: resService.data.name
|
||||||
|
? getServiceDetailsPath(
|
||||||
|
resService.data.name,
|
||||||
|
resService.data.serviceType,
|
||||||
|
ServiceCategory.DATABASE_SERVICES
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
imgSrc: resService.data.serviceType
|
||||||
|
? serviceTypeLogo(resService.data.serviceType)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: resDB.data.name,
|
||||||
|
url: getDatabaseDetailsPath(resDB.data.fullyQualifiedName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
url: '',
|
||||||
|
activeTitle: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
addToRecentViewed({
|
||||||
|
entityType: EntityType.DBT_MODEL,
|
||||||
|
fqn: fullyQualifiedName,
|
||||||
|
serviceType: resService.data.serviceType,
|
||||||
|
timestamp: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setName(name);
|
||||||
|
|
||||||
|
setDescription(description);
|
||||||
|
setColumns(columns || []);
|
||||||
|
setDBTModelTags(getTableTags(columns || []));
|
||||||
|
setDbtViewDefinition(viewDefinition);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
setActiveTab(getCurrentDBTModelTab(tab));
|
||||||
|
}, [dbtModelFQN]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<DBTModelDetails
|
||||||
|
activeTab={activeTab}
|
||||||
|
columns={columns}
|
||||||
|
columnsUpdateHandler={columnsUpdateHandler}
|
||||||
|
dbtModelDetails={dbtModelDetails}
|
||||||
|
dbtModelFQN={dbtModelFQN}
|
||||||
|
dbtModelTags={dbtModelTags}
|
||||||
|
description={description}
|
||||||
|
descriptionUpdateHandler={descriptionUpdateHandler}
|
||||||
|
entityName={name}
|
||||||
|
followDBTModelHandler={followDBTModel}
|
||||||
|
followers={followers}
|
||||||
|
owner={owner as Dbtmodel['owner'] & { displayName: string }}
|
||||||
|
setActiveTabHandler={activeTabHandler}
|
||||||
|
settingsUpdateHandler={settingsUpdateHandler}
|
||||||
|
slashedDBTModelName={slashedDBTModelName}
|
||||||
|
tier={tier as string}
|
||||||
|
unfollowDBTModelHandler={unfollowDBTModel}
|
||||||
|
users={AppState.users}
|
||||||
|
viewDefinition={dbtViewDefinition}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(DBTModelDetailsPage);
|
||||||
@ -40,8 +40,8 @@ import {
|
|||||||
ZERO_SIZE,
|
ZERO_SIZE,
|
||||||
} from '../../constants/explore.constants';
|
} from '../../constants/explore.constants';
|
||||||
import { SearchIndex } from '../../enums/search.enum';
|
import { SearchIndex } from '../../enums/search.enum';
|
||||||
|
import { getTotalEntityCountByType } from '../../utils/EntityUtils';
|
||||||
import { getFilterString } from '../../utils/FilterUtils';
|
import { getFilterString } from '../../utils/FilterUtils';
|
||||||
import { getTotalEntityCountByService } from '../../utils/ServiceUtils';
|
|
||||||
|
|
||||||
const ExplorePage: FunctionComponent = () => {
|
const ExplorePage: FunctionComponent = () => {
|
||||||
const initialFilter = getFilterString(
|
const initialFilter = getFilterString(
|
||||||
@ -58,6 +58,7 @@ const ExplorePage: FunctionComponent = () => {
|
|||||||
const [topicCount, setTopicCount] = useState<number>(0);
|
const [topicCount, setTopicCount] = useState<number>(0);
|
||||||
const [dashboardCount, setDashboardCount] = useState<number>(0);
|
const [dashboardCount, setDashboardCount] = useState<number>(0);
|
||||||
const [pipelineCount, setPipelineCount] = useState<number>(0);
|
const [pipelineCount, setPipelineCount] = useState<number>(0);
|
||||||
|
const [dbtModelCount, setDbtModelCount] = useState<number>(0);
|
||||||
const [searchResult, setSearchResult] = useState<ExploreSearchData>();
|
const [searchResult, setSearchResult] = useState<ExploreSearchData>();
|
||||||
const [initialSortField] = useState<string>(
|
const [initialSortField] = useState<string>(
|
||||||
searchQuery
|
searchQuery
|
||||||
@ -85,6 +86,10 @@ const ExplorePage: FunctionComponent = () => {
|
|||||||
setPipelineCount(count);
|
setPipelineCount(count);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDbtModelCount = (count: number) => {
|
||||||
|
setDbtModelCount(count);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePathChange = (path: string) => {
|
const handlePathChange = (path: string) => {
|
||||||
AppState.explorePageTab = path;
|
AppState.explorePageTab = path;
|
||||||
};
|
};
|
||||||
@ -95,6 +100,7 @@ const ExplorePage: FunctionComponent = () => {
|
|||||||
SearchIndex.TOPIC,
|
SearchIndex.TOPIC,
|
||||||
SearchIndex.DASHBOARD,
|
SearchIndex.DASHBOARD,
|
||||||
SearchIndex.PIPELINE,
|
SearchIndex.PIPELINE,
|
||||||
|
SearchIndex.DBT_MODEL,
|
||||||
];
|
];
|
||||||
|
|
||||||
const entityCounts = entities.map((entity) =>
|
const entityCounts = entities.map((entity) =>
|
||||||
@ -116,35 +122,44 @@ const ExplorePage: FunctionComponent = () => {
|
|||||||
topic,
|
topic,
|
||||||
dashboard,
|
dashboard,
|
||||||
pipeline,
|
pipeline,
|
||||||
|
dbtModel,
|
||||||
]: PromiseSettledResult<SearchResponse>[]) => {
|
]: PromiseSettledResult<SearchResponse>[]) => {
|
||||||
setTableCount(
|
setTableCount(
|
||||||
table.status === 'fulfilled'
|
table.status === 'fulfilled'
|
||||||
? getTotalEntityCountByService(
|
? getTotalEntityCountByType(
|
||||||
table.value.data.aggregations?.['sterms#Service']
|
table.value.data.aggregations?.['sterms#EntityType']
|
||||||
?.buckets as Bucket[]
|
?.buckets as Bucket[]
|
||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
setTopicCount(
|
setTopicCount(
|
||||||
topic.status === 'fulfilled'
|
topic.status === 'fulfilled'
|
||||||
? getTotalEntityCountByService(
|
? getTotalEntityCountByType(
|
||||||
topic.value.data.aggregations?.['sterms#Service']
|
topic.value.data.aggregations?.['sterms#EntityType']
|
||||||
?.buckets as Bucket[]
|
?.buckets as Bucket[]
|
||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
setDashboardCount(
|
setDashboardCount(
|
||||||
dashboard.status === 'fulfilled'
|
dashboard.status === 'fulfilled'
|
||||||
? getTotalEntityCountByService(
|
? getTotalEntityCountByType(
|
||||||
dashboard.value.data.aggregations?.['sterms#Service']
|
dashboard.value.data.aggregations?.['sterms#EntityType']
|
||||||
?.buckets as Bucket[]
|
?.buckets as Bucket[]
|
||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
setPipelineCount(
|
setPipelineCount(
|
||||||
pipeline.status === 'fulfilled'
|
pipeline.status === 'fulfilled'
|
||||||
? getTotalEntityCountByService(
|
? getTotalEntityCountByType(
|
||||||
pipeline.value.data.aggregations?.['sterms#Service']
|
pipeline.value.data.aggregations?.['sterms#EntityType']
|
||||||
|
?.buckets as Bucket[]
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
setDbtModelCount(
|
||||||
|
dbtModel.status === 'fulfilled'
|
||||||
|
? getTotalEntityCountByType(
|
||||||
|
dbtModel.value.data.aggregations?.['sterms#EntityType']
|
||||||
?.buckets as Bucket[]
|
?.buckets as Bucket[]
|
||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
@ -258,8 +273,10 @@ const ExplorePage: FunctionComponent = () => {
|
|||||||
topic: topicCount,
|
topic: topicCount,
|
||||||
dashboard: dashboardCount,
|
dashboard: dashboardCount,
|
||||||
pipeline: pipelineCount,
|
pipeline: pipelineCount,
|
||||||
|
dbtModel: dbtModelCount,
|
||||||
}}
|
}}
|
||||||
updateDashboardCount={handleDashboardCount}
|
updateDashboardCount={handleDashboardCount}
|
||||||
|
updateDbtModelCount={handleDbtModelCount}
|
||||||
updatePipelineCount={handlePipelineCount}
|
updatePipelineCount={handlePipelineCount}
|
||||||
updateTableCount={handleTableCount}
|
updateTableCount={handleTableCount}
|
||||||
updateTopicCount={handleTopicCount}
|
updateTopicCount={handleTopicCount}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { ROUTES } from '../constants/constants';
|
|||||||
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
||||||
import DatabaseDetails from '../pages/database-details/index';
|
import DatabaseDetails from '../pages/database-details/index';
|
||||||
import DatasetDetailsPage from '../pages/DatasetDetailsPage/DatasetDetailsPage.component';
|
import DatasetDetailsPage from '../pages/DatasetDetailsPage/DatasetDetailsPage.component';
|
||||||
|
import DBTModelDetailsPage from '../pages/DBTModelDetailsPage/DBTModelDetailsPage.component';
|
||||||
import EntityVersionPage from '../pages/EntityVersionPage/EntityVersionPage.component';
|
import EntityVersionPage from '../pages/EntityVersionPage/EntityVersionPage.component';
|
||||||
import ExplorePage from '../pages/explore/ExplorePage.component';
|
import ExplorePage from '../pages/explore/ExplorePage.component';
|
||||||
import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
|
import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
|
||||||
@ -105,6 +106,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
component={PipelineDetailsPage}
|
component={PipelineDetailsPage}
|
||||||
path={ROUTES.PIPELINE_DETAILS_WITH_TAB}
|
path={ROUTES.PIPELINE_DETAILS_WITH_TAB}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
component={DBTModelDetailsPage}
|
||||||
|
path={ROUTES.DBT_MODEL_DETAILS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
component={DBTModelDetailsPage}
|
||||||
|
path={ROUTES.DBT_MODEL_DETAILS_WITH_TAB}
|
||||||
|
/>
|
||||||
<Route component={Onboarding} path={ROUTES.ONBOARDING} />
|
<Route component={Onboarding} path={ROUTES.ONBOARDING} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
|||||||
@ -4,3 +4,8 @@
|
|||||||
.cm-string.cm-property {
|
.cm-string.cm-property {
|
||||||
color: #450de2 !important;
|
color: #450de2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-h-full .react-codemirror2,
|
||||||
|
.cm-h-full .react-codemirror2 > .CodeMirror {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,7 +16,8 @@ export const formatDataResponse = (hits) => {
|
|||||||
hit._source.table_name ||
|
hit._source.table_name ||
|
||||||
hit._source.topic_name ||
|
hit._source.topic_name ||
|
||||||
hit._source.dashboard_name ||
|
hit._source.dashboard_name ||
|
||||||
hit._source.pipeline_name;
|
hit._source.pipeline_name ||
|
||||||
|
hit._source.dbt_model_name;
|
||||||
newData.description = hit._source.description;
|
newData.description = hit._source.description;
|
||||||
newData.fullyQualifiedName = hit._source.fqdn;
|
newData.fullyQualifiedName = hit._source.fqdn;
|
||||||
newData.tableType = hit._source.table_type;
|
newData.tableType = hit._source.table_type;
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
export const dbtModelTabs = [
|
||||||
|
{
|
||||||
|
name: 'Schema',
|
||||||
|
path: 'schema',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View Definition',
|
||||||
|
path: 'view_definition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manage',
|
||||||
|
path: 'manage',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getCurrentDBTModelTab = (tab: string): number => {
|
||||||
|
let currentTab;
|
||||||
|
switch (tab) {
|
||||||
|
case 'view_definition':
|
||||||
|
currentTab = 2;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'manage':
|
||||||
|
currentTab = 3;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'schema':
|
||||||
|
default:
|
||||||
|
currentTab = 1;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentTab;
|
||||||
|
};
|
||||||
@ -247,6 +247,15 @@ export const getEntityCountByType = (buckets: Array<Bucket>) => {
|
|||||||
return entityCounts;
|
return entityCounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTotalEntityCountByType = (buckets: Array<Bucket> = []) => {
|
||||||
|
let entityCounts = 0;
|
||||||
|
buckets.forEach((bucket) => {
|
||||||
|
entityCounts += bucket.doc_count;
|
||||||
|
});
|
||||||
|
|
||||||
|
return entityCounts;
|
||||||
|
};
|
||||||
|
|
||||||
export const getEntityLineage = (
|
export const getEntityLineage = (
|
||||||
oldVal: EntityLineage,
|
oldVal: EntityLineage,
|
||||||
newVal: EntityLineage,
|
newVal: EntityLineage,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import PopOver from '../components/common/popover/PopOver';
|
|||||||
import {
|
import {
|
||||||
getDashboardDetailsPath,
|
getDashboardDetailsPath,
|
||||||
getDatasetDetailsPath,
|
getDatasetDetailsPath,
|
||||||
|
getDBTModelDetailsPath,
|
||||||
getPipelineDetailsPath,
|
getPipelineDetailsPath,
|
||||||
getTopicDetailsPath,
|
getTopicDetailsPath,
|
||||||
} from '../constants/constants';
|
} from '../constants/constants';
|
||||||
@ -176,6 +177,10 @@ export const getEntityLink = (
|
|||||||
case EntityType.PIPELINE:
|
case EntityType.PIPELINE:
|
||||||
return getPipelineDetailsPath(fullyQualifiedName);
|
return getPipelineDetailsPath(fullyQualifiedName);
|
||||||
|
|
||||||
|
case SearchIndex.DBT_MODEL:
|
||||||
|
case EntityType.DBT_MODEL:
|
||||||
|
return getDBTModelDetailsPath(fullyQualifiedName);
|
||||||
|
|
||||||
case SearchIndex.TABLE:
|
case SearchIndex.TABLE:
|
||||||
case EntityType.TABLE:
|
case EntityType.TABLE:
|
||||||
default:
|
default:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user