mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 10:39:30 +00:00 
			
		
		
		
	Adding dashboard tab on explore page (#398)
* Adding dashboard tab on explore page * minor fix
This commit is contained in:
		
							parent
							
								
									072c2034ad
								
							
						
					
					
						commit
						1fa033798a
					
				| @ -0,0 +1,83 @@ | |||||||
|  | /* | ||||||
|  |   * 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 { Dashboard } from '../generated/entity/data/dashboard'; | ||||||
|  | import { getURLWithQueryFields } from '../utils/APIUtils'; | ||||||
|  | import APIClient from './index'; | ||||||
|  | 
 | ||||||
|  | export const getDashboards: Function = ( | ||||||
|  |   serviceName: string, | ||||||
|  |   paging: string, | ||||||
|  |   arrQueryFields: string | ||||||
|  | ): Promise<AxiosResponse> => { | ||||||
|  |   const url = `${getURLWithQueryFields( | ||||||
|  |     `/dashboards`, | ||||||
|  |     arrQueryFields | ||||||
|  |   )}&service=${serviceName}${paging ? paging : ''}`;
 | ||||||
|  | 
 | ||||||
|  |   return APIClient.get(url); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getDashboardByFqn: Function = ( | ||||||
|  |   fqn: string, | ||||||
|  |   arrQueryFields: string | ||||||
|  | ): Promise<AxiosResponse> => { | ||||||
|  |   const url = getURLWithQueryFields(`/dashboards/name/${fqn}`, arrQueryFields); | ||||||
|  | 
 | ||||||
|  |   return APIClient.get(url); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const addFollower: Function = ( | ||||||
|  |   dashboardID: string, | ||||||
|  |   userId: string | ||||||
|  | ): Promise<AxiosResponse> => { | ||||||
|  |   const configOptions = { | ||||||
|  |     headers: { 'Content-type': 'application/json' }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return APIClient.put( | ||||||
|  |     `/dashboards/${dashboardID}/followers`, | ||||||
|  |     userId, | ||||||
|  |     configOptions | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const removeFollower: Function = ( | ||||||
|  |   dashboardID: string, | ||||||
|  |   userId: string | ||||||
|  | ): Promise<AxiosResponse> => { | ||||||
|  |   const configOptions = { | ||||||
|  |     headers: { 'Content-type': 'application/json' }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return APIClient.delete( | ||||||
|  |     `/dashboards/${dashboardID}/followers/${userId}`, | ||||||
|  |     configOptions | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const patchDashboardDetails: Function = ( | ||||||
|  |   id: string, | ||||||
|  |   data: Dashboard | ||||||
|  | ): Promise<AxiosResponse> => { | ||||||
|  |   const configOptions = { | ||||||
|  |     headers: { 'Content-type': 'application/json-patch+json' }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return APIClient.patch(`/dashboards/${id}`, data, configOptions); | ||||||
|  | }; | ||||||
| @ -44,7 +44,9 @@ const ErrorPlaceHolderES = ({ type }: Props) => { | |||||||
|       </div> |       </div> | ||||||
|       <div className="tw-flex tw-flex-col tw-items-center tw-mt-10 tw-text-base tw-font-normal"> |       <div className="tw-flex tw-flex-col tw-items-center tw-mt-10 tw-text-base tw-font-normal"> | ||||||
|         <p className="tw-text-lg tw-font-bold tw-mb-1 tw-text-primary"> |         <p className="tw-text-lg tw-font-bold tw-mb-1 tw-text-primary"> | ||||||
|           {`Hi, ${AppState.userDetails.displayName}!`} |           {`Hi, ${ | ||||||
|  |             AppState.userDetails.displayName || AppState.users[0].displayName | ||||||
|  |           }!`}
 | ||||||
|         </p> |         </p> | ||||||
|         {type === 'noData' && noRecordForES()} |         {type === 'noData' && noRecordForES()} | ||||||
|         {type === 'error' && ( |         {type === 'error' && ( | ||||||
|  | |||||||
| @ -33,7 +33,6 @@ const FacetFilter: FunctionComponent<FacetProp> = ({ | |||||||
| }: FacetProp) => { | }: FacetProp) => { | ||||||
|   const [showAllTags, setShowAllTags] = useState<boolean>(false); |   const [showAllTags, setShowAllTags] = useState<boolean>(false); | ||||||
|   const [showAllServices, setShowAllServices] = useState<boolean>(false); |   const [showAllServices, setShowAllServices] = useState<boolean>(false); | ||||||
|   const [showAllClusters, setShowAllClusters] = useState<boolean>(false); |  | ||||||
|   const [showAllTier, setShowAllTier] = useState<boolean>(false); |   const [showAllTier, setShowAllTier] = useState<boolean>(false); | ||||||
|   const sortAggregations = () => { |   const sortAggregations = () => { | ||||||
|     return aggregations.sort((a, b) => |     return aggregations.sort((a, b) => | ||||||
| @ -74,8 +73,6 @@ const FacetFilter: FunctionComponent<FacetProp> = ({ | |||||||
|         return getLinkText(bucketLength, showAllServices, setShowAllServices); |         return getLinkText(bucketLength, showAllServices, setShowAllServices); | ||||||
|       case 'Tags': |       case 'Tags': | ||||||
|         return getLinkText(bucketLength, showAllTags, setShowAllTags); |         return getLinkText(bucketLength, showAllTags, setShowAllTags); | ||||||
|       case 'Service Type': |  | ||||||
|         return getLinkText(bucketLength, showAllClusters, setShowAllClusters); |  | ||||||
|       case 'Tier': |       case 'Tier': | ||||||
|         return getLinkText(bucketLength, showAllTier, setShowAllTier); |         return getLinkText(bucketLength, showAllTier, setShowAllTier); | ||||||
|       default: |       default: | ||||||
| @ -89,8 +86,6 @@ const FacetFilter: FunctionComponent<FacetProp> = ({ | |||||||
|         return getBuckets(buckets, showAllServices); |         return getBuckets(buckets, showAllServices); | ||||||
|       case 'Tags': |       case 'Tags': | ||||||
|         return getBuckets(buckets, showAllTags); |         return getBuckets(buckets, showAllTags); | ||||||
|       case 'Service Type': |  | ||||||
|         return getBuckets(buckets, showAllClusters); |  | ||||||
|       case 'Tier': |       case 'Tier': | ||||||
|         return getBuckets(buckets, showAllTier); |         return getBuckets(buckets, showAllTier); | ||||||
|       default: |       default: | ||||||
| @ -176,7 +171,7 @@ FacetFilter.propTypes = { | |||||||
|   onSelectHandler: PropTypes.func.isRequired, |   onSelectHandler: PropTypes.func.isRequired, | ||||||
|   filters: PropTypes.shape({ |   filters: PropTypes.shape({ | ||||||
|     tags: PropTypes.array.isRequired, |     tags: PropTypes.array.isRequired, | ||||||
|     'service type': PropTypes.array.isRequired, |     service: PropTypes.array.isRequired, | ||||||
|     tier: PropTypes.array.isRequired, |     tier: PropTypes.array.isRequired, | ||||||
|   }).isRequired, |   }).isRequired, | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -121,7 +121,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const getTagsContainer = (tag: ColumnTags, index: number) => { |   const getTagsContainer = (tag: ColumnTags, index: number) => { | ||||||
|     return ( |     return tag.tagFQN ? ( | ||||||
|       <Tags |       <Tags | ||||||
|         className="tw-bg-gray-200" |         className="tw-bg-gray-200" | ||||||
|         editable={editable} |         editable={editable} | ||||||
| @ -132,7 +132,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({ | |||||||
|         }} |         }} | ||||||
|         tag={`#${tag.tagFQN}`} |         tag={`#${tag.tagFQN}`} | ||||||
|       /> |       /> | ||||||
|     ); |     ) : null; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const getTagsElement = (tag: ColumnTags, index: number) => { |   const getTagsElement = (tag: ColumnTags, index: number) => { | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ export const ERROR404 = 'No data found'; | |||||||
| export const ERROR500 = 'Something went wrong'; | 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_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'; | ||||||
| const PLACEHOLDER_ROUTE_SERVICE_TYPE = ':serviceType'; | const PLACEHOLDER_ROUTE_SERVICE_TYPE = ':serviceType'; | ||||||
| @ -63,7 +64,7 @@ export const tableSortingFields = [ | |||||||
| 
 | 
 | ||||||
| export const topicSortingFields = [ | export const topicSortingFields = [ | ||||||
|   { |   { | ||||||
|     name: 'Last Updated Timestamp', |     name: 'Last Updated', | ||||||
|     value: 'last_updated_timestamp', |     value: 'last_updated_timestamp', | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| @ -75,7 +76,7 @@ export const sortingOrder = [ | |||||||
| 
 | 
 | ||||||
| export const facetFilterPlaceholder = [ | export const facetFilterPlaceholder = [ | ||||||
|   { |   { | ||||||
|     name: 'Service Type', |     name: 'Service', | ||||||
|     value: 'Service', |     value: 'Service', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
| @ -113,6 +114,7 @@ export const ROUTES = { | |||||||
|   SIGNIN: '/signin', |   SIGNIN: '/signin', | ||||||
|   DATASET_DETAILS: `/dataset/${PLACEHOLDER_ROUTE_DATASET_FQN}`, |   DATASET_DETAILS: `/dataset/${PLACEHOLDER_ROUTE_DATASET_FQN}`, | ||||||
|   TOPIC_DETAILS: `/topic/${PLACEHOLDER_ROUTE_TOPIC_FQN}`, |   TOPIC_DETAILS: `/topic/${PLACEHOLDER_ROUTE_TOPIC_FQN}`, | ||||||
|  |   DASHBOARD_DETAILS: `/dashboard/${PLACEHOLDER_ROUTE_DASHBOARD_FQN}`, | ||||||
|   DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`, |   DATABASE_DETAILS: `/database/${PLACEHOLDER_ROUTE_DATABASE_FQN}`, | ||||||
|   ONBOARDING: '/onboarding', |   ONBOARDING: '/onboarding', | ||||||
| }; | }; | ||||||
| @ -163,6 +165,12 @@ export const getTopicDetailsPath = (topicFQN: string) => { | |||||||
| 
 | 
 | ||||||
|   return path; |   return path; | ||||||
| }; | }; | ||||||
|  | export const getDashboardDetailsPath = (dashboardFQN: string) => { | ||||||
|  |   let path = ROUTES.DASHBOARD_DETAILS; | ||||||
|  |   path = path.replace(PLACEHOLDER_ROUTE_DASHBOARD_FQN, dashboardFQN); | ||||||
|  | 
 | ||||||
|  |   return path; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export const LIST_TYPES = ['numbered-list', 'bulleted-list']; | export const LIST_TYPES = ['numbered-list', 'bulleted-list']; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -24,4 +24,5 @@ export enum FilterType { | |||||||
| export enum SearchIndex { | export enum SearchIndex { | ||||||
|   TABLE = 'table_search_index', |   TABLE = 'table_search_index', | ||||||
|   TOPIC = 'topic_search_index', |   TOPIC = 'topic_search_index', | ||||||
|  |   DASHBOARD = 'dashboard_search_index', | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,322 @@ | |||||||
|  | import { AxiosResponse } from 'axios'; | ||||||
|  | import { compare } from 'fast-json-patch'; | ||||||
|  | import { isNil } from 'lodash'; | ||||||
|  | import { ColumnTags, TableDetail } from 'Models'; | ||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { useParams } from 'react-router-dom'; | ||||||
|  | import { | ||||||
|  |   addFollower, | ||||||
|  |   getDashboardByFqn, | ||||||
|  |   patchDashboardDetails, | ||||||
|  |   removeFollower, | ||||||
|  | } from '../../axiosAPIs/dashboardAPI'; | ||||||
|  | import { getServiceById } from '../../axiosAPIs/serviceAPI'; | ||||||
|  | import Description from '../../components/common/description/Description'; | ||||||
|  | import EntityPageInfo from '../../components/common/entityPageInfo/EntityPageInfo'; | ||||||
|  | import TabsPane from '../../components/common/TabsPane/TabsPane'; | ||||||
|  | import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface'; | ||||||
|  | import PageContainer from '../../components/containers/PageContainer'; | ||||||
|  | import Loader from '../../components/Loader/Loader'; | ||||||
|  | import ManageTab from '../../components/my-data-details/ManageTab'; | ||||||
|  | import { getServiceDetailsPath } from '../../constants/constants'; | ||||||
|  | import { Dashboard, TagLabel } from '../../generated/entity/data/dashboard'; | ||||||
|  | import { getCurrentUserId, getUserTeams } from '../../utils/CommonUtils'; | ||||||
|  | import { serviceTypeLogo } from '../../utils/ServiceUtils'; | ||||||
|  | import { | ||||||
|  |   getOwnerFromId, | ||||||
|  |   getTagsWithoutTier, | ||||||
|  |   getTierFromTableTags, | ||||||
|  |   getUsagePercentile, | ||||||
|  | } from '../../utils/TableUtils'; | ||||||
|  | import { getTagCategories, getTaglist } from '../../utils/TagsUtils'; | ||||||
|  | 
 | ||||||
|  | const MyDashBoardPage = () => { | ||||||
|  |   const USERId = getCurrentUserId(); | ||||||
|  |   const [tagList, setTagList] = useState<Array<string>>([]); | ||||||
|  |   const { dashboardFQN } = useParams() as Record<string, string>; | ||||||
|  |   const [dashboardDetails, setDashboardDetails] = useState<Dashboard>( | ||||||
|  |     {} as Dashboard | ||||||
|  |   ); | ||||||
|  |   const [dashboardId, setDashboardId] = useState<string>(''); | ||||||
|  |   const [isLoading, setLoading] = useState<boolean>(false); | ||||||
|  |   const [description, setDescription] = useState<string>(''); | ||||||
|  |   const [followers, setFollowers] = useState<number>(0); | ||||||
|  |   const [isFollowing, setIsFollowing] = useState(false); | ||||||
|  |   const [owner, setOwner] = useState<TableDetail['owner']>(); | ||||||
|  |   const [tier, setTier] = useState<string>(); | ||||||
|  |   const [tags, setTags] = useState<Array<ColumnTags>>([]); | ||||||
|  |   const [activeTab, setActiveTab] = useState<number>(1); | ||||||
|  |   const [isEdit, setIsEdit] = useState<boolean>(false); | ||||||
|  |   const [usage, setUsage] = useState(''); | ||||||
|  |   const [weeklyUsageCount, setWeeklyUsageCount] = useState(''); | ||||||
|  |   const [slashedDashboardName, setSlashedDashboardName] = useState< | ||||||
|  |     TitleBreadcrumbProps['titleLinks'] | ||||||
|  |   >([]); | ||||||
|  | 
 | ||||||
|  |   const hasEditAccess = () => { | ||||||
|  |     if (owner?.type === 'user') { | ||||||
|  |       return owner.id === getCurrentUserId(); | ||||||
|  |     } else { | ||||||
|  |       return getUserTeams().some((team) => team.id === owner?.id); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   const tabs = [ | ||||||
|  |     { | ||||||
|  |       name: 'Details', | ||||||
|  |       icon: { | ||||||
|  |         alt: 'schema', | ||||||
|  |         name: 'icon-schema', | ||||||
|  |         title: 'Details', | ||||||
|  |       }, | ||||||
|  |       isProtected: false, | ||||||
|  |       position: 1, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       name: 'Manage', | ||||||
|  |       icon: { | ||||||
|  |         alt: 'manage', | ||||||
|  |         name: 'icon-manage', | ||||||
|  |         title: 'Manage', | ||||||
|  |       }, | ||||||
|  |       isProtected: true, | ||||||
|  |       protectedState: !owner || hasEditAccess(), | ||||||
|  |       position: 2, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   const extraInfo = [ | ||||||
|  |     { key: 'Owner', value: owner?.name || '' }, | ||||||
|  |     { key: 'Tier', value: tier ? tier.split('.')[1] : '' }, | ||||||
|  |     { key: 'Usage', value: usage }, | ||||||
|  |     { key: 'Queries', value: `${weeklyUsageCount} past week` }, | ||||||
|  |   ]; | ||||||
|  |   const fetchTags = () => { | ||||||
|  |     getTagCategories().then((res) => { | ||||||
|  |       if (res.data) { | ||||||
|  |         setTagList(getTaglist(res.data)); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |   const fetchDashboardDetail = (dashboardFQN: string) => { | ||||||
|  |     setLoading(true); | ||||||
|  |     getDashboardByFqn(dashboardFQN, [ | ||||||
|  |       'owner', | ||||||
|  |       'service', | ||||||
|  |       'followers', | ||||||
|  |       'tags', | ||||||
|  |       'usageSummary', | ||||||
|  |     ]).then((res: AxiosResponse) => { | ||||||
|  |       const { | ||||||
|  |         id, | ||||||
|  |         description, | ||||||
|  |         followers, | ||||||
|  |         service, | ||||||
|  |         tags, | ||||||
|  |         owner, | ||||||
|  |         usageSummary, | ||||||
|  |         displayName, | ||||||
|  |       } = res.data; | ||||||
|  |       setDashboardDetails(res.data); | ||||||
|  |       setDashboardId(id); | ||||||
|  |       setDescription(description ?? ''); | ||||||
|  |       setFollowers(followers?.length); | ||||||
|  |       setOwner(getOwnerFromId(owner?.id)); | ||||||
|  |       setTier(getTierFromTableTags(tags)); | ||||||
|  |       setTags(getTagsWithoutTier(tags)); | ||||||
|  |       setIsFollowing(followers.some(({ id }: { id: string }) => id === USERId)); | ||||||
|  |       if (!isNil(usageSummary?.weeklyStats.percentileRank)) { | ||||||
|  |         const percentile = getUsagePercentile( | ||||||
|  |           usageSummary.weeklyStats.percentileRank | ||||||
|  |         ); | ||||||
|  |         setUsage(percentile); | ||||||
|  |       } else { | ||||||
|  |         setUsage('--'); | ||||||
|  |       } | ||||||
|  |       setWeeklyUsageCount( | ||||||
|  |         usageSummary?.weeklyStats.count.toLocaleString() || '--' | ||||||
|  |       ); | ||||||
|  |       getServiceById('dashboardServices', service?.id).then( | ||||||
|  |         (serviceRes: AxiosResponse) => { | ||||||
|  |           setSlashedDashboardName([ | ||||||
|  |             { | ||||||
|  |               name: serviceRes.data.name, | ||||||
|  |               url: serviceRes.data.name | ||||||
|  |                 ? getServiceDetailsPath( | ||||||
|  |                     serviceRes.data.name, | ||||||
|  |                     serviceRes.data.serviceType | ||||||
|  |                   ) | ||||||
|  |                 : '', | ||||||
|  |               imgSrc: serviceRes.data.serviceType | ||||||
|  |                 ? serviceTypeLogo(serviceRes.data.serviceType) | ||||||
|  |                 : undefined, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               name: displayName, | ||||||
|  |               url: '', | ||||||
|  |               activeTitle: true, | ||||||
|  |             }, | ||||||
|  |           ]); | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |       setLoading(false); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const followDashboard = (): void => { | ||||||
|  |     if (isFollowing) { | ||||||
|  |       removeFollower(dashboardId, USERId).then(() => { | ||||||
|  |         setFollowers((preValu) => preValu - 1); | ||||||
|  |         setIsFollowing(false); | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       addFollower(dashboardId, USERId).then(() => { | ||||||
|  |         setFollowers((preValu) => preValu + 1); | ||||||
|  |         setIsFollowing(true); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onDescriptionUpdate = (updatedHTML: string) => { | ||||||
|  |     const updatedDashboard = { ...dashboardDetails, description: updatedHTML }; | ||||||
|  | 
 | ||||||
|  |     const jsonPatch = compare(dashboardDetails, updatedDashboard); | ||||||
|  |     patchDashboardDetails(dashboardId, jsonPatch).then((res: AxiosResponse) => { | ||||||
|  |       setDescription(res.data.description); | ||||||
|  |     }); | ||||||
|  |     setIsEdit(false); | ||||||
|  |   }; | ||||||
|  |   const onDescriptionEdit = (): void => { | ||||||
|  |     setIsEdit(true); | ||||||
|  |   }; | ||||||
|  |   const onCancel = () => { | ||||||
|  |     setIsEdit(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onSettingsUpdate = ( | ||||||
|  |     newOwner?: TableDetail['owner'], | ||||||
|  |     newTier?: TableDetail['tier'] | ||||||
|  |   ): Promise<void> => { | ||||||
|  |     return new Promise<void>((resolve, reject) => { | ||||||
|  |       if (newOwner || newTier) { | ||||||
|  |         const tierTag: TableDetail['tags'] = newTier | ||||||
|  |           ? [ | ||||||
|  |               ...getTagsWithoutTier(dashboardDetails.tags as ColumnTags[]), | ||||||
|  |               { tagFQN: newTier, labelType: 'Manual', state: 'Confirmed' }, | ||||||
|  |             ] | ||||||
|  |           : (dashboardDetails.tags as ColumnTags[]); | ||||||
|  |         const updatedDashboard = { | ||||||
|  |           ...dashboardDetails, | ||||||
|  |           owner: newOwner | ||||||
|  |             ? { ...dashboardDetails.owner, ...newOwner } | ||||||
|  |             : dashboardDetails.owner, | ||||||
|  |           tags: tierTag, | ||||||
|  |         }; | ||||||
|  |         const jsonPatch = compare(dashboardDetails, updatedDashboard); | ||||||
|  |         patchDashboardDetails(dashboardId, jsonPatch) | ||||||
|  |           .then((res: AxiosResponse) => { | ||||||
|  |             setDashboardDetails(res.data); | ||||||
|  |             setOwner(getOwnerFromId(res.data.owner?.id)); | ||||||
|  |             setTier(getTierFromTableTags(res.data.tags)); | ||||||
|  |             resolve(); | ||||||
|  |           }) | ||||||
|  |           .catch(() => reject()); | ||||||
|  |       } else { | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onTagUpdate = (selectedTags?: Array<string>) => { | ||||||
|  |     if (selectedTags) { | ||||||
|  |       const prevTags = dashboardDetails?.tags?.filter((tag) => | ||||||
|  |         selectedTags.includes(tag?.tagFQN as string) | ||||||
|  |       ); | ||||||
|  |       const newTags: Array<ColumnTags> = selectedTags | ||||||
|  |         .filter((tag) => { | ||||||
|  |           return !prevTags?.map((prevTag) => prevTag.tagFQN).includes(tag); | ||||||
|  |         }) | ||||||
|  |         .map((tag) => ({ | ||||||
|  |           labelType: 'Manual', | ||||||
|  |           state: 'Confirmed', | ||||||
|  |           tagFQN: tag, | ||||||
|  |         })); | ||||||
|  |       const updatedTags = [...(prevTags as TagLabel[]), ...newTags]; | ||||||
|  |       const updatedDashboard = { ...dashboardDetails, tags: updatedTags }; | ||||||
|  |       const jsonPatch = compare(dashboardDetails, updatedDashboard); | ||||||
|  |       patchDashboardDetails(dashboardId, jsonPatch).then( | ||||||
|  |         (res: AxiosResponse) => { | ||||||
|  |           setTier(getTierFromTableTags(res.data.tags)); | ||||||
|  |           setTags(getTagsWithoutTier(res.data.tags)); | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchDashboardDetail(dashboardFQN); | ||||||
|  |   }, [dashboardFQN]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchTags(); | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <PageContainer> | ||||||
|  |       {isLoading ? ( | ||||||
|  |         <Loader /> | ||||||
|  |       ) : ( | ||||||
|  |         <div className="tw-px-4 w-full"> | ||||||
|  |           <EntityPageInfo | ||||||
|  |             isTagEditable | ||||||
|  |             extraInfo={extraInfo} | ||||||
|  |             followers={followers} | ||||||
|  |             followHandler={followDashboard} | ||||||
|  |             isFollowing={isFollowing} | ||||||
|  |             tagList={tagList} | ||||||
|  |             tags={tags} | ||||||
|  |             tagsHandler={onTagUpdate} | ||||||
|  |             tier={tier || ''} | ||||||
|  |             titleLinks={slashedDashboardName} | ||||||
|  |           /> | ||||||
|  |           <div className="tw-block tw-mt-1"> | ||||||
|  |             <TabsPane | ||||||
|  |               activeTab={activeTab} | ||||||
|  |               setActiveTab={setActiveTab} | ||||||
|  |               tabs={tabs} | ||||||
|  |             /> | ||||||
|  | 
 | ||||||
|  |             <div className="tw-bg-white tw--mx-4 tw-p-4"> | ||||||
|  |               {activeTab === 1 && ( | ||||||
|  |                 <> | ||||||
|  |                   <div className="tw-grid tw-grid-cols-4 tw-gap-4 w-full"> | ||||||
|  |                     <div className="tw-col-span-full"> | ||||||
|  |                       <Description | ||||||
|  |                         description={description} | ||||||
|  |                         hasEditAccess={hasEditAccess()} | ||||||
|  |                         isEdit={isEdit} | ||||||
|  |                         owner={owner} | ||||||
|  |                         onCancel={onCancel} | ||||||
|  |                         onDescriptionEdit={onDescriptionEdit} | ||||||
|  |                         onDescriptionUpdate={onDescriptionUpdate} | ||||||
|  |                       /> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </> | ||||||
|  |               )} | ||||||
|  |               {activeTab === 2 && ( | ||||||
|  |                 <ManageTab | ||||||
|  |                   currentTier={tier} | ||||||
|  |                   currentUser={owner?.id} | ||||||
|  |                   hasEditAccess={hasEditAccess()} | ||||||
|  |                   onSave={onSettingsUpdate} | ||||||
|  |                 /> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |     </PageContainer> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default MyDashBoardPage; | ||||||
| @ -17,7 +17,12 @@ | |||||||
| 
 | 
 | ||||||
| import { lowerCase } from 'lodash'; | import { lowerCase } from 'lodash'; | ||||||
| import { AggregationType, Bucket } from 'Models'; | import { AggregationType, Bucket } from 'Models'; | ||||||
| import { tiers } from '../../constants/constants'; | import { | ||||||
|  |   tableSortingFields, | ||||||
|  |   tiers, | ||||||
|  |   topicSortingFields, | ||||||
|  | } from '../../constants/constants'; | ||||||
|  | import { SearchIndex } from '../../enums/search.enum'; | ||||||
| 
 | 
 | ||||||
| export const getBucketList = (buckets: Array<Bucket>) => { | export const getBucketList = (buckets: Array<Bucket>) => { | ||||||
|   let bucketList: Array<Bucket> = [...tiers]; |   let bucketList: Array<Bucket> = [...tiers]; | ||||||
| @ -53,3 +58,27 @@ export const getAggrWithDefaultValue = ( | |||||||
|     ? aggregations |     ? aggregations | ||||||
|     : aggregations.filter((item) => allowedAgg.includes(lowerCase(item.title))); |     : aggregations.filter((item) => allowedAgg.includes(lowerCase(item.title))); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const tabsInfo = [ | ||||||
|  |   { | ||||||
|  |     label: 'Tables', | ||||||
|  |     index: SearchIndex.TABLE, | ||||||
|  |     sortingFields: tableSortingFields, | ||||||
|  |     sortField: tableSortingFields[0].value, | ||||||
|  |     tab: 1, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Topics', | ||||||
|  |     index: SearchIndex.TOPIC, | ||||||
|  |     sortingFields: topicSortingFields, | ||||||
|  |     sortField: topicSortingFields[0].value, | ||||||
|  |     tab: 2, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Dashboards', | ||||||
|  |     index: SearchIndex.DASHBOARD, | ||||||
|  |     sortingFields: topicSortingFields, | ||||||
|  |     sortField: topicSortingFields[0].value, | ||||||
|  |     tab: 3, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | |||||||
| @ -37,7 +37,6 @@ import { | |||||||
|   ERROR500, |   ERROR500, | ||||||
|   PAGE_SIZE, |   PAGE_SIZE, | ||||||
|   tableSortingFields, |   tableSortingFields, | ||||||
|   topicSortingFields, |  | ||||||
| } from '../../constants/constants'; | } from '../../constants/constants'; | ||||||
| import { SearchIndex } from '../../enums/search.enum'; | import { SearchIndex } from '../../enums/search.enum'; | ||||||
| import { usePrevious } from '../../hooks/usePrevious'; | import { usePrevious } from '../../hooks/usePrevious'; | ||||||
| @ -47,10 +46,10 @@ import { formatDataResponse } from '../../utils/APIUtils'; | |||||||
| import { getCountBadge } from '../../utils/CommonUtils'; | import { getCountBadge } from '../../utils/CommonUtils'; | ||||||
| import { getFilterString } from '../../utils/FilterUtils'; | import { getFilterString } from '../../utils/FilterUtils'; | ||||||
| import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant'; | import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant'; | ||||||
| import { getAggrWithDefaultValue } from './explore.constants'; | import { getAggrWithDefaultValue, tabsInfo } from './explore.constants'; | ||||||
| import { Params } from './explore.interface'; | import { Params } from './explore.interface'; | ||||||
| 
 | 
 | ||||||
| const visibleFilters = ['tags', 'service type', 'tier']; | const visibleFilters = ['tags', 'service', 'tier']; | ||||||
| 
 | 
 | ||||||
| const getQueryParam = (urlSearchQuery = ''): FilterObject => { | const getQueryParam = (urlSearchQuery = ''): FilterObject => { | ||||||
|   const arrSearchQuery = urlSearchQuery |   const arrSearchQuery = urlSearchQuery | ||||||
| @ -74,7 +73,7 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
| 
 | 
 | ||||||
|   const filterObject: FilterObject = { |   const filterObject: FilterObject = { | ||||||
|     ...{ tags: [], 'service type': [], tier: [] }, |     ...{ tags: [], service: [], tier: [] }, | ||||||
|     ...getQueryParam(location.search), |     ...getQueryParam(location.search), | ||||||
|   }; |   }; | ||||||
|   const showToast = useToastContext(); |   const showToast = useToastContext(); | ||||||
| @ -95,6 +94,7 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|   const [currentTab, setCurrentTab] = useState<number>(1); |   const [currentTab, setCurrentTab] = useState<number>(1); | ||||||
|   const [tableCount, setTableCount] = useState<number>(0); |   const [tableCount, setTableCount] = useState<number>(0); | ||||||
|   const [topicCount, setTopicCount] = useState<number>(0); |   const [topicCount, setTopicCount] = useState<number>(0); | ||||||
|  |   const [dashboardCount, setDashboardCount] = useState<number>(0); | ||||||
|   const [fieldList, setFieldList] = |   const [fieldList, setFieldList] = | ||||||
|     useState<Array<{ name: string; value: string }>>(tableSortingFields); |     useState<Array<{ name: string; value: string }>>(tableSortingFields); | ||||||
|   const isMounting = useRef(true); |   const isMounting = useRef(true); | ||||||
| @ -198,10 +198,20 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|       emptyValue, |       emptyValue, | ||||||
|       SearchIndex.TOPIC |       SearchIndex.TOPIC | ||||||
|     ); |     ); | ||||||
|     Promise.all([tableCount, topicCount]) |     const dashboardCount = searchData( | ||||||
|       .then(([table, topic]: Array<SearchResponse>) => { |       searchText, | ||||||
|  |       0, | ||||||
|  |       0, | ||||||
|  |       getFilterString(filters), | ||||||
|  |       emptyValue, | ||||||
|  |       emptyValue, | ||||||
|  |       SearchIndex.DASHBOARD | ||||||
|  |     ); | ||||||
|  |     Promise.all([tableCount, topicCount, dashboardCount]) | ||||||
|  |       .then(([table, topic, dashboard]: Array<SearchResponse>) => { | ||||||
|         setTableCount(table.data.hits.total.value); |         setTableCount(table.data.hits.total.value); | ||||||
|         setTopicCount(topic.data.hits.total.value); |         setTopicCount(topic.data.hits.total.value); | ||||||
|  |         setDashboardCount(dashboard.data.hits.total.value); | ||||||
|       }) |       }) | ||||||
|       .catch((err: AxiosError) => { |       .catch((err: AxiosError) => { | ||||||
|         setError(ERROR404); |         setError(ERROR404); | ||||||
| @ -228,7 +238,7 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|       searchText, |       searchText, | ||||||
|       currentPage, |       currentPage, | ||||||
|       0, |       0, | ||||||
|       getFilterString(filters, ['service type']), |       getFilterString(filters, ['service']), | ||||||
|       sortField, |       sortField, | ||||||
|       sortOrder, |       sortOrder, | ||||||
|       searchIndex |       searchIndex | ||||||
| @ -270,7 +280,7 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|           } else { |           } else { | ||||||
|             const aggServiceType = getAggregationList( |             const aggServiceType = getAggregationList( | ||||||
|               resAggServiceType.data.aggregations, |               resAggServiceType.data.aggregations, | ||||||
|               'service type' |               'service' | ||||||
|             ); |             ); | ||||||
|             const aggTier = getAggregationList( |             const aggTier = getAggregationList( | ||||||
|               resAggTier.data.aggregations, |               resAggTier.data.aggregations, | ||||||
| @ -380,37 +390,42 @@ const ExplorePage: React.FC = (): React.ReactElement => { | |||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const getTabCount = (index: string) => { | ||||||
|  |     switch (index) { | ||||||
|  |       case SearchIndex.TABLE: | ||||||
|  |         return getCountBadge(tableCount); | ||||||
|  |       case SearchIndex.TOPIC: | ||||||
|  |         return getCountBadge(topicCount); | ||||||
|  |       case SearchIndex.DASHBOARD: | ||||||
|  |         return getCountBadge(dashboardCount); | ||||||
|  |       default: | ||||||
|  |         return getCountBadge(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const getTabs = () => { |   const getTabs = () => { | ||||||
|     return ( |     return ( | ||||||
|       <div className="tw-mb-3 tw--mt-4"> |       <div className="tw-mb-3 tw--mt-4"> | ||||||
|         <nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4 tw-justify-between"> |         <nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4 tw-justify-between"> | ||||||
|           <div> |           <div> | ||||||
|             <button |             {tabsInfo.map((tab, index) => ( | ||||||
|               className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(1)}`} |               <button | ||||||
|               onClick={() => { |                 className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass( | ||||||
|                 setCurrentTab(1); |                   tab.tab | ||||||
|                 setSearchIndex(SearchIndex.TABLE); |                 )}`}
 | ||||||
|                 setFieldList(tableSortingFields); |                 key={index} | ||||||
|                 setSortField(tableSortingFields[0].value); |                 onClick={() => { | ||||||
|                 setCurrentPage(1); |                   setCurrentTab(tab.tab); | ||||||
|                 resetFilters(); |                   setSearchIndex(tab.index); | ||||||
|               }}> |                   setFieldList(tab.sortingFields); | ||||||
|               Tables |                   setSortField(tab.sortField); | ||||||
|               {getCountBadge(tableCount)} |                   setCurrentPage(1); | ||||||
|             </button> |                   resetFilters(); | ||||||
|             <button |                 }}> | ||||||
|               className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(2)}`} |                 {tab.label} | ||||||
|               onClick={() => { |                 {getTabCount(tab.index)} | ||||||
|                 setCurrentTab(2); |               </button> | ||||||
|                 setSearchIndex(SearchIndex.TOPIC); |             ))} | ||||||
|                 setFieldList(topicSortingFields); |  | ||||||
|                 setSortField(topicSortingFields[0].value); |  | ||||||
|                 setCurrentPage(1); |  | ||||||
|                 resetFilters(); |  | ||||||
|               }}> |  | ||||||
|               Topics |  | ||||||
|               {getCountBadge(topicCount)} |  | ||||||
|             </button> |  | ||||||
|           </div> |           </div> | ||||||
|           {getSortingElements()} |           {getSortingElements()} | ||||||
|         </nav> |         </nav> | ||||||
|  | |||||||
| @ -139,6 +139,7 @@ const MyDataDetailsPage = () => { | |||||||
| 
 | 
 | ||||||
|   const extraInfo = [ |   const extraInfo = [ | ||||||
|     { key: 'Owner', value: owner?.name || '' }, |     { key: 'Owner', value: owner?.name || '' }, | ||||||
|  |     { key: 'Tier', value: tier ? tier.split('.')[1] : '' }, | ||||||
|     { key: 'Usage', value: usage }, |     { key: 'Usage', value: usage }, | ||||||
|     { key: 'Queries', value: `${weeklyUsageCount} past week` }, |     { key: 'Queries', value: `${weeklyUsageCount} past week` }, | ||||||
|   ]; |   ]; | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'; | |||||||
| import AppState from '../AppState'; | import AppState from '../AppState'; | ||||||
| import Onboarding from '../components/onboarding/Onboarding'; // Remove this route once Onboarding is added to my-data
 | import Onboarding from '../components/onboarding/Onboarding'; // Remove this route once Onboarding is added to my-data
 | ||||||
| import { ROUTES } from '../constants/constants'; | import { ROUTES } from '../constants/constants'; | ||||||
|  | import MyDashBoardPage from '../pages/dashboard-details'; | ||||||
| import DatabaseDetails from '../pages/database-details/index'; | import DatabaseDetails from '../pages/database-details/index'; | ||||||
| import ExplorePage from '../pages/explore'; | import ExplorePage from '../pages/explore'; | ||||||
| import MyDataPage from '../pages/my-data'; | import MyDataPage from '../pages/my-data'; | ||||||
| @ -64,6 +65,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => { | |||||||
|       <Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} /> |       <Route component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} /> | ||||||
|       <Route component={MyDataDetailsPage} path={ROUTES.DATASET_DETAILS} /> |       <Route component={MyDataDetailsPage} path={ROUTES.DATASET_DETAILS} /> | ||||||
|       <Route component={MyTopicDetailPage} path={ROUTES.TOPIC_DETAILS} /> |       <Route component={MyTopicDetailPage} path={ROUTES.TOPIC_DETAILS} /> | ||||||
|  |       <Route component={MyDashBoardPage} path={ROUTES.DASHBOARD_DETAILS} /> | ||||||
|       <Route component={Onboarding} path={ROUTES.ONBOARDING} /> |       <Route component={Onboarding} path={ROUTES.ONBOARDING} /> | ||||||
|       <Redirect to={ROUTES.NOT_FOUND} /> |       <Redirect to={ROUTES.NOT_FOUND} /> | ||||||
|     </Switch> |     </Switch> | ||||||
|  | |||||||
| @ -6,8 +6,12 @@ import { getRelativeTime } from './TimeUtils'; | |||||||
| export const formatDataResponse = (hits) => { | export const formatDataResponse = (hits) => { | ||||||
|   const formattedData = hits.map((hit) => { |   const formattedData = hits.map((hit) => { | ||||||
|     const newData = {}; |     const newData = {}; | ||||||
|     newData.id = hit._source.table_id || hit._source.topic_id; |     newData.id = | ||||||
|     newData.name = hit._source.table_name || hit._source.topic_name; |       hit._source.table_id || hit._source.topic_id || hit._source.dashboard_id; | ||||||
|  |     newData.name = | ||||||
|  |       hit._source.table_name || | ||||||
|  |       hit._source.topic_name || | ||||||
|  |       hit._source.dashboard_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; | ||||||
|  | |||||||
| @ -7,7 +7,8 @@ export const getFilterString = (filters, excludeFilters = []) => { | |||||||
|     const modifiedFilter = []; |     const modifiedFilter = []; | ||||||
|     const filter = filters[key]; |     const filter = filters[key]; | ||||||
|     filter.forEach((value) => { |     filter.forEach((value) => { | ||||||
|       modifiedFilter.push(`${key.split(' ').join('_')}:${value}`); |       const modifiedKey = key === 'service' ? 'service type' : key; | ||||||
|  |       modifiedFilter.push(`${modifiedKey.split(' ').join('_')}:${value}`); | ||||||
|     }); |     }); | ||||||
|     modifiedFilters[key] = modifiedFilter; |     modifiedFilters[key] = modifiedFilter; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import React from 'react'; | |||||||
| import AppState from '../AppState'; | import AppState from '../AppState'; | ||||||
| import PopOver from '../components/common/popover/PopOver'; | import PopOver from '../components/common/popover/PopOver'; | ||||||
| import { | import { | ||||||
|  |   getDashboardDetailsPath, | ||||||
|   getDatasetDetailsPath, |   getDatasetDetailsPath, | ||||||
|   getTopicDetailsPath, |   getTopicDetailsPath, | ||||||
| } from '../constants/constants'; | } from '../constants/constants'; | ||||||
| @ -156,6 +157,9 @@ export const getEntityLink = ( | |||||||
|     case SearchIndex.TOPIC: |     case SearchIndex.TOPIC: | ||||||
|       return getTopicDetailsPath(fullyQualifiedName); |       return getTopicDetailsPath(fullyQualifiedName); | ||||||
| 
 | 
 | ||||||
|  |     case SearchIndex.DASHBOARD: | ||||||
|  |       return getDashboardDetailsPath(fullyQualifiedName); | ||||||
|  | 
 | ||||||
|     case SearchIndex.TABLE: |     case SearchIndex.TABLE: | ||||||
|     default: |     default: | ||||||
|       return getDatasetDetailsPath(fullyQualifiedName); |       return getDatasetDetailsPath(fullyQualifiedName); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sachin Chaurasiya
						Sachin Chaurasiya