diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg new file mode 100644 index 00000000000..0255ef06e26 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-down.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-down.svg new file mode 100644 index 00000000000..bc4bd7d9951 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-up.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-up.svg new file mode 100644 index 00000000000..5efd97d969a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx index b63b486209f..f9d083f84c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx @@ -47,6 +47,7 @@ import SchemaEditor from '../schema-editor/SchemaEditor'; import SchemaTab from '../SchemaTab/SchemaTab.component'; import TableProfiler from '../TableProfiler/TableProfiler.component'; import TableProfilerGraph from '../TableProfiler/TableProfilerGraph.component'; +import TableQueries from '../TableQueries/TableQueries'; import { DatasetDetailsProps } from './DatasetDetails.interface'; const DatasetDetails: React.FC = ({ @@ -85,6 +86,8 @@ const DatasetDetails: React.FC = ({ entityLineageHandler, isLineageLoading, isSampleDataLoading, + isQueriesLoading, + tableQueries, }: DatasetDetailsProps) => { const { isAuthDisabled } = useAuth(); const [isEdit, setIsEdit] = useState(false); @@ -150,6 +153,17 @@ const DatasetDetails: React.FC = ({ isProtected: false, position: 2, }, + { + name: 'Queries', + icon: { + alt: 'table_queries', + name: 'table_queries', + title: 'Table Queries', + selectedName: '', + }, + isProtected: false, + position: 3, + }, { name: 'Profiler', icon: { @@ -159,7 +173,7 @@ const DatasetDetails: React.FC = ({ selectedName: 'icon-profilercolor', }, isProtected: false, - position: 3, + position: 4, }, { name: 'Lineage', @@ -170,7 +184,7 @@ const DatasetDetails: React.FC = ({ selectedName: 'icon-lineagecolor', }, isProtected: false, - position: 4, + position: 5, }, { name: 'DBT', @@ -182,7 +196,7 @@ const DatasetDetails: React.FC = ({ }, isProtected: false, isHidden: !dataModel?.sql, - position: 5, + position: 6, }, { name: 'Manage', @@ -195,7 +209,7 @@ const DatasetDetails: React.FC = ({ isProtected: true, isHidden: deleted, protectedState: !owner || hasEditAccess(), - position: 6, + position: 7, }, ]; @@ -471,6 +485,14 @@ const DatasetDetails: React.FC = ({ )} {activeTab === 3 && ( +
+ +
+ )} + {activeTab === 4 && (
({ @@ -481,7 +503,7 @@ const DatasetDetails: React.FC = ({ />
)} - {activeTab === 4 && ( + {activeTab === 5 && (
= ({ />
)} - {activeTab === 5 && Boolean(dataModel?.sql) && ( + {activeTab === 6 && Boolean(dataModel?.sql) && (
= ({ />
)} - {activeTab === 6 && !deleted && ( + {activeTab === 7 && !deleted && (
void; followTableHandler: () => void; unfollowTableHandler: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx index d0bc522b57a..1f031bf4761 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx @@ -77,6 +77,7 @@ const DatasetDetailsProps = { addLineageHandler: jest.fn(), removeLineageHandler: jest.fn(), entityLineageHandler: jest.fn(), + tableQueries: [], }; jest.mock('../ManageTab/ManageTab.component', () => { return jest.fn().mockReturnValue(

ManageTab

); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableQueries/TableQueries.tsx new file mode 100644 index 00000000000..55b32eb5034 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableQueries/TableQueries.tsx @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames'; +import React, { FC, HTMLAttributes, useState } from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { CSMode } from '../../enums/codemirror.enum'; +import { SQLQuery, Table } from '../../generated/entity/data/table'; +import { withLoader } from '../../hoc/withLoader'; +import SVGIcons, { Icons } from '../../utils/SvgUtils'; +import { Button } from '../buttons/Button/Button'; +import SchemaEditor from '../schema-editor/SchemaEditor'; + +interface TableQueriesProp extends HTMLAttributes { + queries: Table['tableQueries']; +} +interface QueryCardProp extends HTMLAttributes { + query: SQLQuery; +} + +const QueryCard: FC = ({ className, query }) => { + const [expanded, setExpanded] = useState(false); + const [, setIsCopied] = useState(false); + const [showCopiedText, setShowCopiedText] = useState(false); + + const copiedTextHandler = () => { + setShowCopiedText(true); + setTimeout(() => { + setShowCopiedText(false); + }, 1000); + }; + + return ( +
+
setExpanded((pre) => !pre)}> +
+

+ Last run by{' '} + + {query.user?.displayName ?? query.user?.name} + {' '} + and took{' '} + {query.duration} seconds +

+ + +
+
+
+
+ { + setIsCopied(result); + if (result) copiedTextHandler(); + }}> + + + + +
+
+
+ ); +}; + +const TableQueries: FC = ({ queries, className }) => { + return ( +
+
+ {queries?.map((query, index) => ( + + ))} +
+
+ ); +}; + +export default withLoader(TableQueries); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOver.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOver.tsx index 043e025cf4a..1ce2a0d54b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOver.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOver.tsx @@ -29,6 +29,7 @@ const PopOver: React.FC = ({ title, trigger, theme = 'dark', + sticky = false, }): JSX.Element => { return ( = ({ html={html} position={position} size={size} + sticky={sticky} theme={theme} title={title || ''} trigger={trigger}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOverTypes.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOverTypes.ts index 5a8959c94ca..ec584398ad4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOverTypes.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/popover/PopOverTypes.ts @@ -30,4 +30,5 @@ export type PopOverProp = { className?: string; delay?: number; hideDelay?: number; + sticky?: boolean; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx index fc4ad503013..b0cf18d16c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/schema-editor/SchemaEditor.tsx @@ -39,12 +39,18 @@ const SchemaEditor = ({ name: CSMode.JAVASCRIPT, json: true, }, + options, + editorClass, }: { value: string; className?: string; mode?: Mode; + options?: { + [key: string]: string | boolean | Array; + }; + editorClass?: string; }) => { - const options = { + const defaultOptions = { tabSize: JSON_TAB_SIZE, indentUnit: JSON_TAB_SIZE, indentWithTabs: false, @@ -57,6 +63,7 @@ const SchemaEditor = ({ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], mode, readOnly: true, + ...options, }; const [internalValue, setInternalValue] = useState( getSchemaEditorValue(value) @@ -72,7 +79,8 @@ const SchemaEditor = ({ return (
diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts index 47da26851ef..24229268883 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts @@ -43,4 +43,5 @@ export enum TabSpecificField { DATAMODEL = 'dataModel', CHARTS = 'charts', TASKS = 'tasks', + TABLE_QUERIES = 'tableQueries', } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx index a646938c057..ce26b4f7229 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx @@ -78,6 +78,8 @@ const DatasetDetailsPage: FunctionComponent = () => { const [isLineageLoading, setIsLineageLoading] = useState(false); const [isSampleDataLoading, setIsSampleDataLoading] = useState(false); + const [isTableQueriesLoading, setIsTableQueriesLoading] = + useState(false); const USERId = getCurrentUserId(); const [tableId, setTableId] = useState(''); const [tier, setTier] = useState(); @@ -123,6 +125,7 @@ const DatasetDetailsPage: FunctionComponent = () => { ); const [deleted, setDeleted] = useState(false); const [isError, setIsError] = useState(false); + const [tableQueries, setTableQueries] = useState([]); const activeTabHandler = (tabValue: number) => { const currentTabIndex = tabValue - 1; @@ -280,6 +283,28 @@ const DatasetDetailsPage: FunctionComponent = () => { break; } + case TabSpecificField.TABLE_QUERIES: { + if ((tableQueries?.length ?? 0) > 0) { + break; + } else { + setIsTableQueriesLoading(true); + getTableDetailsByFQN(tableFQN, tabField) + .then((res: AxiosResponse) => { + const { tableQueries } = res.data; + setTableQueries(tableQueries); + }) + .catch(() => + showToast({ + variant: 'error', + body: 'Error while getting table queries', + }) + ) + .finally(() => setIsTableQueriesLoading(false)); + + break; + } + } + default: break; } @@ -493,6 +518,7 @@ const DatasetDetailsPage: FunctionComponent = () => { followers={followers} isLineageLoading={isLineageLoading} isNodeLoading={isNodeLoading} + isQueriesLoading={isTableQueriesLoading} isSampleDataLoading={isSampleDataLoading} joins={joins} lineageLeafNodes={leafNodes} @@ -505,6 +531,7 @@ const DatasetDetailsPage: FunctionComponent = () => { slashedTableName={slashedTableName} tableDetails={tableDetails} tableProfile={tableProfile} + tableQueries={tableQueries} tableTags={tableTags} tier={tier as TagLabel} unfollowTableHandler={unfollowTable} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx index c7416911bc6..1fca03d8ac2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx @@ -193,6 +193,7 @@ const TourPage = () => { tableProfile={ mockDatasetData.tableProfile as unknown as Table['tableProfile'] } + tableQueries={[]} tableTags={mockDatasetData.tableTags} tier={'' as unknown as TagLabel} unfollowTableHandler={handleCountChange} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css index a6ef62a3d91..a58f38be269 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css +++ b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css @@ -887,3 +887,6 @@ body .profiler-graph .recharts-active-dot circle { ); pointer-events: none; /* so the text is still selectable */ } +.table-query-editor > .CodeMirror { + height: auto !important; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts index 46b7da9324d..b2dfce176c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts @@ -27,6 +27,11 @@ export const datasetTableTabs = [ path: 'sample_data', field: TabSpecificField.SAMPLE_DATA, }, + { + name: 'Queries', + path: 'table_queries', + field: TabSpecificField.TABLE_QUERIES, + }, { name: 'Profiler', path: 'profiler', @@ -53,27 +58,31 @@ export const getCurrentDatasetTab = (tab: string) => { currentTab = 2; break; - - case 'profiler': + case 'table_queries': currentTab = 3; break; - case 'lineage': + case 'profiler': currentTab = 4; break; - case 'dbt': + case 'lineage': currentTab = 5; break; - case 'manage': + case 'dbt': currentTab = 6; break; + case 'manage': + currentTab = 7; + + break; + case 'schema': default: currentTab = 1; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index 305eeaefae9..5f6dca3abc0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -28,7 +28,6 @@ import IconConfigColor from '../assets/svg/config-color.svg'; import IconConfig from '../assets/svg/config.svg'; import IconControlMinus from '../assets/svg/control-minus.svg'; import IconControlPlus from '../assets/svg/control-plus.svg'; -import IconCopy from '../assets/svg/copy.svg'; import IconDashboardGrey from '../assets/svg/dashboard-grey.svg'; import IconDashboard from '../assets/svg/dashboard.svg'; import IconAsstest from '../assets/svg/data-assets.svg'; @@ -81,10 +80,13 @@ import IconUpArrow from '../assets/svg/ic-up-arrow.svg'; import IconVEllipsis from '../assets/svg/ic-v-ellipsis.svg'; import IconWorkflows from '../assets/svg/ic-workflows.svg'; import IconChevronDown from '../assets/svg/icon-chevron-down.svg'; +import IconCopy from '../assets/svg/icon-copy.svg'; +import IconDown from '../assets/svg/icon-down.svg'; import IconKey from '../assets/svg/icon-key.svg'; import IconNotNull from '../assets/svg/icon-notnull.svg'; import IconTour from '../assets/svg/icon-tour.svg'; import IconUnique from '../assets/svg/icon-unique.svg'; +import IconUp from '../assets/svg/icon-up.svg'; import IconInfo from '../assets/svg/info.svg'; import IconIngestion from '../assets/svg/ingestion.svg'; import IconLineageColor from '../assets/svg/lineage-color.svg'; @@ -239,6 +241,8 @@ export const Icons = { ANNOUNCEMENT: 'icon-announcement', ANNOUNCEMENT_WHITE: 'icon-announcement-white', CHEVRON_DOWN: 'icon-chevron-down', + ICON_UP: 'icon-up', + ICON_DOWN: 'icon-down', }; const SVGIcons: FunctionComponent = ({ @@ -690,6 +694,14 @@ const SVGIcons: FunctionComponent = ({ case Icons.CHEVRON_DOWN: IconComponent = IconChevronDown; + break; + case Icons.ICON_DOWN: + IconComponent = IconDown; + + break; + case Icons.ICON_UP: + IconComponent = IconUp; + break; default: