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:
darth-coder00 2021-11-26 23:27:11 +05:30 committed by GitHub
parent fdd6ca14b7
commit dc1576cd91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 905 additions and 33 deletions

View File

@ -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
);
};

View File

@ -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;

View File

@ -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;
}

View File

@ -73,6 +73,7 @@ const Explore: React.FC<ExploreProps> = ({
updateTableCount,
updateTopicCount,
updateDashboardCount,
updateDbtModelCount,
updatePipelineCount,
}: ExploreProps) => {
const location = useLocation();
@ -197,6 +198,10 @@ const Explore: React.FC<ExploreProps> = ({
case SearchIndex.PIPELINE:
updatePipelineCount(count);
break;
case SearchIndex.DBT_MODEL:
updateDbtModelCount(count);
break;
default:
break;
@ -339,6 +344,8 @@ const Explore: React.FC<ExploreProps> = ({
return getCountBadge(tabCounts.dashboard);
case SearchIndex.PIPELINE:
return getCountBadge(tabCounts.pipeline);
case SearchIndex.DBT_MODEL:
return getCountBadge(tabCounts.dbtModel);
default:
return getCountBadge();
}

View File

@ -68,8 +68,10 @@ describe('Test Explore component', () => {
topic: 2,
dashboard: 8,
pipeline: 5,
dbtModel: 7,
}}
updateDashboardCount={mockFunction}
updateDbtModelCount={mockFunction}
updatePipelineCount={mockFunction}
updateTableCount={mockFunction}
updateTopicCount={mockFunction}

View File

@ -35,6 +35,7 @@ export interface ExploreProps {
topic: number;
dashboard: number;
pipeline: number;
dbtModel: number;
};
searchText: string;
sortValue: string;
@ -48,6 +49,7 @@ export interface ExploreProps {
updateTopicCount: (count: number) => void;
updateDashboardCount: (count: number) => void;
updatePipelineCount: (count: number) => void;
updateDbtModelCount: (count: number) => void;
fetchData: (value: SearchDataFunctionType[]) => void;
searchResult: ExploreSearchData | undefined;
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { isUndefined, lowerCase } from 'lodash';
import { isNil, isUndefined, lowerCase } from 'lodash';
import { DatasetSchemaTableTab } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router';
@ -121,7 +121,7 @@ const SchemaTab: FunctionComponent<Props> = ({
/>
)}
</div>
{!isReadOnly ? (
{!isReadOnly && !isNil(sampleData) ? (
<div className="tw-col-span-2 tw-text-right tw-mb-4">
<div
className="tw-w-60 tw-inline-flex tw-border tw-border-main

View File

@ -55,9 +55,18 @@ type PipelineSource = {
pipeline_name: string;
} & CommonSource;
type DBTModelSource = {
dbt_model_id: string;
dbt_model_name: string;
} & CommonSource;
type Option = {
_index: string;
_source: TableSource & DashboardSource & TopicSource & PipelineSource;
_source: TableSource &
DashboardSource &
TopicSource &
PipelineSource &
DBTModelSource;
};
const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
@ -67,10 +76,13 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
const [dashboardSuggestions, setDashboardSuggestions] = useState<
DashboardSource[]
>([]);
const [pipelineSuggestions, setPipelineSuggestions] = useState<
PipelineSource[]
>([]);
const [DBTModelSuggestions, setDBTModelSuggestions] = useState<
DBTModelSource[]
>([]);
const isMounting = useRef(true);
const setSuggestions = (options: Array<Option>) => {
@ -94,6 +106,11 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
.filter((option) => option._index === SearchIndex.PIPELINE)
.map((option) => option._source)
);
setDBTModelSuggestions(
options
.filter((option) => option._index === SearchIndex.DBT_MODEL)
.map((option) => option._source)
);
};
const getGroupLabel = (index: string) => {
@ -114,6 +131,11 @@ const Suggestions = ({ searchText, isOpen, setIsOpen }: SuggestionProp) => {
label = 'Pipelines';
icon = Icons.PIPELINE_GREY;
break;
case SearchIndex.DBT_MODEL:
label = 'DBT Models';
icon = Icons.TABLE_GREY;
break;
case SearchIndex.TABLE:
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>
);
};

View File

@ -7,30 +7,44 @@ import 'codemirror/addon/fold/foldgutter.js';
import 'codemirror/addon/selection/active-line';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/sql/sql';
import React, { useState } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { JSON_TAB_SIZE } from '../../constants/constants';
import { CSMode } from '../../enums/codemirror.enum';
import { getSchemaEditorValue } from './SchemaEditor.utils';
const options = {
tabSize: JSON_TAB_SIZE,
indentUnit: JSON_TAB_SIZE,
indentWithTabs: false,
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
autoCloseBrackets: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
mode: {
name: 'javascript',
json: true,
},
readOnly: true,
type Mode = {
name: CSMode;
json?: boolean;
};
const SchemaEditor = ({ value }: { value: string }) => {
const SchemaEditor = ({
value,
className = '',
mode = {
name: CSMode.JAVASCRIPT,
json: true,
},
}: {
value: string;
className?: string;
mode?: Mode;
}) => {
const options = {
tabSize: JSON_TAB_SIZE,
indentUnit: JSON_TAB_SIZE,
indentWithTabs: false,
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
autoCloseBrackets: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
mode,
readOnly: true,
};
const [internalValue, setInternalValue] = useState(
getSchemaEditorValue(value)
);
@ -43,7 +57,7 @@ const SchemaEditor = ({ value }: { value: string }) => {
};
return (
<div>
<div className={className}>
<CodeMirror
options={options}
value={internalValue}

View File

@ -49,6 +49,7 @@ const ASSETS_NAME = [
'topic_name',
'dashboard_name',
'pipeline_name',
'dbt_model_name',
];
const SearchedData: React.FC<SearchedDataProp> = ({

View File

@ -37,6 +37,7 @@ export const ERROR500 = 'Something went wrong';
const PLACEHOLDER_ROUTE_DATASET_FQN = ':datasetFQN';
const PLACEHOLDER_ROUTE_TOPIC_FQN = ':topicFQN';
const PLACEHOLDER_ROUTE_PIPELINE_FQN = ':pipelineFQN';
const PLACEHOLDER_ROUTE_DBT_MODEL_FQN = ':dbtModelFQN';
const PLACEHOLDER_ROUTE_DASHBOARD_FQN = ':dashboardFQN';
const PLACEHOLDER_ROUTE_DATABASE_FQN = ':databaseFQN';
const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN';
@ -139,6 +140,8 @@ export const ROUTES = {
DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`,
PIPELINE_DETAILS: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}`,
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',
INGESTION: '/ingestion',
};
@ -238,6 +241,17 @@ export const getPipelineDetailsPath = (pipelineFQN: string, tab?: string) => {
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) => {
let path = ROUTES.TEAM_DETAILS;
path = path.replace(PLACEHOLDER_ROUTE_TEAM, teamName);

View File

@ -104,6 +104,10 @@ export const getCurrentIndex = (tab: string) => {
case 'pipelines':
currentIndex = SearchIndex.PIPELINE;
break;
case 'dbt_model':
currentIndex = SearchIndex.DBT_MODEL;
break;
case 'tables':
@ -132,6 +136,11 @@ export const getCurrentTab = (tab: string) => {
break;
case 'dbt_model':
currentTab = 5;
break;
case 'tables':
default:
currentTab = 1;
@ -179,4 +188,13 @@ export const tabsInfo = [
path: 'pipelines',
icon: Icons.PIPELINE_GREY,
},
{
label: 'DBT Model',
index: SearchIndex.DBT_MODEL,
sortingFields: topicSortingFields,
sortField: '',
tab: 5,
path: 'dbt_model',
icon: Icons.PIPELINE_GREY,
},
];

View File

@ -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',
}

View File

@ -21,4 +21,5 @@ export enum EntityType {
TOPIC = 'topic',
DASHBOARD = 'dashboard',
PIPELINE = 'pipeline',
DBT_MODEL = 'dbt_model',
}

View File

@ -26,4 +26,5 @@ export enum SearchIndex {
TOPIC = 'topic_search_index',
DASHBOARD = 'dashboard_search_index',
PIPELINE = 'pipeline_search_index',
DBT_MODEL = 'dbt_model_search_index',
}

View File

@ -388,7 +388,7 @@ declare module 'Models' {
// topic interface end
interface RecentlyViewedData {
entityType: 'dataset' | 'topic' | 'dashboard' | 'pipeline';
entityType: 'dataset' | 'topic' | 'dashboard' | 'pipeline' | 'dbt_model';
fqn: string;
serviceType?: string;
timestamp: number;

View File

@ -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);

View File

@ -40,8 +40,8 @@ import {
ZERO_SIZE,
} from '../../constants/explore.constants';
import { SearchIndex } from '../../enums/search.enum';
import { getTotalEntityCountByType } from '../../utils/EntityUtils';
import { getFilterString } from '../../utils/FilterUtils';
import { getTotalEntityCountByService } from '../../utils/ServiceUtils';
const ExplorePage: FunctionComponent = () => {
const initialFilter = getFilterString(
@ -58,6 +58,7 @@ const ExplorePage: FunctionComponent = () => {
const [topicCount, setTopicCount] = useState<number>(0);
const [dashboardCount, setDashboardCount] = useState<number>(0);
const [pipelineCount, setPipelineCount] = useState<number>(0);
const [dbtModelCount, setDbtModelCount] = useState<number>(0);
const [searchResult, setSearchResult] = useState<ExploreSearchData>();
const [initialSortField] = useState<string>(
searchQuery
@ -85,6 +86,10 @@ const ExplorePage: FunctionComponent = () => {
setPipelineCount(count);
};
const handleDbtModelCount = (count: number) => {
setDbtModelCount(count);
};
const handlePathChange = (path: string) => {
AppState.explorePageTab = path;
};
@ -95,6 +100,7 @@ const ExplorePage: FunctionComponent = () => {
SearchIndex.TOPIC,
SearchIndex.DASHBOARD,
SearchIndex.PIPELINE,
SearchIndex.DBT_MODEL,
];
const entityCounts = entities.map((entity) =>
@ -116,35 +122,44 @@ const ExplorePage: FunctionComponent = () => {
topic,
dashboard,
pipeline,
dbtModel,
]: PromiseSettledResult<SearchResponse>[]) => {
setTableCount(
table.status === 'fulfilled'
? getTotalEntityCountByService(
table.value.data.aggregations?.['sterms#Service']
? getTotalEntityCountByType(
table.value.data.aggregations?.['sterms#EntityType']
?.buckets as Bucket[]
)
: 0
);
setTopicCount(
topic.status === 'fulfilled'
? getTotalEntityCountByService(
topic.value.data.aggregations?.['sterms#Service']
? getTotalEntityCountByType(
topic.value.data.aggregations?.['sterms#EntityType']
?.buckets as Bucket[]
)
: 0
);
setDashboardCount(
dashboard.status === 'fulfilled'
? getTotalEntityCountByService(
dashboard.value.data.aggregations?.['sterms#Service']
? getTotalEntityCountByType(
dashboard.value.data.aggregations?.['sterms#EntityType']
?.buckets as Bucket[]
)
: 0
);
setPipelineCount(
pipeline.status === 'fulfilled'
? getTotalEntityCountByService(
pipeline.value.data.aggregations?.['sterms#Service']
? getTotalEntityCountByType(
pipeline.value.data.aggregations?.['sterms#EntityType']
?.buckets as Bucket[]
)
: 0
);
setDbtModelCount(
dbtModel.status === 'fulfilled'
? getTotalEntityCountByType(
dbtModel.value.data.aggregations?.['sterms#EntityType']
?.buckets as Bucket[]
)
: 0
@ -258,8 +273,10 @@ const ExplorePage: FunctionComponent = () => {
topic: topicCount,
dashboard: dashboardCount,
pipeline: pipelineCount,
dbtModel: dbtModelCount,
}}
updateDashboardCount={handleDashboardCount}
updateDbtModelCount={handleDbtModelCount}
updatePipelineCount={handlePipelineCount}
updateTableCount={handleTableCount}
updateTopicCount={handleTopicCount}

View File

@ -24,6 +24,7 @@ import { ROUTES } from '../constants/constants';
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
import DatabaseDetails from '../pages/database-details/index';
import DatasetDetailsPage from '../pages/DatasetDetailsPage/DatasetDetailsPage.component';
import DBTModelDetailsPage from '../pages/DBTModelDetailsPage/DBTModelDetailsPage.component';
import EntityVersionPage from '../pages/EntityVersionPage/EntityVersionPage.component';
import ExplorePage from '../pages/explore/ExplorePage.component';
import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
@ -105,6 +106,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
component={PipelineDetailsPage}
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
exact

View File

@ -4,3 +4,8 @@
.cm-string.cm-property {
color: #450de2 !important;
}
.cm-h-full .react-codemirror2,
.cm-h-full .react-codemirror2 > .CodeMirror {
height: 100%;
}

View File

@ -16,7 +16,8 @@ export const formatDataResponse = (hits) => {
hit._source.table_name ||
hit._source.topic_name ||
hit._source.dashboard_name ||
hit._source.pipeline_name;
hit._source.pipeline_name ||
hit._source.dbt_model_name;
newData.description = hit._source.description;
newData.fullyQualifiedName = hit._source.fqdn;
newData.tableType = hit._source.table_type;

View File

@ -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;
};

View File

@ -247,6 +247,15 @@ export const getEntityCountByType = (buckets: Array<Bucket>) => {
return entityCounts;
};
export const getTotalEntityCountByType = (buckets: Array<Bucket> = []) => {
let entityCounts = 0;
buckets.forEach((bucket) => {
entityCounts += bucket.doc_count;
});
return entityCounts;
};
export const getEntityLineage = (
oldVal: EntityLineage,
newVal: EntityLineage,

View File

@ -5,6 +5,7 @@ import PopOver from '../components/common/popover/PopOver';
import {
getDashboardDetailsPath,
getDatasetDetailsPath,
getDBTModelDetailsPath,
getPipelineDetailsPath,
getTopicDetailsPath,
} from '../constants/constants';
@ -176,6 +177,10 @@ export const getEntityLink = (
case EntityType.PIPELINE:
return getPipelineDetailsPath(fullyQualifiedName);
case SearchIndex.DBT_MODEL:
case EntityType.DBT_MODEL:
return getDBTModelDetailsPath(fullyQualifiedName);
case SearchIndex.TABLE:
case EntityType.TABLE:
default: