mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-11-04 04:29:13 +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,
 | 
			
		||||
  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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,7 @@ const ASSETS_NAME = [
 | 
			
		||||
  'topic_name',
 | 
			
		||||
  'dashboard_name',
 | 
			
		||||
  'pipeline_name',
 | 
			
		||||
  'dbt_model_name',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const SearchedData: React.FC<SearchedDataProp> = ({
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
  DASHBOARD = 'dashboard',
 | 
			
		||||
  PIPELINE = 'pipeline',
 | 
			
		||||
  DBT_MODEL = 'dbt_model',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
} 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}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -4,3 +4,8 @@
 | 
			
		||||
.cm-string.cm-property {
 | 
			
		||||
  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.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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user