From bab9d35f605866e125b1c81195bf23e91e115608 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:52:51 +0530 Subject: [PATCH 01/63] support table details customization page --- .../DomainLabelV2/DomainLabelV2.tsx | 10 +- .../SchemaTab/SchemaTab.component.tsx | 49 +-- .../SchemaTab/SchemaTab.interfaces.ts | 3 - .../Database/SchemaTab/SchemaTab.test.tsx | 8 +- .../SchemaTable/SchemaTable.component.tsx | 58 ++- .../SchemaTable/SchemaTable.interface.ts | 5 +- .../Database/SchemaTable/SchemaTable.test.tsx | 75 ++-- .../TableSchemaTab/TableSchemaTab.tsx | 393 +++++++++++++++++ .../CustomizeTabWidget/CustomizeTabWidget.tsx | 12 +- .../SynonymsWidget/GenericWidget.tsx | 278 ++++++------ .../GlossaryDetails.component.tsx | 2 +- .../tabs/GlossaryOverviewTab.component.tsx | 12 +- .../AddDetailsPageWidgetModal.tsx | 29 +- .../CustomiseGlossaryTermDetailPage.tsx | 21 +- .../constants/CustomizeWidgets.constants.ts | 40 +- .../ui/src/enums/CustomizeDetailPage.enum.ts | 2 + .../CustomizablePage/CustomizablePage.tsx | 5 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 408 +++++------------- .../CustomizeGlossaryTermBaseClass.ts} | 52 ++- .../utils/CustomizePage/CustomizePageUtils.ts | 37 +- .../utils/GlossaryTerm/GlossaryTermUtil.tsx | 13 +- .../resources/ui/src/utils/TableClassBase.ts | 40 ++ .../resources/ui/src/utils/TableUtils.tsx | 4 +- 23 files changed, 884 insertions(+), 672 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx rename openmetadata-ui/src/main/resources/ui/src/utils/{CustomiseGlossaryTermPage/CustomizeGlossaryTermPage.ts => CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts} (93%) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx index 80a9fec26998..94d30616c14f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx @@ -43,14 +43,14 @@ export const DomainLabelV2 = < id: string; fullyQualifiedName: string; } ->( - props: Partial -) => { - const { data, permissions, type: entityType } = useGenericContext(); +>({ + hasPermission, + ...props +}: Partial) => { + const { data, type: entityType } = useGenericContext(); const { id: entityId, fullyQualifiedName: entityFqn, domain } = data; const { t } = useTranslation(); const [activeDomain, setActiveDomain] = useState([]); - const hasPermission = permissions.EditAll; const handleDomainSave = useCallback( async (selectedDomain: EntityReference | EntityReference[]) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx index 89e415e026e1..f2ffee1bbb0e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx @@ -11,21 +11,20 @@ * limitations under the License. */ +import { Col, Row } from 'antd'; import { t } from 'i18next'; import { lowerCase } from 'lodash'; -import React, { Fragment, FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import Searchbar from '../../common/SearchBarComponent/SearchBar.component'; import SchemaTable from '../SchemaTable/SchemaTable.component'; import { Props } from './SchemaTab.interfaces'; const SchemaTab: FunctionComponent = ({ - table, onUpdate, hasDescriptionEditAccess, hasTagEditAccess, onThreadLinkSelect, isReadOnly = false, - testCaseSummary, }: Props) => { const [searchText, setSearchText] = useState(''); @@ -34,29 +33,27 @@ const SchemaTab: FunctionComponent = ({ }; return ( - -
-
- -
-
- -
+ + + + + + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts index f4c3bcc2d27d..51e575ed2e4f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts @@ -13,14 +13,11 @@ import { ThreadType } from '../../../generated/api/feed/createThread'; import { Table } from '../../../generated/entity/data/table'; -import { TestSummary } from '../../../generated/tests/testCase'; export type Props = { - table?: Table; hasDescriptionEditAccess: boolean; hasTagEditAccess: boolean; isReadOnly?: boolean; - testCaseSummary?: TestSummary; onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; onUpdate: (columns: Table['columns']) => Promise; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx index aab789455c02..3a61fea225ba 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx @@ -27,6 +27,13 @@ jest.mock('../SchemaTable/SchemaTable.component', () => { return jest.fn().mockReturnValue(

SchemaTable

); }); +jest.mock('../../GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockReturnValue({ + data: MOCK_TABLE, + permissions: {}, + }), +})); + describe('Test SchemaTab Component', () => { it('Renders all the parts of the schema tab', () => { const { queryByTestId, container } = render( @@ -34,7 +41,6 @@ describe('Test SchemaTab Component', () => { hasDescriptionEditAccess hasTagEditAccess isReadOnly={false} - table={MOCK_TABLE} onThreadLinkSelect={jest.fn()} onUpdate={mockUpdate} />, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index ddc560a9579c..dc85048f40e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -40,17 +40,17 @@ import { COLUMN_CONSTRAINT_TYPE_OPTIONS, TABLE_SCROLL_VALUE, } from '../../../constants/Table.constants'; -import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; -import { - OperationPermission, - ResourceEntity, -} from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType, FqnPart } from '../../../enums/entity.enum'; -import { Column } from '../../../generated/entity/data/table'; +import { + Column, + Table as TableType, +} from '../../../generated/entity/data/table'; +import { TestSummary } from '../../../generated/tests/testCase'; import { TagSource } from '../../../generated/type/schema'; import { TagLabel } from '../../../generated/type/tagLabel'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; +import { getTestCaseExecutionSummary } from '../../../rest/testAPI'; import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils'; import { getEntityName, @@ -69,10 +69,10 @@ import { prepareConstraintIcon, updateFieldTags, } from '../../../utils/TableUtils'; -import { showErrorToast } from '../../../utils/ToastUtils'; import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTablePlaceHolder'; import Table from '../../common/Table/Table'; import TestCaseStatusSummaryIndicator from '../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component'; import { EntityName, @@ -93,13 +93,23 @@ const SchemaTable = ({ hasDescriptionEditAccess, hasTagEditAccess, isReadOnly = false, - table, - testCaseSummary, onUpdate, onThreadLinkSelect, }: SchemaTableProps) => { const { theme } = useApplicationStore(); const { t } = useTranslation(); + const [testCaseSummary, setTestCaseSummary] = useState(); + const [searchedColumns, setSearchedColumns] = useState([]); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); + + const [editColumn, setEditColumn] = useState(); + + const { fqn: decodedEntityFqn } = useFqn(); + + const [editColumnDisplayName, setEditColumnDisplayName] = useState(); + const { permissions: tablePermissions, data: table } = + useGenericContext(); + const { testCaseCounts, tableColumns, joins, tableConstraints } = useMemo( () => ({ testCaseCounts: testCaseSummary?.columnTestSummary ?? [], @@ -110,17 +120,6 @@ const SchemaTable = ({ [table, testCaseSummary] ); - const [searchedColumns, setSearchedColumns] = useState([]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [tablePermissions, setTablePermissions] = - useState(); - const [editColumn, setEditColumn] = useState(); - - const { fqn: decodedEntityFqn } = useFqn(); - - const [editColumnDisplayName, setEditColumnDisplayName] = useState(); - const { getEntityPermissionByFqn } = usePermissionProvider(); - const tableFqn = useMemo( () => getPartialNameFromTableFQN( @@ -136,19 +135,12 @@ const SchemaTable = ({ [tableColumns] ); - const fetchResourcePermission = async (entityFqn: string) => { + const fetchTestCaseSummary = async () => { try { - const permissions = await getEntityPermissionByFqn( - ResourceEntity.TABLE, - entityFqn - ); - setTablePermissions(permissions); + const response = await getTestCaseExecutionSummary(table?.testSuite?.id); + setTestCaseSummary(response); } catch (error) { - showErrorToast( - t('server.fetch-entity-permissions-error', { - entity: entityFqn, - }) - ); + setTestCaseSummary(undefined); } }; @@ -164,9 +156,7 @@ const SchemaTable = ({ ); useEffect(() => { - if (!isEmpty(tableFqn)) { - fetchResourcePermission(tableFqn); - } + fetchTestCaseSummary(); }, [tableFqn]); const handleEditColumn = (column: Column): void => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts index 61ac837633a0..1982535bb3cb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts @@ -13,8 +13,7 @@ import { ReactNode } from 'react'; import { ThreadType } from '../../../generated/api/feed/createThread'; -import { Column, Table } from '../../../generated/entity/data/table'; -import { TestSummary } from '../../../generated/tests/testCase'; +import { Column } from '../../../generated/entity/data/table'; export interface SchemaTableProps { hasDescriptionEditAccess: boolean; @@ -23,8 +22,6 @@ export interface SchemaTableProps { isReadOnly?: boolean; onUpdate: (columns: Column[]) => Promise; onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; - table?: Table; - testCaseSummary?: TestSummary; } export type TableCellRendered = ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx index 4625e2a293dc..5ed502eeea51 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx @@ -17,6 +17,7 @@ import { MemoryRouter } from 'react-router-dom'; import { Column } from '../../../generated/entity/data/container'; import { Table } from '../../../generated/entity/data/table'; import { MOCK_TABLE } from '../../../mocks/TableData.mock'; +import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import EntityTableV1 from './SchemaTable.component'; import { SchemaTableProps } from './SchemaTable.interface'; @@ -29,7 +30,7 @@ const mockTableConstraints = [ columns: ['address_id', 'shop_id'], }, ] as Table['tableConstraints']; -const columns = [ +const mockColumns = [ { name: 'comments', dataType: 'STRING', @@ -74,9 +75,23 @@ const mockEntityTableProp: SchemaTableProps = { hasTagEditAccess: true, onThreadLinkSelect, onUpdate, - table: { ...MOCK_TABLE, columns, tableConstraints: mockTableConstraints }, }; +const mockGenericContextProps = { + data: { + ...MOCK_TABLE, + columns: mockColumns, + tableConstraints: mockTableConstraints, + } as Table, + permissions: DEFAULT_ENTITY_PERMISSION, +}; + +jest.mock('../../GenericProvider/GenericProvider', () => ({ + useGenericContext: jest + .fn() + .mockImplementation(() => mockGenericContextProps), +})); + const columnsWithDisplayName = [ { name: 'comments', @@ -166,6 +181,10 @@ jest.mock('../../../constants/Table.constants', () => ({ }, })); +jest.mock('../../../rest/testAPI', () => ({ + getTestCaseExecutionSummary: jest.fn().mockResolvedValue({}), +})); + describe('Test EntityTable Component', () => { it('Initially, Table should load', async () => { render(, { @@ -184,7 +203,7 @@ describe('Test EntityTable Component', () => { wrapper: MemoryRouter, }); - const tableTags = screen.getAllByText('TableTags'); + const tableTags = await screen.findAllByText('TableTags'); expect(tableTags).toHaveLength(6); @@ -194,15 +213,10 @@ describe('Test EntityTable Component', () => { }); it('Table should load empty when no data present', async () => { - render( - , - { - wrapper: MemoryRouter, - } - ); + mockGenericContextProps.data = { ...MOCK_TABLE, columns: [] } as Table; + render(, { + wrapper: MemoryRouter, + }); const entityTable = await screen.findByTestId('entity-table'); @@ -214,6 +228,10 @@ describe('Test EntityTable Component', () => { }); it('should render column name only if displayName is not present', async () => { + mockGenericContextProps.data = { + ...MOCK_TABLE, + columns: mockColumns, + } as Table; render(, { wrapper: MemoryRouter, }); @@ -228,15 +246,13 @@ describe('Test EntityTable Component', () => { }); it('should render column name & displayName for column if both presents', async () => { - render( - , - { - wrapper: MemoryRouter, - } - ); + mockGenericContextProps.data = { + ...MOCK_TABLE, + columns: columnsWithDisplayName, + } as Table; + render(, { + wrapper: MemoryRouter, + }); const columnDisplayName = await screen.findAllByTestId( 'column-display-name' @@ -251,16 +267,13 @@ describe('Test EntityTable Component', () => { }); it('should not render edit displayName button is table is deleted', async () => { - render( - , - { - wrapper: MemoryRouter, - } - ); + mockGenericContextProps.data = { + ...MOCK_TABLE, + columns: columnsWithDisplayName, + } as Table; + render(, { + wrapper: MemoryRouter, + }); expect( screen.queryByTestId('edit-displayName-button') diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx new file mode 100644 index 000000000000..ca5487460a11 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx @@ -0,0 +1,393 @@ +/* + * Copyright 2024 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 { AxiosError } from 'axios'; +import { isEmpty, isEqual, noop } from 'lodash'; +import { EntityTags } from 'Models'; +import React, { useCallback, useMemo, useState } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs, EntityType } from '../../../enums/entity.enum'; +import { CreateThread } from '../../../generated/api/feed/createThread'; +import { + Table, + TagLabel, + TagSource, +} from '../../../generated/entity/data/table'; +import { ThreadType } from '../../../generated/entity/feed/thread'; +import { Page, PageType, Tab } from '../../../generated/system/ui/page'; +import { useFqn } from '../../../hooks/useFqn'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; +import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; +import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; +import { postThread } from '../../../rest/feedsAPI'; +import customizeGlossaryTermPageClassBase from '../../../utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; +import { getEntityName } from '../../../utils/EntityUtils'; +import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import { + getJoinsFromTableJoins, + getTagsWithoutTier, + getTierTags, +} from '../../../utils/TableUtils'; +import { createTagObject } from '../../../utils/TagsUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; +import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; +import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; +import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; +import SchemaTab from '../SchemaTab/SchemaTab.component'; + +const ReactGridLayout = WidthProvider(RGL); + +export const TableSchemaTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + const { fqn: tableFqn } = useFqn(); + const [isEdit, setIsEdit] = useState(false); + const [threadLink, setThreadLink] = useState(''); + const [threadType, setThreadType] = useState( + ThreadType.Conversation + ); + const { t } = useTranslation(); + const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); + + const onThreadPanelClose = () => { + setThreadLink(''); + }; + const { + data: tableDetails, + permissions: tablePermissions, + onUpdate, + type: entityType, + } = useGenericContext(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.Table + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); + } + }, [currentPersonaDocStore, tab]); + const { + tier, + tableTags, + deleted, + columns, + owners, + domain, + extension, + tablePartition, + dataProducts, + description, + entityName, + joinedTables = [], + } = useMemo(() => { + const { tags } = tableDetails; + + const { joins } = tableDetails ?? {}; + + return { + ...tableDetails, + tier: getTierTags(tags ?? []), + tableTags: getTagsWithoutTier(tags ?? []), + entityName: getEntityName(tableDetails), + joinedTables: getJoinsFromTableJoins(joins), + }; + }, [tableDetails, tableDetails?.tags]); + + const { + editTagsPermission, + editDescriptionPermission, + editCustomAttributePermission, + editAllPermission, + viewAllPermission, + } = useMemo( + () => ({ + editTagsPermission: + (tablePermissions.EditTags || tablePermissions.EditAll) && !deleted, + editDescriptionPermission: + (tablePermissions.EditDescription || tablePermissions.EditAll) && + !deleted, + editCustomAttributePermission: + (tablePermissions.EditAll || tablePermissions.EditCustomFields) && + !deleted, + editAllPermission: tablePermissions.EditAll && !deleted, + editLineagePermission: + (tablePermissions.EditAll || tablePermissions.EditLineage) && !deleted, + viewSampleDataPermission: + tablePermissions.ViewAll || tablePermissions.ViewSampleData, + viewQueriesPermission: + tablePermissions.ViewAll || tablePermissions.ViewQueries, + viewProfilerPermission: + tablePermissions.ViewAll || + tablePermissions.ViewDataProfile || + tablePermissions.ViewTests, + viewAllPermission: tablePermissions.ViewAll, + viewBasicPermission: + tablePermissions.ViewAll || tablePermissions.ViewBasic, + }), + [tablePermissions, deleted] + ); + + const onDescriptionEdit = useCallback(() => setIsEdit(true), []); + const onCancel = useCallback(() => setIsEdit(false), []); + + const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { + setThreadLink(link); + if (threadType) { + setThreadType(threadType); + } + }; + + const createThread = async (data: CreateThread) => { + try { + await postThread(data); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.create-entity-error', { + entity: t('label.conversation'), + }) + ); + } + }; + + const descriptionWidget = useMemo(() => { + return ( + { + if (value !== description) { + await onUpdate({ ...tableDetails, description: value }); + } else { + onCancel(); + } + }} + onThreadLinkSelect={onThreadLinkSelect} + /> + ); + }, [tablePermissions, tableDetails, deleted, entityName, deleted, owners]); + + /** + * Formulates updated tags and updates table entity data for API call + * @param selectedTags + */ + const handleTagsUpdate = async (selectedTags?: Array) => { + if (selectedTags && tableDetails) { + const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; + const updatedTable = { ...tableDetails, tags: updatedTags }; + await onUpdate(updatedTable); + } + }; + + const handleTagSelection = async (selectedTags: EntityTags[]) => { + const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); + await handleTagsUpdate(updatedTags); + }; + + const tagsWidget = useMemo(() => { + return ( + + ); + }, []); + + const glossaryWidget = useMemo(() => { + return ( + + ); + }, []); + + const onColumnsUpdate = async (updateColumns: Table['columns']) => { + if (tableDetails && !isEqual(columns, updateColumns)) { + const updatedTableDetails = { + ...tableDetails, + columns: updateColumns, + }; + await onUpdate(updatedTableDetails); + } + }; + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DESCRIPTION)) { + return descriptionWidget; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { + return ( + + ); + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) + ) { + return ( + + await onUpdate({ ...tableDetails, tableConstraints: constraint }) + } + /> + ); + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) + ) { + return isEmpty(joinedTables) ? null : ( + + ); + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.DATA_PRODUCTS) + ) { + return ( + + ); + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TAGS)) { + return tagsWidget; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.GLOSSARY_TERMS) + ) { + return glossaryWidget; + } else if ( + // widgetConfig.i.startsWith(DetailPageWidgetKeys.KNOWLEDGE_ARTICLES) + // ) { + // return ( + // + // ); + // } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.CUSTOM_PROPERTIES) + ) { + return ( + + isRenderedInRightPanel + entityDetails={extension} + entityType={EntityType.TABLE} + handleExtensionUpdate={onUpdate} + hasEditAccess={Boolean(editCustomAttributePermission)} + hasPermission={Boolean(viewAllPermission)} + maxDataCap={5} + /> + ); + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS) + ) { + return tablePartition ? ( + + ) : null; + } + + return getWidgetFromKey({ + widgetConfig: widgetConfig, + handleOpenAddWidgetModal: noop, + handlePlaceholderWidgetKey: noop, + handleRemoveWidget: noop, + isEditView: false, + }); + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout, descriptionWidget, tagsWidget, glossaryWidget]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + {threadLink ? ( + + ) : null} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx index 1047b18c2b5b..c29fd8e03ec0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -28,7 +28,6 @@ import { WidgetConfig, } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; -import customizeGlossaryTermPageClassBase from '../../../../utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage'; import { getAddWidgetHandler, getLayoutUpdateHandler, @@ -36,6 +35,7 @@ import { getRemoveWidgetHandler, getUniqueFilteredLayout, } from '../../../../utils/CustomizableLandingPageUtils'; + import { getCustomizableWidgetByPage, getDefaultTabs, @@ -220,7 +220,7 @@ export const CustomizeTabWidget = () => { newWidgetData as unknown as Document, placeholderWidgetKey, widgetSize, - customizeGlossaryTermPageClassBase.detailPageMaxGridSize + 8 ) ); setIsWidgetModalOpen(false); @@ -288,18 +288,14 @@ export const CustomizeTabWidget = () => { className="grid-container" cols={8} draggableHandle=".drag-widget-icon" - margin={[ - customizeGlossaryTermPageClassBase.detailPageWidgetMargin, - customizeGlossaryTermPageClassBase.detailPageWidgetMargin, - ]} - rowHeight={customizeGlossaryTermPageClassBase.detailPageRowHeight} + margin={[16, 16]} + rowHeight={100} onLayoutChange={handleLayoutUpdate}> {widgets} {currentPageType && ( setIsWidgetModalOpen(false)} maxGridSizeSupport={8} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index e89d8da90c2c..59ed77d8dc17 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -25,6 +25,7 @@ import { TagSource } from '../../../../generated/type/tagLabel'; import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; +import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; import { getJoinsFromTableJoins } from '../../../../utils/TableUtils'; import { ExtensionTable } from '../../../common/CustomPropertyTable/ExtensionTable'; @@ -34,6 +35,7 @@ import RichTextEditorPreviewer from '../../../common/RichTextEditor/RichTextEdit import TagButton from '../../../common/TagButton/TagButton.component'; import SchemaTable from '../../../Database/SchemaTable/SchemaTable.component'; import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; +import { GenericProvider } from '../../../GenericProvider/GenericProvider'; import TagsViewer from '../../../Tag/TagsViewer/TagsViewer'; import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; @@ -221,142 +223,146 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { return ( - noop()} - /> + + data={{ + ...tableClassBase.getDummyData(), + columns: [ + { + name: 'address_id', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: 'Unique identifier for the address.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', + tags: [], + ordinalPosition: 1, + }, + { + name: 'shop_id', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: + 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim_shop table.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.shop_id', + tags: [], + ordinalPosition: 2, + }, + { + name: 'first_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'First name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.first_name', + tags: [], + ordinalPosition: 3, + }, + { + name: 'last_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Last name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.last_name', + tags: [], + ordinalPosition: 4, + }, + { + name: 'address', + dataType: DataType.Varchar, + dataLength: 500, + dataTypeDisplay: 'varchar', + description: 'Clean address test', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address', + tags: [], + ordinalPosition: 5, + }, + { + name: 'company', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: + "The name of the customer's business, if one exists.", + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.company', + tags: [], + ordinalPosition: 7, + }, + { + name: 'city', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'The name of the city. For example, Palo Alto.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.city', + tags: [], + ordinalPosition: 8, + }, + { + name: 'region', + dataType: DataType.Varchar, + dataLength: 512, + dataTypeDisplay: 'varchar', + description: + // eslint-disable-next-line max-len + 'The name of the region, such as a province or state, where the customer is located. For example, Ontario or New York. This column is the same as CustomerAddress.province in the Admin API.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.region', + tags: [], + ordinalPosition: 9, + }, + { + name: 'zip', + dataType: DataType.Varchar, + dataLength: 10, + dataTypeDisplay: 'varchar', + description: 'The ZIP or postal code. For example, 90210.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.zip', + tags: [], + ordinalPosition: 10, + }, + { + name: 'country', + dataType: DataType.Varchar, + dataLength: 50, + dataTypeDisplay: 'varchar', + description: + 'The full name of the country. For example, Canada.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.country', + tags: [], + ordinalPosition: 11, + }, + { + name: 'phone', + dataType: DataType.Varchar, + dataLength: 15, + dataTypeDisplay: 'varchar', + description: 'The phone number of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.phone', + tags: [], + ordinalPosition: 12, + }, + ], + }} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.TABLE} + onUpdate={async () => noop()}> + noop()} + /> + ); } else if ( props.widgetKey.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 013b9e62d22a..d00726afbd54 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -98,7 +98,7 @@ const GlossaryDetails = ({ return customizeGlossaryPageClassBase.getDefaultWidgetForTab(activeTab); } - const page = currentPersonaDocStore?.data?.pages.find( + const page = currentPersonaDocStore?.data?.pages?.find( (p: Page) => p.pageType === PageType.Glossary ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index e9fb9bc11c8d..6bff51c34d78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -25,7 +25,7 @@ import { TagLabel, TagSource } from '../../../../generated/type/tagLabel'; import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection'; import { WidgetConfig } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; -import customizeGlossaryTermPageClassBase from '../../../../utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage'; +import customizeGlossaryTermPageClassBase from '../../../../utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityVersionByField, @@ -33,8 +33,8 @@ import { } from '../../../../utils/EntityVersionUtils'; import { getWidgetFromKey } from '../../../../utils/GlossaryTerm/GlossaryTermUtil'; import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; -import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; +import { DomainLabelV2 } from '../../../DataAssets/DomainLabelV2/DomainLabelV2'; import { OwnerLabelV2 } from '../../../DataAssets/OwnerLabelV2/OwnerLabelV2'; import { ReviewerLabelV2 } from '../../../DataAssets/ReviewerLabelV2/ReviewerLabelV2'; import { useGenericContext } from '../../../GenericProvider/GenericProvider'; @@ -79,7 +79,7 @@ const GlossaryOverviewTab = ({ return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); } const pageType = isGlossary ? PageType.Glossary : PageType.GlossaryTerm; - const page = currentPersonaDocStore?.data?.pages.find( + const page = currentPersonaDocStore?.data?.pages?.find( (p: Page) => p.pageType === pageType ); @@ -195,12 +195,8 @@ const GlossaryOverviewTab = ({ const domainWidget = useMemo(() => { return ( - diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal.tsx index 0b19ee104540..4169f4279aac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal.tsx @@ -11,12 +11,10 @@ * limitations under the License. */ -import { CheckOutlined } from '@ant-design/icons'; -import { Modal, Space, Tabs, TabsProps } from 'antd'; -import { isEmpty, toString } from 'lodash'; +import { Modal, Tabs, TabsProps } from 'antd'; +import { isEmpty, sortBy, toString } from 'lodash'; import { default as React, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { LIGHT_GREEN_COLOR } from '../../../../constants/constants'; import { CommonWidgetType, GridSizes, @@ -33,7 +31,6 @@ interface Props { open: boolean; maxGridSizeSupport: number; placeholderWidgetKey: string; - addedWidgetsList: Array; handleCloseAddWidgetModal: () => void; handleAddWidget: ( widget: CommonWidgetType, @@ -46,7 +43,6 @@ interface Props { function AddDetailsPageWidgetModal({ open, widgetsList, - addedWidgetsList, handleCloseAddWidgetModal, handleAddWidget, maxGridSizeSupport, @@ -66,7 +62,7 @@ function AddDetailsPageWidgetModal({ const tabItems: TabsProps['items'] = useMemo( () => - widgetsList?.map((widget) => { + sortBy(widgetsList, 'name')?.map((widget) => { const widgetSizeOptions: Array = widget.data.gridSizes.map((size: GridSizes) => ({ label: ( @@ -79,20 +75,9 @@ function AddDetailsPageWidgetModal({ return { label: ( - - {widget.name} - {addedWidgetsList.some( - (w) => - w.startsWith(widget.fullyQualifiedName) && - !w.includes('EmptyWidgetPlaceholder') - ) && ( - - )} - + + {widget.name} + ), key: widget.fullyQualifiedName, children: ( @@ -105,7 +90,7 @@ function AddDetailsPageWidgetModal({ ), }; }), - [widgetsList, addedWidgetsList, getAddWidgetHandler, maxGridSizeSupport] + [widgetsList, getAddWidgetHandler, maxGridSizeSupport] ); const widgetsInfo = useMemo(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx index 772fcfc3174b..f67234918b24 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx @@ -11,19 +11,13 @@ * limitations under the License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import gridBgImg from '../../../../assets/img/grid-bg-img.png'; import { Page } from '../../../../generated/system/ui/page'; import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; import '../../../../pages/MyDataPage/my-data.less'; -import customizeGlossaryTermPageClassBase from '../../../../utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage'; -import { - getLayoutWithEmptyWidgetPlaceholder, - getUniqueFilteredLayout, -} from '../../../../utils/CustomizableLandingPageUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { CustomizeTabWidget } from '../../../Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; import { GlossaryHeaderWidget } from '../../../Glossary/GlossaryHeader/GlossaryHeaderWidget'; @@ -39,26 +33,13 @@ function CustomizeGlossaryTermDetailPage({ const { t } = useTranslation(); const { currentPage, currentPageType } = useCustomizeStore(); - const [layout, setLayout] = useState>( - (currentPage?.layout as WidgetConfig[]) ?? - customizeGlossaryTermPageClassBase.defaultLayout - ); - const handleReset = useCallback(async () => { - // Get default layout with the empty widget added at the end - const newMainPanelLayout = getLayoutWithEmptyWidgetPlaceholder( - customizeGlossaryTermPageClassBase.defaultLayout, - 2, - 4 - ); - setLayout(newMainPanelLayout); await onSaveLayout(); }, []); const handleSave = async () => { await onSaveLayout({ ...(currentPage ?? ({ pageType: currentPageType } as Page)), - layout: getUniqueFilteredLayout(layout), }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts index 233d4e00a9d5..5f8bd18798f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts @@ -43,7 +43,7 @@ export const TAGS_WIDGET: CommonWidgetType = { export const GLOSSARY_TERMS_WIDGET: CommonWidgetType = { fullyQualifiedName: DetailPageWidgetKeys.GLOSSARY_TERMS, - name: i18n.t('label.tag-plural'), + name: i18n.t('label.glossary-term'), data: { gridSizes: ['small'] }, }; @@ -64,3 +64,41 @@ export const OWNER_WIDGET: CommonWidgetType = { name: i18n.t('label.owner'), data: { gridSizes: ['small'] }, }; + +export const TERMS_TABLE_WIDGET: CommonWidgetType = { + fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.TERMS_TABLE, + name: i18n.t('label.terms'), + data: { gridSizes: ['large'] }, +}; + +export const REFERENCES_WIDGET: CommonWidgetType = { + fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.REFERENCES, + name: i18n.t('label.reference-plural'), + data: { gridSizes: ['small'] }, +}; + +export const REVIEWER_WIDGET: CommonWidgetType = { + fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.REVIEWER, + name: i18n.t('label.reviewer'), + data: { gridSizes: ['small'] }, +}; + +export const SYNONYMS_WIDGET: CommonWidgetType = { + fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.SYNONYMS, + name: i18n.t('label.synonym-plural'), + data: { + gridSizes: ['small'], + }, +}; + +export const RELATED_TERMS_WIDGET: CommonWidgetType = { + fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.RELATED_TERMS, + name: i18n.t('label.related-term-plural'), + data: { gridSizes: ['small'] }, +}; + +export const DATA_PRODUCTS_WIDGET: CommonWidgetType = { + fullyQualifiedName: DetailPageWidgetKeys.DATA_PRODUCTS, + name: i18n.t('label.data-product-plural'), + data: { gridSizes: ['small'] }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index ee626b72d07a..e89704dbbebd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -29,7 +29,9 @@ export enum DetailPageWidgetKeys { DOMAIN = 'KnowledgePanel.Domain', GLOSSARY_TERMS = 'KnowledgePanel.GlossaryTerms', CUSTOM_PROPERTIES = 'KnowledgePanel.CustomProperties', + TABLE_CONSTRAINTS = 'KnowledgePanel.TableConstraints', EMPTY_WIDGET_PLACEHOLDER = 'ExtraWidget.EmptyWidgetPlaceholder', + PARTITIONED_KEYS = 'KnowledgePanel.PartitionedKeys', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx index 3f3bf34da1f1..91d6f7eadd97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx @@ -180,7 +180,10 @@ export const CustomizablePage = () => { name: `${personaDetails.name}-${personaFQN}`, fullyQualifiedName: pageLayoutFQN, entityType: EntityType.PAGE, - data: {}, + data: { + pages: [], + navigation: [], + }, }); setCurrentPageType(pageFqn as PageType); } else { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 9d1325ac1080..b1a4dfcd34a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -12,28 +12,23 @@ * limitations under the License. */ -import { Col, Row, Space, Tabs, Tooltip } from 'antd'; +import { Col, Row, Tabs, Tooltip } from 'antd'; import { AxiosError } from 'axios'; -import classNames from 'classnames'; import { compare } from 'fast-json-patch'; -import { isEmpty, isEqual, isUndefined } from 'lodash'; +import { isUndefined } from 'lodash'; import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useHistory, useParams } from 'react-router-dom'; import { ReactComponent as RedAlertIcon } from '../../assets/svg/ic-alert-red.svg'; -import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import { withSuggestions } from '../../components/AppRouter/withSuggestions'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import SchemaTab from '../../components/Database/SchemaTab/SchemaTab.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; +import { TableSchemaTab } from '../../components/Database/TableSchemaTab/TableSchemaTab'; +import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -44,7 +39,6 @@ import { } from '../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; import { mockDatasetData } from '../../constants/mockTourData.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -59,11 +53,10 @@ import { FqnPart, TabSpecificField, } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { Table, TableType } from '../../generated/entity/data/table'; import { Suggestion } from '../../generated/entity/feed/suggestion'; -import { ThreadType } from '../../generated/entity/feed/thread'; +import { Page, PageType } from '../../generated/system/ui/page'; import { TestSummary } from '../../generated/tests/testCase'; import { TagLabel } from '../../generated/type/tagLabel'; import LimitWrapper from '../../hoc/LimitWrapper'; @@ -71,7 +64,7 @@ import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useSub } from '../../hooks/usePubSub'; import { FeedCounts } from '../../interface/feed.interface'; -import { postThread } from '../../rest/feedsAPI'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getDataQualityLineage } from '../../rest/lineageAPI'; import { getQueriesList } from '../../rest/queryAPI'; import { @@ -93,6 +86,7 @@ import { defaultFields } from '../../utils/DatasetDetailsUtils'; import EntityLink from '../../utils/EntityLink'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; +import { getGlossaryTermDetailTabs } from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import tableClassBase from '../../utils/TableClassBase'; import { @@ -100,16 +94,14 @@ import { getTagsWithoutTier, getTierTags, } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; -import { FrequentlyJoinedTables } from './FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import './table-details-page-v1.less'; -import TableConstraints from './TableConstraints/TableConstraints'; const TableDetailsPageV1: React.FC = () => { const { isTourOpen, activeTabForTourDatasetPage, isTourPage } = useTourProvider(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const [tableDetails, setTableDetails] = useState
(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); @@ -120,11 +112,7 @@ const TableDetailsPageV1: React.FC = () => { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [isEdit, setIsEdit] = useState(false); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); + const [queryCount, setQueryCount] = useState(0); const [loading, setLoading] = useState(!isTourOpen); @@ -133,6 +121,7 @@ const TableDetailsPageV1: React.FC = () => { ); const [testCaseSummary, setTestCaseSummary] = useState(); const [dqFailureCount, setDqFailureCount] = useState(0); + const [customizedPage, setCustomizedPage] = useState(null); const tableFqn = useMemo( () => @@ -279,23 +268,12 @@ const TableDetailsPageV1: React.FC = () => { } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const onCancel = () => { - setIsEdit(false); - }; - - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { - tier, tableTags, deleted, version, followers = [], - description, entityName, - joinedTables = [], id: tableId = '', } = useMemo(() => { if (tableDetails) { @@ -446,52 +424,6 @@ const TableDetailsPageV1: React.FC = () => { [tableDetails] ); - const onDescriptionUpdate = async (updatedHTML: string) => { - if (!tableDetails) { - return; - } - if (description !== updatedHTML) { - const updatedTableDetails = { - ...tableDetails, - description: updatedHTML, - }; - await onTableUpdate(updatedTableDetails, 'description'); - setIsEdit(false); - } else { - setIsEdit(false); - } - }; - - const onTableConstraintsUpdate = async ( - updatedTableConstraints: Table['tableConstraints'] - ) => { - if (!tableDetails) { - return; - } - const updatedTableDetails = { - ...tableDetails, - tableConstraints: updatedTableConstraints, - }; - await onTableUpdate(updatedTableDetails, 'tableConstraints'); - }; - - const onColumnsUpdate = async (updateColumns: Table['columns']) => { - if (tableDetails && !isEqual(tableDetails.columns, updateColumns)) { - const updatedTableDetails = { - ...tableDetails, - columns: updateColumns, - }; - await onTableUpdate(updatedTableDetails, 'columns'); - } - }; - - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const handleDisplayNameUpdate = async (data: EntityName) => { if (!tableDetails) { return; @@ -500,23 +432,6 @@ const TableDetailsPageV1: React.FC = () => { await onTableUpdate(updatedTable, 'displayName'); }; - /** - * Formulates updated tags and updates table entity data for API call - * @param selectedTags - */ - const handleTagsUpdate = async (selectedTags?: Array) => { - if (selectedTags && tableDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedTable = { ...tableDetails, tags: updatedTags }; - await onTableUpdate(updatedTable, 'tags'); - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleTagsUpdate(updatedTags); - }; - const onExtensionUpdate = async (updatedData: Table) => { tableDetails && (await onTableUpdate( @@ -529,10 +444,7 @@ const TableDetailsPageV1: React.FC = () => { }; const { - editTagsPermission, - editDescriptionPermission, editCustomAttributePermission, - editAllPermission, editLineagePermission, viewSampleDataPermission, viewQueriesPermission, @@ -567,143 +479,39 @@ const TableDetailsPageV1: React.FC = () => { [tablePermissions, deleted] ); - const schemaTab = useMemo( - () => ( - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - afterSlot={ - - - - } - beforeSlot={ - !isEmpty(joinedTables) ? ( - - ) : null - } - customProperties={tableDetails} - dataProducts={tableDetails?.dataProducts ?? []} - domain={tableDetails?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editTagPermission={editTagsPermission} - entityFQN={tableFqn} - entityId={tableDetails?.id ?? ''} - entityType={EntityType.TABLE} - selectedTags={tableTags} - tablePartition={tableDetails?.tablePartition} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-panel-container entity-resizable-right-panel-container ', - }} - /> - - - ), - [ - isTourPage, - tableTags, - joinedTables, - tableFqn, - isEdit, + const schemaTab = useMemo(() => , []); + + const tabs = useMemo(() => { + // const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = tableClassBase.getTableDetailPageTabs({ + schemaTab, + queryCount, + isTourOpen, + tablePermissions, + activeTab, deleted, tableDetails, - entityName, - onDescriptionEdit, - onDescriptionUpdate, - testCaseSummary, - editTagsPermission, - editDescriptionPermission, - editAllPermission, + totalFeedCount: feedCount.totalCount, + onExtensionUpdate, + getEntityFeedCount, + handleFeedCount, viewAllPermission, editCustomAttributePermission, - ] - ); + viewSampleDataPermission, + viewQueriesPermission, + viewProfilerPermission, + editLineagePermission, + fetchTableDetails, + testCaseSummary, + isViewTableType, + }); - const tabs = useMemo(() => { - return tableClassBase - .getTableDetailPageTabs({ - schemaTab, - queryCount, - isTourOpen, - tablePermissions, - activeTab, - deleted, - tableDetails, - totalFeedCount: feedCount.totalCount, - onExtensionUpdate, - getEntityFeedCount, - handleFeedCount, - viewAllPermission, - editCustomAttributePermission, - viewSampleDataPermission, - viewQueriesPermission, - viewProfilerPermission, - editLineagePermission, - fetchTableDetails, - testCaseSummary, - isViewTableType, - }) - .filter((data) => !data.isHidden); + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.SCHEMA + ); }, [ schemaTab, queryCount, @@ -923,23 +731,6 @@ const TableDetailsPageV1: React.FC = () => { [tableDetails] ); - const onThreadPanelClose = () => { - setThreadLink(''); - }; - - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const updateVote = async (data: QueryVote, id: string) => { try { await updateTablesVotes(id, data); @@ -952,6 +743,24 @@ const TableDetailsPageV1: React.FC = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Table) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + if (loading) { return ; } @@ -971,59 +780,56 @@ const TableDetailsPageV1: React.FC = () => { entity: t('label.table'), })} title="Table details"> - - {/* Entity Heading */} -
- - - {/* Entity Tabs */} - - - - - <> - - {threadLink ? ( - - ) : null} - + { + await saveUpdatedTableData(data); + }}> + + {/* Entity Heading */} + + + + {/* Entity Tabs */} + + + + + <> + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts similarity index 93% rename from openmetadata-ui/src/main/resources/ui/src/utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage.ts rename to openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts index bf22e99fe1d2..37ebf303ee78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomiseGlossaryTermPage/CustomizeGlossaryTermPage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts @@ -23,6 +23,14 @@ import { CommonWidgetType, CUSTOM_PROPERTIES_WIDGET, DESCRIPTION_WIDGET, + DOMAIN_WIDGET, + OWNER_WIDGET, + REFERENCES_WIDGET, + RELATED_TERMS_WIDGET, + REVIEWER_WIDGET, + SYNONYMS_WIDGET, + TAGS_WIDGET, + TERMS_TABLE_WIDGET, } from '../../constants/CustomizeWidgets.constants'; import { GlossaryTermDetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; @@ -236,9 +244,7 @@ class CustomizeGlossaryTermPageClassBase { } > */ - public getWidgetsFromKey( - widgetKey: T - ) { + public getWidgetsFromKey(widgetKey: string) { if (widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.HEADER)) { return GlossaryHeaderWidget; } else if (widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.TABS)) { @@ -360,33 +366,25 @@ class CustomizeGlossaryTermPageClassBase { return []; } - public getCommonWidgetList(): CommonWidgetType[] { - return [ + public getCommonWidgetList(isGlossary: boolean): CommonWidgetType[] { + const commonWidgetList = [ DESCRIPTION_WIDGET, - { - fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.SYNONYMS, - name: 'Synonyms', - data: { - gridSizes: ['small'], - }, - }, - { - fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.RELATED_TERMS, - name: 'Related Terms', - data: { gridSizes: ['small'] }, - }, - { - fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.REFERENCES, - name: 'References', - data: { gridSizes: ['small'] }, - }, - { - fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.REVIEWER, - name: 'Reviewer', - data: { gridSizes: ['small'] }, - }, + TERMS_TABLE_WIDGET, + DOMAIN_WIDGET, + REFERENCES_WIDGET, + REVIEWER_WIDGET, CUSTOM_PROPERTIES_WIDGET, + TAGS_WIDGET, ]; + + return isGlossary + ? commonWidgetList + : [ + ...commonWidgetList, + OWNER_WIDGET, + SYNONYMS_WIDGET, + RELATED_TERMS_WIDGET, + ]; } } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 31b4498082e0..16d5db2fc205 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -11,35 +11,14 @@ * limitations under the License. */ import { TabsProps } from 'antd'; -import { - CommonWidgetType, - CUSTOM_PROPERTIES_WIDGET, - DESCRIPTION_WIDGET, - DOMAIN_WIDGET, - GLOSSARY_TERMS_WIDGET, - TAGS_WIDGET, -} from '../../constants/CustomizeWidgets.constants'; +import { CommonWidgetType } from '../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../enums/entity.enum'; import { PageType } from '../../generated/system/ui/page'; -import customizeGlossaryTermPageClassBase from '../CustomiseGlossaryTermPage/CustomizeGlossaryTermPage'; -import customizeDetailPageClassBase from '../CustomizeDetailPage/CustomizeDetailPage'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; -import customizeMyDataPageClassBase from '../CustomizeMyDataPageClassBase'; +import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import i18n from '../i18next/LocalUtil'; import tableClassBase from '../TableClassBase'; -export const getDefaultLayout = (pageType: string) => { - switch (pageType) { - case PageType.GlossaryTerm: - return customizeGlossaryTermPageClassBase.defaultLayout; - case PageType.Table: - return customizeDetailPageClassBase.defaultLayout; - case PageType.LandingPage: - default: - return customizeMyDataPageClassBase.defaultLayout; - } -}; - export const getGlossaryTermDefaultTabs = () => { return [ { @@ -229,16 +208,12 @@ export const getCustomizableWidgetByPage = ( switch (pageType) { case PageType.GlossaryTerm: case PageType.Glossary: - return customizeGlossaryTermPageClassBase.getCommonWidgetList(); + return customizeGlossaryTermPageClassBase.getCommonWidgetList( + pageType === PageType.Glossary + ); case PageType.Table: - return [ - DESCRIPTION_WIDGET, - CUSTOM_PROPERTIES_WIDGET, - DOMAIN_WIDGET, - TAGS_WIDGET, - GLOSSARY_TERMS_WIDGET, - ]; + return tableClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx index e88986669211..63f0337fbaa7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx @@ -11,18 +11,17 @@ * limitations under the License. */ import { TabsProps } from 'antd'; -import { isUndefined, uniqueId } from 'lodash'; +import { get, isUndefined, uniqueId } from 'lodash'; import React from 'react'; import EmptyWidgetPlaceholder from '../../components/MyData/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder'; import { SIZE } from '../../enums/common.enum'; import { LandingPageWidgetKeys } from '../../enums/CustomizablePage.enum'; -import { GlossaryTermDetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; import { Document } from '../../generated/entity/docStore/document'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; -import customizeGlossaryTermPageClassBase from '../CustomiseGlossaryTermPage/CustomizeGlossaryTermPage'; import { moveEmptyWidgetToTheEnd } from '../CustomizableLandingPageUtils'; +import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import customizeMyDataPageClassBase from '../CustomizeMyDataPageClassBase'; import { getEntityName } from '../EntityUtils'; @@ -62,14 +61,10 @@ export const getWidgetFromKey = ({ ); } - const widgetKey = customizeGlossaryTermPageClassBase.getKeyFromWidgetName( + const Widget = customizeGlossaryTermPageClassBase.getWidgetsFromKey( widgetConfig.i ); - const Widget = customizeGlossaryTermPageClassBase.getWidgetsFromKey< - typeof widgetKey - >(widgetConfig.i as GlossaryTermDetailPageWidgetKeys); - return ( !get(data, 'isHidden', false)); }; export const getTabLabelMap = (tabs?: Tab[]): Record => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 91e354e073ab..fe60b6a98014 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -11,6 +11,14 @@ * limitations under the License. */ import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; @@ -23,6 +31,7 @@ import { } from '../generated/entity/data/table'; import { TestSummary } from '../generated/tests/testCase'; import { FeedCounts } from '../interface/feed.interface'; +import i18n from './i18next/LocalUtil'; import { getTableDetailPageBaseTabs } from './TableUtils'; export interface TableDetailPageTabProps { @@ -259,6 +268,37 @@ class TableClassBase { deleted: false, }; } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + name: i18n.t('label.frequently-joined-table-plural'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + name: i18n.t('label.table-constraints'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } } const tableClassBase = new TableClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index d860009a9b0d..960d7a7094d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -902,9 +902,7 @@ export const getTableDetailPageBaseTabs = ({ /> ), isHidden: isUndefined(tableDetails?.schemaDefinition), - key: isViewTableType - ? EntityTabs.VIEW_DEFINITION - : EntityTabs.SCHEMA_DEFINITION, + key: EntityTabs.VIEW_DEFINITION, children: , }, { From d99febf98c576e280d9ae1c1b2ec80f03101e2f0 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:15:38 +0530 Subject: [PATCH 02/63] update widget preview --- .../ui/src/assets/img/widgets/Domain.png | Bin 0 -> 11586 bytes .../ui/src/assets/img/widgets/References.png | Bin 0 -> 12326 bytes .../src/assets/img/widgets/RelatedTerms.png | Bin 0 -> 20059 bytes .../ui/src/assets/img/widgets/Reviewers.png | Bin 0 -> 16235 bytes .../ui/src/assets/img/widgets/Synonyms.png | Bin 0 -> 11203 bytes .../ui/src/assets/img/widgets/Terms.png | Bin 0 -> 39925 bytes .../assets/img/widgets/custom_properties.png | Bin 0 -> 32675 bytes .../src/assets/img/widgets/data-products.png | Bin 0 -> 13508 bytes .../assets/img/widgets/description-large.png | Bin 0 -> 95249 bytes .../ui/src/assets/img/widgets/description.png | Bin 0 -> 76430 bytes .../img/widgets/frequently-joined-tables.png | Bin 0 -> 33096 bytes .../src/assets/img/widgets/glossary-term.png | Bin 0 -> 22838 bytes .../ui/src/assets/img/widgets/owners.png | Bin 0 -> 17778 bytes .../assets/img/widgets/table-constraints.png | Bin 0 -> 39223 bytes .../src/assets/img/widgets/table-schema.png | Bin 0 -> 173650 bytes .../ui/src/assets/img/widgets/tags.png | Bin 0 -> 23231 bytes .../SynonymsWidget/GenericWidget.tsx | 190 ++++++++++++++++++ .../constants/CustomizeWidgets.constants.ts | 2 +- .../src/utils/CustomizeMyDataPageClassBase.ts | 56 ++++++ .../resources/ui/src/utils/TableClassBase.ts | 26 ++- 20 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Domain.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/References.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/RelatedTerms.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Reviewers.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Synonyms.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Terms.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/custom_properties.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/data-products.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/description-large.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/description.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/frequently-joined-tables.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/glossary-term.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/owners.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-constraints.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-schema.png create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/tags.png diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Domain.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Domain.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7f03db39e68268119ef95288526ff3de7cad96 GIT binary patch literal 11586 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 CfFGd% literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/References.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/References.png new file mode 100644 index 0000000000000000000000000000000000000000..3c9b58f3a083165535eb61af04747e758ba8f537 GIT binary patch literal 12326 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 literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/RelatedTerms.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/RelatedTerms.png new file mode 100644 index 0000000000000000000000000000000000000000..96aa7f089874aa146770cc281f7da60ce397e480 GIT binary patch literal 20059 zcmeFZWl$W^+wF@(!T>YDodE(QxZB_!G(?aPWC#}A2lruc2qZvo36KN{f#A*{L4yQ$ z2^wHF#4;VUcL4E9+xn z;pAdrVc#br0RA%gY~vRemJX|ivchu&_TMbRM3b(p+gD%l9w@SeV?o`(lAKA(-`nvB zV{yfXmKlc1tA>|NyvwUz^O?QBwdo6Smt;JZ3?9a#7hrK4l4KKN)l?)}9|)5Bd+8`6 zXKiicZL>J^RxanuNU8Ng)8u@`Vnyrqj+r-^rsw%YZOd=hA<2zcpVc^VRo>SP9&05} z3y_=;U`vuJHLQf6^d_+xB+!dHX>8bc{g^au{4(Ln*2l~O8%kjThPi+ml zI>(HuCNMScpdf5T?EihdN&R7-4&>ierlh+ygZG?k&_^lH?T&O|8$KRp@zV1gws;9R zdamAGT~ zzRo!vxb=u1PH5cBkI`8VW@iPqQAHP*2P@N`C3_BUNs$ZEi#^)uLhCuncgL#BwO!?p z=}0@eZR9X^iqF^!%^=RXw`SiT{YW{9H^gxCI8 zgAzQVhk0EBz5c5s?>`NWzH7=SE_s!^6Y5js-ZfkQ(S5zUj`~c2Uvk&f=g~ON?@stw z1^e;c{`dJCE_kePlPI;8*M@wMdxV!|Kq~w4U_-Uf?e%vFRiLvJk=~v(k#JVnd#@Ed|0CY`g?=XFWb{&h<@n=8Esg;}{P*)Yn~M3-N;SrJV(B7+L9P8dv)&WS z_qOJUtqUWW0%3ofdxq@3@^X;XkKjTecJVck+}b%e&5z>Frt_8uiP~ubP)5X3h+XZx zm*#3u0`qa)Y?k*zI#6|Lc)Z9Y*Ahhn{v89C){MXGGx^&4lTA)QjaqAgQsu>;3~u?^ zl>Bq{Kkf60Yj47wP*3|pdt_p zP(Oj7azd?3cP#DJ$)uSsA)|*LRiUrp4B_5%nhy5&n}0jr0jh@hf_<x?&_d= z+B#$XmJJ5UL|e%o_Oe~m@qWT6FR`W=LlKHJJput} zAyppEoSu^lz7<0@o$nG&!;7eDt1J>}mu!eAhpS7u1pnm5ZI{0&cdsO&T|(Xoe8!I$ zN?e@j_C)7Z+jYa$VgDGpE-a~kEzL4sqjA6Ga`Bo^k$d}7@)1VI6gZ^ol`(%=m*MmH zgzw8o>aW&BWmjeTDiQn9Ig?++pAK_rZfY>D)+SA;8=$3Ym9!kn(uN;urhe+l@#P3I z%XTsb4m>T&IYuf1^- z!qxZYV#9sF!YH^K_6z~NL!z`Qddn@F%WRL(f(_m5#?!h=vq!(5 z*}D6eMB;;=#>bL^Dx2Y<9M`}=U3Hu@x7>~g+g<)GQqF-JHsZ}1;oqq?-T9lwjZ8I1 zP5pM?6*;un0Tb`%o?IhN{Gb-9ljJwu#2<0g5O+nBP;uVoXFR+$ z;9D|IXY4D)0}LLT&?h{yQugg^Gz>q4I(<0)uo_Y?hw=%GnObLJd-F)H&ow9D^k?XU zklqdP1%&0z;OVS$P6VH4>~O!8*2PNuyw65fmB5<+q^X}{3$qpYCqvAyI31PXx<97R zBGdd0`0Nim3VF7{o}ahtmQ=~(S@){A6cc3O#vd=xV_Orm``Yx{Q!I>Pj*H@5k-(fx zLHnHe1j^na9cduEavv9Fibiz{Jwf5oIfuUhy*e zJ|@$p%z^GHGE9C{8LmV$s0XtGurvlU~1n6GsqXJr2zsSL$PSkV?3XG^naBf1ZQ=e-vLE+p_)u?ZP| ziH1a8C2<@M3O4mhPp;Ko{;f4TU4bHLx~)znO_lJ3?AB`0#>fMxj0z^owj;TxUTgue zdL=z^U3dl7y{^A{&IS%a@I(AiQpkLx3l>-i$^n)zhpQ-HSdr31)n86@7Q5aIUPUxv z;Hna8wBgL_G3IMbkT2~vpvq@A2&wv;E^4#PeOkr$Le|ZO+cal666=Ia2$td;A+=Z1YE$rgRwOIl*S;K!JqLmF`ae|k5)TUtXFp7( z$N@T!$B|P$hN&1UDMG&C_U5vxCkn~n8a&A@Z?a<4A&f+spFdcBpvVR%gNUS>`r0{d zLM(|0l<&C)&VP#8VN(U)Co+IJYo$Hz7GZS)BT<@Y+p;`|(T=ZNxfgPRsC5sP?+2Uj zoWrT||MjvZ4D=A7$jg1Etx~Np9nR|99T9bUX!iX0EBy~9F?qhoiUNPIB9R?39^y$< zXgzV`@UW0PFEaNJQ*!ZYf}AXw8X=L*p|YePE$@dw)@!0|upaU~c&e^MCel&aCTaTVU}40T zjR6<^qLeL~2rV2n#TCQV6hOoa(?V2%qll9C@VG*;5}uk?2f*z`I=^AKG*ceDq8gn7;if3zXwuTvax3{8 z{PT}4+bWg@wq2~sF!55;mUF&xm27b08f6l>U>2HUg}1b}nF|NCU;pwqPIa1rGPRwW z-Xz-xtLOvJp$S#)I4!cLvvZ1QkGkjgVZOdo%DvFU9dzXnd2@Z;3lc`^_yiZwHlwLN z>~xJfL3E!;M@pwM8vEpHblrd24#ES5u3fGr^j}(H$flCvFLmM4c=^YCx?$d*CEg4x z)0qyoIq!76rBYBvi>lL`_8Pl}V=uBDDzcn>fG?!My1sI@K6LJat<-C{zT#3ilh|Wv|+cU%xEzPSGu}ypYA?I;q|4>dJ`TWPeHFEWGd*h8gz@ z=UniFozF$Cys3U!0K8cxg#fD~uIx^`v7&WDW%y;9ODJc2cRDhiprJ|Lk4K*gJ#cJI z=cKGyhQHX4&fmvm>`nCXkbXXW_~E+*ZAo!VSC8*|8Erow&M#kBDxSXV!k^E8_BCjk zQBUrZ9mF&F?N-*83$HRK1QBhbtT^yiekqX+l4l-7*BFQ2joCLWcGB3~ZK6u&n17(GRTR963TkahWK-xyMc7-+%Zr?Sa@ zGmL1t&YiU+7{p0GGjBM+G6JW@SS5Ir*7l?b>+5t-_TYW51wlsPfR8KBfH(5uzxZ4@ zWFhKG41Oh565{!Vb$Fzq6xd3*35J$&y;GwCFiRrC2^-~-2tqKKeY3@}5Rn)jR|i%I z+DQf_hNql~tAM6@hk}()+CI<+#kn&uy}+~IMN%-y;L&tEMp`>JpG@pdzhTuxbivPZ z4XMd1IPHzP&oasR>GC7p-dIbn%g;dSOd?xp?^~B?hMSoIql~IA(ZelAgHss&i1~wM zw1oO0T?P?aN6|yyx8c*ayn<}q;vBIQZY8ys*UYjLq+RyPu*I#VRzs)MZx{Ubuf6R%YJ|s$rBo{A zWf~2Vn5Agv_z!zF#_6}kdTQJl_3-xkD+aVRKk#X0IKc$V#7_rTUSb}xvR)77f^&(; z!tdeShek&xm(f7%K`}jgA{{XVR0`_2%$F@=y|_KlU$&bNUY_&U_RtGe7B>v(_bTwh zgdtZBbto5{FTFiBv5nkVH+V&|D*lj$8$yiFg{b(W zyNTD#ju<<7Q@(lxs|RPn>HrTa0=7d$(x|FpJs>lVjbJ||w+Ib}>w2Nz63OE+xMmPx zg&UXUc_YiB7*#T%&-=;EB?w*p5av-2yWDAsr;`cs7%Ee>axK&|-JyIKj}NJSE5<}z zhr!=sdFdWSrp1!G3`B8er$yZFD6N}>*`Z^i4521@+2&9Z2MvuGb!jWqWXcYU+oOyF z4%Q{5Oz&DJTQ4ThVaq zZPPX%ng_*Ak~JfTuNmkCd)JB)JXRe8UfYk7<2KdNMc5`e9z9f9WefOrLzzB4mjn0c z_4x|EFTHtr_2vE7(f%x@Bk#ErdEfI9&i}01DnL8O)!KUuEB?^3h^3MC#&FwDBWfbm zQZK>Sk*R#jy2AF+H8_V6wIX;%Wz*+_*|<kni* zyxt3!<_&A#cSh;IPI~wrdFwKEN=Y>z@D~=_$mB$>N0RNUKjU!z$AO_;7Q<0C2|J5ojx>^Ph#8B+my#SF|MCc%ejZ&M?i!4ph|gzd z2Q9BVRg8=X!Wey~uazv^UzR$BSbsq7gu+R`D_+*Wo!=}v54msl_^a_*g{D6DnUcPoQ4JpLEj6hZ z;(b-aQ)XDnT4sDFR910Bsr&sJ8A(!&XGotgvdID@%S{nL84MKsGJ^x5-FoOQ&*SfIJQBeZf*d5-}AkEfcx>xY4UkXFtg)S{LjmVM>-& z2+YcrxcJ+FG{O2wc``-i8C+Nr&DbZ+)IVo{2v7*9S*=#8FJ7^*+7n+p@=p>FJO|q7 z;(mZQ5SIzBwCtIgLRCZARAYN95VC?_h*V>@0J8A!&GR4ltlz~(a!%bGE#%8Cw|{?i zFX+^pz0(ZM#3We%k}&-QGkm zvx4#rbM^~g3<`~izYM+m&`V>W9aKO&x_2k?GmMo92#5!ON7SlJ!89O*1^yx0!omnb zn`uj|tW1D}0?S{AK!Bzc15o3S6~wpyPWX^LN*^u+v@yY`-X*11zitJVLw}GNd1TPG z@wM=>wkMM3+GKgYG_G^8_}4+*l2|k6*5B0Q;XJ``^}8bQz1Wm_m z+p8o`FsT({Dq=0sJ&`neV@qs|%VfSSN9@pZ`ngGX>});NOb37~Y5<#v8wJ!%vBLgG z@`+XRJpGt&dw=z2F){whL46@t&&&{~Z#D(lBbUfzj(?e8FeVN^e!qNAtK#Lm{DPQC zK2|K7=EjLrxBY8H%SRa%?|DxHKz3;!y<9kJss39t>vbtj$9_mp9BXO2O*^wv39@n9 zHOGd!10vRj>Zv93rQr-9nZ5&&67*3~#=5LZDuBk)qwq^--vjeJ^m@;` zRr72yC`js{4R`EOappUG8J~X@xBn_UMpUTR55j{kYlAeM19o0D9(J5Hoy(o&HtO)M zKk&2fX{emi=!;rBq5~-sNc-;9Zxn50`|&mJyxs@2=^%eT=0l~062g)6hq^zwm^AAx zgb8DwEwvLeYX$uq%Nb49l}*=?SPqA|30QQ5vnwS)8|Qo1ekPA`WH{)5ZoflNV{zgO z=S3>Eg1LZzYBv*j`FCtcdVlUWxJI)eY$WJ(+GY%w43;Q1=h9hZ`Ns*+)kQ43V?~@7 znmo`DwYBa@|B024_T?IY%3Du&Sw0t^b*QQ~Zx3Y;iwXILADpYG z3_BVSp5~haB(N<&Ru~wjClO*L4QaaEYb=f;=d>1_iC3CJ(JOOs7e#boA_|H>YudG^z{1z`4cxL*3A(E_3MPAaZ2-K{FQ4_=rE4Cfd z%l70RNmit>fTaP=PJ8twOdNd?^GomQq#{WBS&^28-(J1VSVsfGDexILEM#ATqlp{y z#enHb&Ub#TenGOp6zyWGD}|d>MDuu!N%UC@MtanMA-c?9|Lr<|9}klAlh2oL_;Uuz z`w4-^1s(uLT(=im2|0_0fOm13WYJU|G{Y-rt+&_5WVZ)oTT_+^+5q#|m(nfsWzM0@ zxn22DZ#sVa{n}P}ykA1Rb!RW#e0+7YAYt!Ij$XBNm&nMy$o%Uzv74(6Io+U}qnx8n z9Ju{xp@tnWqI=g>Ha^75oJQ#ECInrrnpjWiv{all{pbr+ZP<~qaV3f0i3@1n{oX%1 z{`px|+TPFm#3zS(d|_^ZxhBOcM{Yj9rER9ltaqMYoSp1xdbh4NVCGbfCZAX~U#S=! z*N~(v2)=n$3|9)n?nH=&MmGTX^*5v9-aQc(XTRCm!*3pKxC*7?={DKvOhH!|SarvN z*3;nb#QCFPxty`sTJUQ42G%f1clt7{r5Mtw9;p_2HBy4{leIySkv*J*bHyynqM?&J z8hWsGl*=Cz8tkSF3z3)uy+!|{!TOLm=&*IJ;PRD-@>;UThqwR)scrZHjf;uMD4KR; zDwz*Yu2NrQ*MCT6U3jzu<4=fi*d$V>U5)zFkV0!Umi4qS*39qk50iz$tfpiRm`G;9 z0{g?m_We4}!t~p*gWbB-`1%2LOUa?HX>y7Z>dci^g&DW*x}OAJ%;Ibx-d+vgu7N4b zo6i>figg3FpZO5g8;T=}0#B=Q*7M$mQ?=#=j`^(9a!vXn%|F|ZJ7=q8yUP+f5Oyvx z(EF?j@8X`sG>w0qo#Jr7^Ngb&CLG2aCOwfvCfYwO%`qXwr~`wmt)Dn^KQEH0&#rq< zuO`E}#YcAk?iH;$2c^4xSS)wFwY&UgQ-;M6o$JQ4f&MxmJg9!(OThO1HM#{OX{B-D zZX(E$?U8Ky(qPHdMYm4e!b}Mv9=fQQZ3)I`W=aG@Vu{J3UVa;s>SYHSfH-&n=VNh&#&0Sx$FzOPSO4k_nw?p+Q)13XTfp@adBmQy)#*MSW zkR$O|{GZLnIaMKbVp~N6b+XGeDhfse%wkzb&j{8Ms`X97>~7l4ZF=wPF;Rge1T>Qk z*aVnpx?U3fGB!uh!V4;wp=kMEdiXTDOiUo&%0id}#vJp@B6k6(4I4|RcbGx0$?S4w zvH`>;_@VV;(D_g)9-?OMT81);?cP*k{-;L@;>?+k(gR^Fqywx{hAcrXMndrFAJzKk`vLsdi^YJ3`mkZaGb?e zwg~zDag?tAinbzdNz9!L%w)ZG6KNB%$Qqj^3f5JMo!05L-q3 zDkzmZs~@&XczZFekn4Zm$viYHqJ%*`kUD_bOttsgeBIqJXgPfEATCK(vjG%Z(^RbW z`gPM~^*UhEKa`2zNhI90NBpA;Y2z52bKVv4Bwe1&@{Si`$)Yy%cO~^XKTK^kc@`{W z_imMI_HH7&xXR3kZ_^DtOm-_Mb;7LIJ`iEDC^>t<{{9}_5;rv7uULYim7oA7H_W?r>ERyl^+t+ycD={ z?O{x%_Z_8G!f2J3Jnnu^5LwN-04{Dy*6m|xk9^8{OT$IydKf9VlJ$LAf{wC}uCvSA z%J!T~PMM1Y6vun9D(PWG_JQ=n%2cT1_G^ z?{JI@Lq602H~gn}k%NStZ7Tg>3gB z$?4?&sxh7^Ml?Q?Xt}o3qnNaxj4Q-YwU#<6rEL$s9ZzE)YeYE;I}SRxBJ=js^&0~5 zbyXuvO;&9aYD`c!t<9T^-Xk@m$4(`#POWv8B$dyM`kHw|s(fX$& zI^on=QpY`VH-f433ziPl3(pUYk5Q~`1(qRg6;=sc6<^C69?xp+X^d<3eHi{`XF@F) zYZwb=+hsp_;0l^D-ov+Bnem`F&z9jX@;51${MaIl1@uaU z)%r_q2QJ;tpXCTVCIOHj_Qf$80k%z9F@`-Kh+k9aae?Xx_7cf5Nd+n?wQ0>r*svkM zwB7&O2xSxf{=vd&=!ZgT!?I;^n0){e;WtO~HdYC5U^OJKHpaV(3bBWy)%^dxEai#N;EH0}-bsEXQ1aFjj=yf$97lZdZ`y$_t4bCs=Ur-U;lD;&JyuwKV8^T$L+)bQ%Hxqt}N#9 zvx$^z;B7QV8hec!7ID&6bMR^?>^d*Agg^G(3(Cp3!NLNqfc6X!Lb~z=k>$+28NOYK z1+PpH%3g{jcFlZp^Ve!#;eh=#TVHI0wkgI#KYI@MFt#8Rj0BdZwe_SfptG2LT1*Gi zs6~7BD#a3AVY*n9h)@)nuJi`yTGc(wij5lv{bZp?0creA#G&^KYc&p?D|D=_^t`L%ugKeQLgm%_ zDMsOJdQNK*-IoQ-*S3?!*@bZn;#PeD1sDb8v5=DWsQkP)>OW|zRKf;-1OmnOkI<^A zkV&+9VXR=yTo4{f9v0Cp_#uMnU{~)C!r2ym&WmiA0w>9B57@|EP8Y!tqkjM3#3lBK zVHSAkD{RjYUri+wlm3*v?r)49S~S+^Mkgzz=L{rYL}{yxj2-1FGt92H4cw=cU8kt?BB<%W zmRfW%BP>I7RndZ-96S7JIChfEie&$a7*nt~Yqfk8xssa^V=O z?y9@>X2fY3+X|7hdf^yuiIAH*R}b(k3aMthK3y13!tq$ zzUPkK7>#4TtunU&JU&x{SVLijivav#Qq_`O*H3D3E~HTTnYUfq@bmlu8mBMokM0c% zSv@_~chT8n+{pE>-w=$#Zr7zn>j;XZfONZKNS}niVKL3G(#OM%Rzt2925rUgE}0_F zV!)}!S!oMx?z;25{DldT!VZKd?{+A6sQAOFbXu*mW?H^sf>7{<3R4-T}1-zmOUU|>7` ztn@-YAZqFtYu9O*j&Omg)xJopl1q1CuWz(Bxq+RYH5(gmEmhl4`*-=aR&Ia1!XEOEP+cl_}cK?5n98e;sFVaRY2ED$(f- zzi#&ZV@K@aKos*HCp@3hf(s3rd|+oG9SpUFOWJ!1luQLJ3X1b1mmjU{+8V;4+k5bQ7VDJB%W_EE;R_S)iiHNBt{*Bp zuVbXfZENYm?dBQ{Yx(E`a+BijQT$k%YbvQ&w?C-24?K;3?5MzF)_L8T98ct^QtH&~ z$YtrLo4cy-Tq6}8{LuL+e@6N*0`9!h&#Q^!9N^xk54L}hcl{9BneN|fA^sK>*T>rTZpW1wa#5*B zbVjkv4CS?wKz=Xx9=4xX`}_ggOvy=^9vQXnjUZu%@qjo5GP@G7D_?<51hv|fp6I?K z2+wnEvi{M;;XgURGt?5y)%}Z=Dazs@Bl=b#{I|m@xW6`04qinwv>`iA7tOy%qhe{S zSu)z_sM>ZaK1Ur6it{c*#$I*nJ)avx5W;?n@XUANv;MOTP}`DzF@uBVUawOEDlzk5h=LT2P0c&AQQ+-^SwltioeN_7!nNN%BqH3F+*+D5 z!M*Ic-SIs5hMPj(5$>U{1B-kyH4NVnE`Qp;+GBIlkwMpvqRqoY=UVdDzvNwhF(*=x z-5l;!nO=D)U*>|pO>gzW@kKWo(nirQjQUE;1qH=NE#8Wh`FB zL{zqiIte>W64!%9$92@wiV}?pk(?UW6-~W6LTRQ$_&+GY=j7}0> zE+CLe+^KJGC=K@s_Cqf7-4_A*`pf+|kZnaI~r!o9_l*(m4ZK=EP0f&u1Wvl?TEVrLsiS9V{hf~(m1mCtz>|~>ljG>>h zF-_*wi0-uCCh@MSSYy4G_HbHuIz;?xEhta5RA_^HLR*=%fEL0uN)BiLw9rccPk;Co zQIjqc?_#s^tn3n4VXEH5(h7h7wMGl?pv26-n9|*YW-0cOWpu~b)NE>VCZv#eZp~|Z zJnp>CEznJ)Qo^9aCcdYRL$?VgKkc#X^)V7UK+e8rZ+v@#ve~#;m8F#aTOHxACaKSF ziSgUkoEid`qi3liM=gJH_vO^29Gz}Fh#vJr7GkZ3pe=hk2b4@m=bVu=_cuw_%QjYS z5tK$>2s;(Rdc%;v+R|1eq0KxN;R>I;nThj{URhsS6k@&{izBQcNE(4yu^ofM#4(A} zHx+LuYzh=u#NwI=M_635(;jIRsId)bEvz^x(zsbij;7V(A~d4fKm`wS4wIi_1jOuAhHn1kHE9%&AlYc_#b*6jC7)ZfZq)KI8lx19L|l z5xuv_Cvx3Nc{*>+tD!d#F~gi3^9guCcW$Flq4lHS^V6xXU*9K=WRkRW)$wyJs1 za5l~_aF$`Wmt_EY@A(vE~faarM0JKTpf_PI38GdkVFwuWdTkTNR)Wzx_Z*{E)( zfvJDy56X*pOQJ8uFxu#1=(;nW)Jdqjpk+#$(Uvr>74fFYFBaAYLsldNONthyq^z}z zubZ`z)xg-8VnOZufRGj{wLbXmmgkRhWLe3+7*z-jyYn@}b%VI@e2ta=q{@Kh4)xe~orwo;iMlba{eDvfB?Y zHUbY{WiprM#nxtSK$5gC(fUE-o?SM6LTh+jaU}4BC$U%^OG`!jpT(0S^JrGvatViliJuwVQXx7uz&;6%Rp+34*JCdIY4O*3d3~#I%=yz2@^ob@VCe? z;~*Lvm|~^4D{t5nGO9|A1ge5CZzu;li|^L*>L=#g(oSTga6>GbyN>1rJkau_1-0b;w6T5enas#SbiE zC32Mpzj6wE4CzUvzEG8L(WzO9O_Nt9-`RdA+f;@as3XmP*cVmpvQOh{t*wE!p4K#p zxGp69=EQ-METqUFoKA76PFES{!L(J%^0CreCZhVCPB?qP;~&a$I4O*Ibi~_^jrp4t?;z?@2Uge z|8QrC+36zJz_0~EJQCI@Q;|5mf~6zdLIt@IjTh70`-R%Kieps`fW#hNo z<#K6CJ-UkjzIwwd`_tYBRhNFH5dfelEYHyEZp?x}3W{ra22!i~x>K{q-OPTY%y-y| z_aGRU$h8aCS$n8?w- ze5mWeQ)z<;?|)^BbF0R%`uokSYB-x1AN-|HwgmOM;X-bndeE$HF3;3ls9y|GoM>RZ zT#9h0Y*Xqu^PX9$>95`EwYl(f7qWY_T(fMbyr9wGww`R4z`n?c)%t{RuLB>~X^Cyz zvqTYd`|UEO2T*w2s8Pj0^kiG(&k|ZzS_N%RLg@iP50Hj^*@=xL=YnfC@%F==$!>j3 zlb9>EaY$a*zh4eFg>*>IQ#7adsjR6wE*1gzO8FP#gdKplcmY;z-+7R`Sg7iw$GkG4 zqfcfqZzxmszdZ0QZ;JL4CZn|771*AO^fLA3TJg1Gw=?{x&E48rpsB<>tlC(Tbg3!;?S-t88JnPS$o9Di;kTrpgnvHP2+`pt9798dqzLn>_=nN=` zcWdRU@N(jY$E_OH=T29v5)wgmvYaN@-xG8U{ou~Z{x=wzNtj~6ajKtk1 zmkC7v4VHKx5-VMpD*oK&l!QvxY?pQ+WAO*v)y+;zdM5PowwNM&4d3oPqR%Z3QPWT0 z+H(u~5!Vi>8)=slUwF+f>BFTevOC;Yfx2(mdruEpjrt5nR8Na8+B$6hd=Q3y2LV|M zuxpt_qd+X z>+>PXh0PX!2d2tCR}lOMsQ(v?#(gYrRFbM1nI&2$=_?D+Y}i*Ql~4R-!>6qOjdp)` zq!JIyfOoP9Ni5M?(JR-qu1>H2Pw1^4G}Zqj0{b;#N=92?#T><{+NTi_?_dd*mK>ohTgY9Y5O3GFljNvEL}ZYv^!MX;~e>+i<_TyI|d80aXwugy#K z764@Z$q%7zxP$H!PAO~uZ>Ha9lV(srmY`rq=7uWi^v%>aiwnx27!*oGbCyUCZ=>%$ z_t9;;SX8|h0E@| zJ$p2Iza!6|18$wb71}>OULmGalCt!?6Lt{ z1th!m=A>e5AYJ%qRIqt{M2%TH=VHKS^v)uN#2Ik+)=S6tWN27^K z?XSQWflQHw+&gzp-k8PJuDQ2g%N~=zBa$=?M!ofF^@Al`#b;TA2SC?nU4FCA?$H17 zSk8^ef5K`h&vZ0$&e5E1^$MU7>yy<92^XSiDe`tjqo~;a*(x}Is+MgGx-i@p2^KJo1 zok_n2Y$IED%*FR|7Qk;MPbYw6vIE~UcnBl{FlwvycUk;**Ggote=4da+jVTRV3RLKzsr^}TfGX9Iz2&~nr0|Efz+t6eT`(VU3mJAOYf$k~h z57^SYFbt&bK}%Y0(6`w1(yr8fuls`tXYDzZv0IyxFf(B@jYHh!x@_Bk2=e=3CI9U^ z-^H-|?e$r4aklScKZl^BjKwvbZh*OE;6GkWZiP2!klR&w|IIFHoVFitAYSZFsxWgXC=MK9X~PI+tR-ZjHBYpGE8k-HH6IQg_C z+JL(NoT>G2cGg|^r5d;sR?88Se*s$lb5WS0xsz1d!zbXUc(zoaoagrVm84QP+Ru%F z365OChztSoOFze`TV*Y1t_FQjGC`pGow4A^2`Ee#b*NT$ew6J{XAIOYoVVvLPD%Xf z&Yd!tF3ee+wc|yd9W24Ao9XxOcx&q@=hk0GSO9bz#(c%{I`Crs!WaPsTtBxWcB6%S zPlw-#AOQc)VKa@mlZin+#@l&J+eh4;gyXp@LmnkyR2a6P%&gM7U|J3#wa{SXsK{g+ zZ5LH1wBQJ5eNM~I1Egj^N-u+1iPEZZ(1M3=(J7CfeLt{`z#_r807jTk1@TgkW0H)k z`XB~~Cy?N)q6wy1|L(Ka%lUn`*he_*L&K6}vgb?3H)pMKS%4E`tB<=v>n?KlZ)UP( z;-P#+{hyD!(7R;&*d2squ>9#mq6Ay6b>jg9%|<$@R;OAV`ogB8}?IC(=tffPljcVtJKE7Qhv$ z`n>*a2vE7bCGZySB`Y1+da5D1{@u?9VTa@@)a?Q}JLd-3jt5Ueu;3^r!RMJ*IYMhl@mPXlkWzpfgr5CKuY#u&NvM@7d|;@_HxRq-{ML-XIXhOfxe{J$@={LVwUC zNHMU13c%x8czToYjXf&`JO5sL42?})6Q_$z6DpVx9D?)~=wJs&HbpO~toC4%1 zCmo8ASHVKs?m0GGw4~b~Q^JOBFLG|vrFAnCG>pS(^mGqin)z;hs%HXh5jkIWtbj*@ zmWU#E%XvW$sYoAE(w;GKpNy-s4b-`^{KoL~TPJCM$7h<^NONTv>z4y~_csIt?+^3M zK_qO10MdJKe&^nS9noX|OvFfTWF|*qI-gAuDKjh>axh}z1@9~86c)$<7uU>fF;{wV6HASv=Xo($wmcZ{(sX)kEl%F|#%F>8SUhhB> zlZoPr@9KX+m3x)5nii|{4;AtT>0jDa4@`SD7BSu+XG1jGgylw-x6a+PI0oG9+3~ga zVEwM!R29;Gwn~(YspRN?XbqWok1L2g*%^5RSw$y{5wl(dR%EQb)iR({QGqXWd9J0;`RVrVp0ApNYd!v{P-3)lrnkB5V9-1*4RLZKP|f;XO304c0$ zHlUo;xM_S^2nNGT=qtnPl(kms*}NVMbLHdca`m9gm8@ur)LdNokc&}0r78vc(Mt;U z)$Gr8E#85cB$`lb#2C|IVl}$@K-~z2GYsJJ>4y_12q+C9BDMs;89(uuke@N;On?tf zyUX-t(%#Fqch5kbl_)#a;0I?KgyGeV48Z@Ls3r%RBgFkNeJ*(NTl(_J zOMaWMo(C0=7c^kmpa5Zt1QcsIgp5AFI-Bc_q3A^7n?v2^V^8rsuBEh>Lm&8x5)K`o z2NqWH&m!EMkl8+!2e8oW_t>bQhbbWlbv)XIQ7;YerxX*$%ejsFsIxZ#FSo%^D#QR} ze)nS+-lM*iRpwj7X});X>Ia}+yZ~*LNnkvd@gJHd8vJHCr|Ga7@n|wY$V5FvXGEOE zi9l0!LQ3Q_S*1{6XmTXiqfYO^flz1BfvZ0esqdB@Myah#x#I@4QXYru6=tTfQ(U(6 z;GBc%tJohTSEZ?=1tb~ct?_JwiltHbt3I5}m8c+EMWMF=kpd61_JN1gZGe->TuP`3 zJdbj*{wXXOoAx4d^=?4XeCK_T{1WU^>F`Uw{eP5m=I?CfdmN|LiUyU~5=A*_rPE?c zw7nvh65H)y3_~mzji9aGqMahTOsq|j*m`TJrL@!P9ZL;GB&8x96w%VwZHtnI(2g~T z-F%~Sf4YCd{rNoSJm>j5-}8OW`~7{tUy$mGv6kjgOz7)SImRt8Gxc-{NYiU3CDS@RE#v1`!FGPD8GO#;$6UMC zc(`@#;b4Oxb?lzYqHE{vo7n8#W%;AmI2H6@yu#SpjalLQv7*OL@SIH2ByHD(!t-Z- zo0D!o<9mm6)V$mW#&|>505-}> z)hUqXQd5TAo^`dG2oq0V0S=KTQl2hhUK;fYN!Zg?yCDl5t{*>e0f|WVHm*+~5;1CX zNK>=!?kIyr9=;S-A>EzKy=vJ$t0Ckr7s55V8NYK8hS8fryr8KNg}BaN^QK`Zo+Wq+R!L?(Z9zy~650K>Z3bV>Jk8N;W4;=66V5l|n5) zxvJC)Wo!O8$^mN=Zs$ttEC2L_oc(vVU2X_6oUKal;T~O=9>Uv0WqVF?k+wC49+s#J zVH4_ZsGNL~ldgW-OG{gY)2TMxH8lKIgH|7n9=hfxhb}(O-A8YrI9PrOg9cS>>8*k5 z0!<$^)m#|NDM@n&!%QPaJgHn2Ps5ZHiqdiX4pl~@RY?tvAVIsQ+%>$7C99oGfl=tX zr5(Qcg&E>{XlUpki8Hb79~cjjrCm?SI}nnLE7a%e-ohEO`<6_63rG)*uQ6}jenL{v z)T_#4%19DAy^{{|kS^dKc8Uwa*)XKwlM1TF>Ofib!{P5Z>6Kc|B7c}X**bWXN+9)g z`#W$<30>ct*i9J*^cqH9PjhO;tny=;aqYRL?B+K|qxM84FYEKO%|?_$j<_c2jdekj zvFW`QGaPLLS~)pmi5^cZF9JxLcm3`2jy>EJ>-EvSB2Mi+wE>X?c8t%EM}DgZufrGw z4;--3#!(m@!LXv2^4B##P$1>Cg6Ytzc$tv+I00cU0q3*F?QfAvcuXwuk>;3kKf+Nv z%g%X%G$I?y=A%Ai8T8(U=Ecf)9#)x0tboY5h0d--&PBoCathGylkd-DyV5oVm zC=J1Cn8eDa*`~Lwr0WOr-A^q*&`qpxHFiAJ;y8$24(eMV^>-kAVO-R|nHMb%0O@FXU|7d%Zq8x?&Z*y!^g6J*eIG7TrT%3 zY{9}sJI|(}{HL$IsU4OVP&EJq#8EWyaomo-Gu4}3>YHMBM<=@dgE`0&6AYO*l;}zo z4d?kb3Ema|@=K}LBD&+!1d`jE$dv6z#_g-V>HUP=cpBh)qJw~KWiWAcGEw1RPZi_l zEXBY?)dJ-Z(vef@COE!L1PW8^M13!Js5;dAOGWL)b)6_Wr^!z*KuQDp{OR?741)Ty zrTV*Ww`5!uDD3}50s*t?cV%P9ba`Zj5!N{Lwiqb+rC{JGSkynB)@OG;)_DN@F_K5F z{Q~uYI=~P0=>UJ~-DefqrmHLSgS5$A>gcPpgAUm2u%^Zzfu43U0{*~nm*7eoO1Y3H zWwurPV(DR;hS3Y)3D!wDMNPpr1G2xAl3T*HjF&PUXjOpV!9wi3F;9G-Lto*ES<(WE zVur3IJ|fw7Q5P&M(`D3b@&w{8kNJvT(||8l(=rV>PJyNrik^d7O6UyM_9tMuu*4l~ z=}&Imb62O}c4iM*6_M(=OTS4Py@#gY7&Xpp+;*`XTi4@N7;B4;!)mBir_S947e{ft z2#gi=f=(rXxD>ziy3*QT&OqY~TBL)dOc5KBrBnRBl@IIU zm>tHE=|5xwYDYg7K?|+_g;$;9i=wVaEAF?ty0X3^j&PRxbkKOXC9IORQuABFpAi9#~x_RXn~iazDov&-S=vEZnM5>@NVYAL=xS zC$GCGJ8i#_k&L`(VX}W~zc0?}i?6&|%%Q-@XSiO1_+W0R6hW3X{99;G!LAJJ&E@u} z$TE{x1#?4>eXa8V#H^9u5QPivj-3I(Bc_gJ+(H#__&!40Z}|E&aMyYPPinn%MJ)Ol zH=ZB1nMHK~+W)ga+G`0-(zcLchshWU%tluhUhom}3ENu1Xus jCe_u5b>EPXxV1M$Uhfs4NY0G&IA5AR!sb5V)B;r5FW3v6CRz)>vsKgSt- zXU$7RrIRU52=AXw^N1VITzYpq+O>O~^4(MqAXhw@(_t6t@p+TJ8YUN@Ki~RQ+kA2} z|D>*Go+@P6kj!)O(Z3JtpK~?PVV@re+>bpM`#Iz2-eDd8nPd5C{vJWJ2vV?x-#I85 z23u$aFQku@o%9_Z_%OlKoeiBm+czqISPx&o*Bb*d6 ztTiJjJ;DDBcjM5nl^t~ zskMp6WFOyUm6Dr;vx7fSJv082T{?49QV8?o=UdmY{Z5mk} zwiYbCe%MAqR9md9tfa)SSDeV2fuxSW1l?jyZcg9ytLp3NYsC)|JKF<@=%~k!ADesw zKQQGn(}~tT+}}K`{rK^B^M6y6UxG%X10AENNT+mx;!B6wd_`g^emlP1*ExmXjcjbD z4l9mSG?f<&6p~`>S<$pB>6#mca?YI6&vz~2fDW1t3j-5f4}2n_hrq|M{U z$?l$>QHQ;M!&>S{j!|>|ggu7*N>Wcvaq-V`Q(4qha|K~MyZ-WvBLI@fv5)ko=ez>8 zAW){><;fO&iiujDncm zcGPQlJ)blH@D4P}W0s(zY(t-Hbe(h`OShP9{`9Has@B#$<*eMs0o$i*+42 zD2O@Oefd*LJK6(n)=v2&T#;aEXk?6(E|VfLwX|+)Sb+mWJd9NVfNRyIbne`|ycLiJ zjPr9;Zmg>M$)FMzm3^U5C?Q!`mK2g{2J)?MU&sY=MNr17UjB3qDjJ3}6;t6lBfmoR zbxcp{Z@62q{{cJ+HWa@Hy5F(Z&F@d0Mv}UOwJ~UPO1R3iXJuz{ zEb*O<1+Q6G6)b2+RzU%EUeP0b7FaSM9AJ4;Tt2q&?z3(mUtj9yIBv&z%YNCd$|l72 zjSimzKDAK-!FPLmyVbq_tila%Nul^eKKFwS43y4b+#fU8Zt9oc)Xmjx+G~~xm$Pp8 zT>bJ?c|+LNl6j!oN$b(yQVH@2wVys+rnfydGBV|{F+Vob?3oUIV$0lG#$jdFoYhI5 zMX(8$3;x#(e+q@`dm9cD<#W9~YZzY6j#xcUh@!$*Pli`a@6p++*J)dB}Q!U~c{)HcG8*mGRTNo-> zQO=YNm%b-Gc|)l4U~k1S)0_wxBZp2kxj&$#WJS>07dwK=x22mnJSn#Egi$*q}h^a_S6dvDv3d0aeCx6D*h#xY`EIBK~pyMrxCvG-E1 zmaS{+=I^+#L5M9eQjpjI{41%OeKXfnKy&LwSi8&;2wbCH62sY>-i?%={K%&UZ*D>O z@wq;ylDo(f(QEhXe>%pQ-M1LGpUz@9%^I`LNy3q2#J{Y?M{(xt`@-jh zUG(mo%z||AcMuJTh$zjWS?2>rm>*xeI+)_}TcH|2O*BfdDBvHw{BdrqeYAZ|wgmBA zMrpW6ha7*)VrOgjQcL;52mk4fYqNrYwa)ijT$Ws>U%o90n3x01H=AzU{I2RRyL98+ zZWkD|zK|EFU0YNvQL=>ng(`~hrc8LJZi@?7M(5Be?ikMJ4fAisRu?-n+$ z^TxTP_{<}}&A4A41-`Nf!lCsD7J9=1F`r;SGs}BXzvFm2Hb44TovBvvH|;H5TLf#c z0f>Jq098R%&NU$)>BxNDD#?3caN!Hcj){tZleTb9|I9)Igkc(j(ANwiA4cWwGr$ z_dS)d_EjKiu#L~u=dsld+m7({s@`4Wg+EM)@^GU*XSlCv`{-ECZ{_vONmjhk^c=cC zKU5b%9NS}6^vYOMtXtq9LAD2sJtIPXQh#q5EYv%9;gnt`u3w>RzhzDOV3#2~V2erv zF5)Hc*HdR81b;}cICL^md+So3R#dBT%g9Sp<@R3Y?$v0{9-jUdeFR}vhA8Pg3cO*V z_vF%FZpod`-Z+4CN9CDCg^J(Ekj0@(sN#=*!9<~QOZP2a3GV{FR#BljUd?TXtwpKv zVWGDzr~DtVK0SVVx#cA2DjR0`TP#3Qn6iJb;!yT|Qfn1oDI$mOi;pdM9H^5bkTvbZ z<1L<;l|<)7@YNkq!G`RGwRQEx^_G)7Phu{P|0YY{@YQjOqAJ&;t7ylkRlC|9(?PM~ zGenn9|7vpq&gGQ`#)Fs4=8c3p_SG&4>(*gt>h&%aeKwk1YG9!Tl|a%eOsYGkru$ud zcK6%d55fzUNkG9iMAD}>^Qv94npML~Uahm~;b3Q=p6W=i4@MiL1LCp*Z0F&G?+M$2 zv?gwQy>B1NK$IP{=PB{T|cI`gHK>03e>wxid27sTh9v@RCMGX9>LQ4*XE5 zc64)Cht6Zh<6fZ1^91xj_%+fA9)ev^*=EK0XdPUX>N;EuVuj0dmS^(tS<$e4zHA>4 zu<%v^l(c?SZdVw@V;;u;>yFQN{C@Fz$!;${$75qd-i>it-UntXfG!EwP1M-TDLyZ=c`V7abU^!$)OI3L=!sI@w%;>|qs_;wye5G!0M z`BAv-56kPjz&+74UHknPJABRvH$;v@9B|Pf)0W4G=aG2&!opZE#Rvi0V#Oia34!JK z-Q=S55Y6FPuN|P4L)bWo6pcWv`n<_Rw5!w6_hT70AiEgASomd*u?Equ4Jti3=*SkG zuQkL8>ncy%7JCI9GY_{AtnE&T!8`)pPOQ%IzKYffH{g=feneU4%kx>7s0`~m?>IY1 z&lAf z-X+kYzVZ5A*HA6u=kxR^&isGcSJPutSDq(!H+NdXtkx`r?R(1?og@L@6=U~M1OItd zP{7+rp?zn{mGGZZ+2P~bF}asgUxV)^RjidP*zm~4$UjNQE~fn{A$!yM3?yM=dH-s| zUxe@P3EdJFVt>)KH4#?hkpi7f%w=x)?Mvqgof&rmZaT@_{hr<=l3=Q=;d{8XJq9$5 z=+z2$pl|@wxBU9nm4Y83ch4NA8RiB&cCsz%j@Uo& zBC`p#K1KUqtmh0bS%yTUh2k&&7vp}-vlJYW1^BFG=vKym(7DuJ&Q7YoCsR{ti}>6G zr8_Fz@QK(K+oE4Cy#@nOLyJNWC1|-`u*%cZwne!WAVbR~b2E#et-LnjtBL5%;i;u( z=(1AXj}ukD$_>e3eg}XXClAbZaMugNy9r=_V6hqNv}r%$Ol5fl=yVwKnsPz)9WfnH zMoj;cZISU{O7}&?>%A`lpN3{1ZOtHlI?dX`v_Y3968Q&tD9laJJ!Zwn(TzEn?2v>= zymE|Zu0|0DT1!II{_BdT>N4#`sZ3>Vyl|6}UV3+OeDcxqZ{(L2RP!SGtYcvS;g#(Ri(L|&}Ut;dUx)7N)P_QLkn1Nkx!J{J`q4M}CH zX^M3Pu5ciIhwCr(@v?hFam%NSuWjg)uXNYK$vjnMWrJzsEeOT8sY;ZLLZe(kD#33R6z<4Mihj_LAxJ<>7*=f6M1^v6aO;C6mG~$UwN{ z&<+fT4!ywl<{-cDxj|R27@cDBM=nzs!Tw1VzQ}gn zUVI>)oCaDk6T}>;)Tv@|fLs2Jhbf5s+`?tQ6)k*7r998HLd?~JpnU-mykk_YNZ~lj zl6^1iV4tt5wK(Loj!TT@w@4Q&=<18I@-j4gaQ#Td-g5}rzxob>;M>Vqfc!V$#~(eL zeaeYqL8)K(!zipu1s)}y+z|#b--{1|*q46cb11YX)Jc+!*G^QIM4)9leC!0nDMM+7jrQl7~9fys=C?Xf& zmZOZUtn4)h!F>*pN_O?XBM=`yF(p$#Z~Zu2Jd_x-f=en*UL?VIJIZ&oXot%3;U4JN zf{gQxJ@T)!va{NJo(-L!1@?DgVJQE}osCxnGl=|;;bv2qU%~u*C)DXVhs=^aa^p>` z5`GK*CArKIh>0&>;M(pSx^vOcrO|PPl5E?xo!)kmXpN|^C;NWC&LM!VDdzr;`==^5 zqFcGFNG|9U^R6;3{btW74p>{`_E*D^3-G4KE1p;XK1|aL#j@}yOa7+JR zGDwe+hBqg0?pg(d6F8y|md5I|&faGBbdbwlSjfH4eLCB6!YJjht*u9A8JZr)tgf>j zuqCoevDbaXv04-31Yh%&O?WmoG(2A~)bx+_ib((&}20 zdS1FUkP7itZ~AJRPDh?%(!C6@Juc!|Z_hNfY-y(aeQyJqJ<%^~=d}xzl|#98y8q3x z(KkurqKlk#5Vs;sxe)Rw{$Skgs0!o=Lc_ME4%2TN>3Y`0u<_HFlI-x0rktr=71jF5 zvkTc%`!h#m2Y)nPW`=O+$}8?8pY(7D1j5Jf%o5G3CCfEx^#`MuI(e{IOZ`^(LQ?E; zs`czxrd8+f$Fg}b3GTvH)>o^4#r{M{PkXa+O(zAFqo@h!=dC5RYO}o_FS{Y;PpcfX z-P39d?%>MZ7j5oGmUB$-Ehp|zHWa*<=L?_FodKHoHp%JZ2QR#lD#^e{ekk0B?q&p>}b%pA^9t%R8;N7zXkh^8P z25YPwmz;LN6tU{Yv77k?)t#hDUvvx+WD2#jBTD39C;~U@Lp!VAl#9Jtjogc*rtXQd zNHj2Y869#sGeyLE=H;akOu$-1ylr4nVN@snVbOw2DFATddKI@yOVAzOrK8Upsh!p+1VL zh6T*(LGF9=ToMA@swpjaSw#i~r>*W6_{UJh<|MRXu*Lwe18fSSy(#Bg3I7`FHjXC; ziup38RUsXBX|p)|GU?rL(Os9rmXLS;TIRn0eU~@)QqqHZ!cgjAs0mw3u=v{U_i5Ey zN;}=gj?KmWb|jDQC8$0h7&H8^((oV*fAx^NX(P4kpuWfKi7;N8}gUH*e@6~iFdlZboSJJlaY$tENJ=@eV@FsJi%=WI~ z%IF}itrOEZ;;8m(Iqiy{4y)UezbuXIoTk##5!RRAq-11jeEW6U9!xvhxwq}aUD1=< z$6+;$*xH*nQ3Ati!$Un$yX;T*7+{_HW(lUZ?{>!t7Nz&GLG%+>yP|RAx1pW(v@1lc z)1A=)zUHr442dW#l(|I^BurCNm=iVu`ajZ?8Iv*WhdErIt*<%&{A4qYZATM4&}@2hBT$iyRcM)1#He?qtn0v=1|DyoAG>4AYbUy@npnL`{56fE z`m_3~Hoq&`!0vQ7q3th{Y;pb^cFIKIT!bXr{uO+q7Q*T&$5oG9%2sa1eYJWo*0kK? z4LKMH%>%s+Xf^^mT!aNiUmj;^P;|YXSxxSZcVu+)>H=J|Ev2uYr8ayG)Y2@67fBZ@ zaaI{yAKR=1-KQ&I!lJ}aiqjF@H9C#0F5A5RRrO6*0_#!j)%2DTGi7?*pg-feXRcpS z^@XKCDEz8hJI!T!>_T&G?B@hj&07#+A7m8IWwQ!#%*CPexym5Njmxv^5>a6G2LEan$g&bYPM zBb|7g9xayoa}RD_w)HtF4v&2D^Q7HhH`)6GY}j%T7ZR7yG0-A}iM(G9+i23X)6&5u z2b0(WPBzPzFp-8?R4BdwL)7W$jnQcC*v7{Q!1OqzTlu+;dx`K*8RTetw$0D{{+o)Z zCl8h!cKn(bN;Fo#6%S5Wpa=rdAkFy3VC#N(ye~c3ZsNXR1wKg$ z2FHhroDH5yP}}~i{P=0@qMLJnsL08#q|tdUXq?z90UuIIzTY1p(mch@J>SNAIV=T* zCL2V}+>z(0Jg=!LyH$w!wbC~BPz)$oOX&53-pqKKdn-|0U@vFr`A~&?OJwxr-bv`j zerWH}iR=vZHTxI(;SGfaCg9nXB^vV}bG9|--R;iIIzb0<2Bw7N-_m#GJS1ZIgdI?Y z*mqe?K8Rt5;V;t2Go4Jio;F#SB{;F#?SEN)h8Ig7r=h}{v#v$cciiAzuk~9A69poR zQytRFq?MlBB~9E}a<>>R8;`%OD4!(2eK zZ=k?qQ=VNj=(zjTH?XF0Xu?(@+CyZZp}Q)@|N92PZ`kP22MJmcB`(Psh zmzCPqaj*>*Uty2k=W)fA=iI=G;Cm?t#cqz|&OIoX2dl6|{*6#>u@}Gfx-K}o6Xy&9e z5B;FOf>@wRog>hq$5g)8^z+QSK(#}QEpe;+8#q?lsW6E?d)z_mS7IyOIvu|lTJFop z2fg)Cw4cnexwC&;LVIMcRUBEMOxtf^+D;bGgQ4)Vrbf+bWy_>ZRU!YFL;{(+*VT&f z%hy|8(OB*YvWKQ-cJAk}Ix{6~pe(jvFswu(T%z= zx5gpkWDqwDPU~#NLkQ5qfTrYbCCq&Ms^Y}$X;;}Fr|gVj1BG=yC`+ilQH9>>r(I8p z#6RB1DA*y53ZXx*$ihs@sqWu3a1Zu8af&hW+)>(p!UjAzXIpFStddiPY3<~*~p zP_!T*oGE1rppHD+8X4@#DYWqm0mHs~$REC;#%w!ktnI-jMxuX!TK614EdzZEP67OQ zIO(j@rrrt_k~!#*F4aPEr1lr93i5rVU!XSzV)$Rlm-9cyt;(W=2&u9|9d9auUGn;T zW;&5oJ*<+LZ=gs^DI-%hY|{=|owi?8Axd?GK0}0vTrfw<5gnZ63o@S;wJ8ldPLDs; zfg{KJHi1-KrlNz{c#Q8#aRBQNwilbyy=ec={YR#9C#G?voAzmKT;OaGo@Dr=8n@#@nGaHIWgTv5#3s_pm z0hT59aqf^Tsn9GOa=D`5UHy!8s7x#cw%I*&idAu44BK^Hrh%e0LLG9=ic)|(Kh1(I z^hIMLy&E_N(>9@y1JNAnLs?SE6kiruZub2HH??v7V`L>&ZYv_-lP zh8(Pu4VDxp<{2xEwN%J2(231vhlEcT2l?hCHv0-qSb+kC4W5iJ+c-kSygN6jz0h$J zag!x{Mn$FFdBig&*!|1-OR~pO>;CNE7VTEWR>W$i9BwYQQ{Jc9Ew=wGNSR*vP+L!F zIEtEy**8n7P2w-s293~&qkkNbA{a#7QB)hy^}2lnk&f`ud3MSC{@=qc>^HtY^818p z3^hhGxLYC^qD5_j@LwjH+fy?*`=YfCA2hZ&x)l%Jyf32J zg;RU^DG+N1FaG~;-C zpKZF<@{th%C%;zel>vjw42%YmXU-PC1?}H$uWP?ME>m&R^3Eyr+Ln{U&dicL#J#Sa zrdXG}kuh$2Z7^XN9@WF%C)>A0egQo??BV%r32xeLsyUL1Pf0VWDC4_qGb>$ zvD8}Bwz$-(I{tdbZI{+^uxqU62ttySv$bbs%H^Y`eelt{&xcJKgTZp358+-$)$Hfh zl|F#Jxq4Jl2!OgwZ+`ACcU+<9U2cG6rSW}(~=Z{qicoPH+uxnNGI`b23RY`dUBL?a} zc0kSXz%Y?k)d4hQHUz>+hzWm-5b$bYhj;fUT3pG}anA@tNuU@DvtM1hCI~j3C9_)P z{(pNbU953;gD$0)w#yK<8V_f+7GxrI!bHTCy^V(j2t-s?+d>UFQAjyqFjz$YWF?yt zDAwUYY__qLXgr+PwTa$rlD{>)y0q3^kvpvYN5&|oy>7`eB&Zy-fS3w->^RxEgHwl^ zr%kiv?^aa>R(ep9KHZl&Jg`r}Z`Jk<6D8%>bEjY;1A`4Y^0%1VWs`U~{o>lF^J)zL`1LtDZ@pnsyc==(7+w{XUhyB_d@|WN2IpaWBn)RBkijiuT8s`oDNAh zbaQ%3W#1XuDz57WeI379&&DZP2G{kLGdFYfa;z2*OgZ^2oq25`*nU$7Q1bHj{ohq6 zjgIZ?#WFNAnr6cUvLV}FjOo{F?SL-JnYCPx_&JbGvK`;09qQh;*s`W|D_8P!)Ps$! z@6SYNBu1m6U?2y zq^-Y(Fb$06i z{-8L7kY%HuV+g7G^OBk)u7MMfs7q^SL@v&Kt*C#r*bNE@v47_m1?}>m!Od!^6b^lr zfi}ut2<;r(kBHOI(W{FWOxiKz!?^0CSPFG)te$d4&&aY75pQEjD6avdZ)_n?wA9wBp4Ov&ZxkPOt(|Q zkUtkl-<+7DmS;F?;mf*HDTTNbbq`jJx)>Z*D#0zP>xmN2-7uK>p;e4Z^7>sB?AZ!* z^1^mqgp;CyUlpzO!F9OK$0sH0s^QPm?biz5E@=NMU_Xxy;QOd%O8vfKO+tM49ph57H&P2)w$w6lO{)~T{(*H zIPAw0YI?9)MG#{~NWI)zd_X4aUdy%@O4WEwu0KToLVMjk4bE<$V{7N!6)JZ*%Hach zg_q)A&fo64;2v4BnvL;ceDcnCmNU!s`R^Oen3EQ|S%n$l?vDolinhGZaByjtN_puZ zFEkX?<>aiUw|Sm1NB<@R)f>E_`l~nR8mgLrj{~GaU7#D0MPgt*}LFTWyp1pg744ULW$l?TLdSO3)0HWVVjQeKR&}SkSF7!!s#}WvwpVd=?(T3R;%DyT zMM>HXWV*u#-|T^*mLquk!wn4w22`M+L5DSRn}zbUTMc5jgIB-Oj`fY65eU4^jPs8% z!e+X;O^wdXIuZCPQfBCji$Yg5Mf@fUyiyw^pS558&$MA}gpdvR3g04HaWq^!ue?P) z2J({0WgG$t2pU|Y#Lp>rVQwL4g}GM`b2rbdq|}seD@1?%e9+IELY>$QUfn+*;6yB! zBu#iYb<*2l>z-i@S@3y($~m%t3g9hx)biegx<4PxZxPYzX;Lj6wA|2AiebM(eT$83 z0E(zEVFMqJqa}Rsji$cP z6yH!*xGDnuy+a=!cTN?@w)TTYwlFRt<(qfDLga1Kghn-sxIwd;_CNYt z`tPotOW@VYrj#JKfV~dxVK|aWebvVJg966hmr59YN}s>|Ix{d(l3nXKSB4f;zM|NB zD(w1%mM+vda^nX)FWbolHDv2&0O|5DK$}RmRjftW^+aR*7hIy)4^!E$<5@3`Uh@7p z{rYwO>1?A`4l2aWG*MEd5&Aw7CiSwtcQ51(WtLm){Z95{Nt&uea=&6ot~9PbO7@=| znFZN{F|q1_k6qY7NCCuqQt={zswAbbg}bx{u4qLe3rMPy9cro6!5eDU|)1Df>S-98gKQpqNi(z#f-wL}u?pm@KWMt~_ zKHIW13Is~HP)ph9IqxXh7H(WO70MimGb>$w@i>^5*0!Ba_Lb;#}|Z0D`L z9rS6Mzs8t<%)WvF^p+T7QQr`HQEdP=X`*RV_i3P${#!qKc2X(6Zx1z1 zHD@}GX*(&oI$sXQzU*C@w9M%8@ot6n7JO9^u__9$9$dfxy1v3Hfs&Lkk-yi*Zj=gG zCC<1)FalB2iG3Tdd;?6GowPP3eBtDDKvBpzd!h~=1u@Xz5{kX23K3knFaP>$@v_tDoy#tcYW-hpZ}neheUF)^D&jhhhx+M7ZnY#?*&?Vebi^ zBrOFvIERwmbvoYvJ@MrA!E=#3znBLDod;geVY^wE=>ZR?Xu6-c_yXI5Hcx`xm&9Fj zOvlq@&w5Vp8+$|69uCkV%6iVCQ+~LJpp};Nab1-%d-b$<75(^xi!t#vqBgkBBVm%UXCg zGp7qR80b*5!ddi^9$eG9(AvWTw7>Jnk7kc`q1Yq0qM{s``nSktjlU+|4>f(IrUt~g zs9Z-)a!#qHM~19eJ)J0OQVFk|`LcZMwL8aQO<1Sz&&$CZc7QQtsq8d8VfZ>Ru`5O= zLbbAI-kY)OyJ<>1?$eQGTzhh}ymYFJ12Qr7ILz0P_>$A~h!1n55Z|*>DkGiRkLt)2 zZ6&s?;235x(7%&C0@|f%iEq%(J1xPpsE+;qG@v_c2hzj)=G)tf1*sPaB~O<;6$)rUaG#nNe&^q?6_RD zwlx^dKX(W$3gpCVQ;c?*FJ#T2nJi37^3i{vD41+sizitT=Z%TDKSy%p%b~RrA7XG7 z=C$bNAucf0&S^Ygh1dfYEU$Nb<6cJl0GSO$oNF<>RAi{X;XJL| zW-dNYzKxd>*%9TaLzR(2bHBTWoRIrxPWm<6$G;FNJ3vOyce3&|>U&)R98LpoTa15O zghC_7za_{=A0$@ubtEnIKFEm*==_`3K9><0wGuRTfMahnGE#y&2$DYktohPIr@qoS zNm>_bbMm}V^`ak~%tcr{G_|f^c$RH6w0R)IN$u>ghA$*k%&hmyC6*PWzn*{(};F;&8zSb zvi6GYU8S{v+^E7j`#T5F^doQbIbmfbjqIo%l`1~9y>mliDo~3rFL1vbMx0YFs%Yc$ z2y7Yrt_#%29K?3>7blH6gogTRZ4>u?8Vz!2B)Qi1YtzPw7*isLV882oZ?NgSH{+>D zn|4Upfp>{1mP?d&c^|*Rhi$(M5|91@QY#FJGZ=Ks4e&z9f0JLUjNf=CcZJN!OhsQL zLd@$>PGorbPch-c?dxJDY{%O5dDDtyYXHQYMfHk)$tj#VutjU z-(UY}J}-LDa<(<31hWQrF(!GZ;7PF z3>+4>tjvjd;oBb7?9l$t>*W|WP+K>bLk6m~Pxg03!*$B1vWdL-n26RM z2F7_G#(a7wGPvPmp)Bl22T{uute}Asy9xN9`PN6Yd~D0F+B&isk5Zga%mPf&iTb$6 zUhSoeSE7hvH1-qGVTYbPeL&XlEX)i@ zCba)&j_2_-blw}9LEW|4?ZB|fp*i#VDWJ$B;uSWf#|H8a^WlWDa_Sxoi4Gom{b%e& zx%G`7r>2VR-_eaP94?9Zlk^(MWqo`oI#}M4=XS3zNn!xAlvzOl2P&-V<2(Q6u8P@b z>T$mdzWmhgwlc9!M~5H}WvSndx)v!RBoVUtptb4GmUdla=NRacLi3o!)ToDDT4)qe zu;qNh0J09zJ_OR^75M!sd{((XFZ`f0+qRZsoz4_}^bGHvK+Oz@g4*XU8voUjXaeG> z=wzPO*`24pNvUu*EVO^R(~<65W62&9&(fF8At_WZdfKLI*eyy;3hU9uYE=szOA?l! z(zPn`guDuJ&9pVHC|&A>_+UiG79FFC+OFbN+QVL8+jVe>RJ_)V+RydEdQE0-dN_1@ zdPSxOy(xGRSw%Pf}Ke*P7 z2T(%zulEKNoy>O`cPIyN#6RABLPRIc;e@t~^0((@!!F_e{x%k82Z(VG#>_rLTswu7 z6Mwg7bhgdMx$lNbNyyYv?u8P0*=3W5_Z^n@7w>bsC`qN2ml$sCds*F3CNR;T9rNF* zBj$|9*6ScF48E|bH%*G<8Pf649=-e3#)ujScJ!aSbe^O@f64sQ7(q7>xwox7@bhBm zk-H8YHIJSJ=$@3^m0=I3t=qnJQG|pMTYigxW!*r+oL)(PZ=^5!{W}ykZ|P&%&CHeD z_aTb@-QISH2DkL5X4oNjoBL}Diy;vb-Pf=87yVcc55Zk#WK8w1)-FU=FF)QlahWT= zgPPP<*J$h=51Nc0 zFKX`gHahX>?D<(sOl3GLvW!V!)q{Q_P-}QxxGy z7#|o;Gb$fhWysmH)MTB)i7%{luh^8S`1bVr9pviVxo9J-*R-n0uh1gaxA??Mszvkd z)1nW^No}o|GEOo~ekrx|-5EYRlb;vtg11HF(A!KFg8m z-ajCRqEOSV4S?I{D!lfzlvfrrLDMrShp`^eC!+9U>_4OZevduUw5+kcKI*pLmGpp` z$}8Z7%t^^1t#l74;GXWao?Cj`#~m6N&74DxoN>RyT4jlduX%OeO3)`~l;=H#pP)wD z!PBqjSIz~!Ff@ee9tM3$XjSSOkf@&Ww-cqThC`#ep-(odq`NLF4%g+}A!^^nzU49i z#J-RsYJR+uq^D66Fw76}BZo|chURUDwzgfI_Q8&rtgKL5I}hS~Om9ey>(0JPz$rSc z4Ip37SjgtIPnX|p>r3`@aQ@7??^)C9s15n-SgGL_s5EY8WQsy!E>4e#A!#J-6o$Y| zg{GF2TSm~2rtak|YP_e7epanqj)!l*2Vs|X2moAtycuhP22eNxPMfH?r@q>ptgAG7 zi1DVu&1v&MY$L?znFwy|zqG3-jjNm{IDBn&p}gogRXOhG)d{?(!E~NKIHZ6Y=)IM@ zu!6>Ln3nXhP^Swf&A}q-o^FeT{e2fENS%}KcLea;s4r)~@$B$=Cm_FoCa$B1 z`o%f|aQ^fxU5S&;_P@Hx|J8#2-;~B;jxt&-r3Z16V;CRq{U<}ljgCe>$ar*hL3fGp zf0Vb`YSp0q6A52uY31eR+5hLw-gHT)B95B+G=%((kDuR{qt=N6bHvBrIPJtfo516t zJEKMlYzxlP-F)=YHha}mN24i_+}U-D^Mdn{3+kaud9;{Xjyxu&UX>5dsZK?a;Vf5< zdP(PhUb(;#_U#XctxOQ-?s!%oP))y%p~Bby>~TbX`=0-=D%mVl8mtSYkAN>vMhuuOV5HcY#0)9W=N~yT_32_51ItB*P zXK6GV)!5iL55DtNl2rXnzi9vObppMUMeE#|w|HsyMI1VFn$#7;Ikwkq;>eo2mzS5H x{wLLbS*)hbQ4~g5SXg|LH-elvRoHpRe@Zq!de8dX1g9KesAux9PS-j9{{ei>Ir0Di literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Synonyms.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Synonyms.png new file mode 100644 index 0000000000000000000000000000000000000000..e47fdbc4f63c67a6f60490ff1585ae88eac70788 GIT binary patch literal 11203 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 literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Terms.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/Terms.png new file mode 100644 index 0000000000000000000000000000000000000000..6b86546eb6a578f32b37ea60198b643edd3a71f1 GIT binary patch literal 39925 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 wang9R* literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/data-products.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/data-products.png new file mode 100644 index 0000000000000000000000000000000000000000..e01210f4a54811e257905532119d618fad3d4a31 GIT binary patch literal 13508 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

Xmi=aXa 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

{ onSaveLayout={handlePageCustomizeSave} /> ); + case PageType.StoredProcedure: + return ( + + ); + case PageType.Topic: + return ( + + ); + case PageType.DashboardDataModel: + return ( + + ); + default: return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx index 080be151a1bc..19e815bad0d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { noop } from 'lodash'; +import { noop, toLower } from 'lodash'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import gridBgImg from '../../assets/img/grid-bg-img.png'; @@ -54,6 +54,13 @@ export const CustomizeTableDetailPage = ({ noop(); }; + if (!currentPageType) { + // eslint-disable-next-line no-console + console.error('currentPageType is not defined'); + + return null; + } + return ( { return [ @@ -127,20 +128,14 @@ export const getTabLabelFromId = (tab: EntityTabs) => { } }; -const getCustomizeTabObject = (tab: EntityTabs) => ({ - id: tab, - name: tab, - displayName: getTabLabelFromId(tab), - layout: tableClassBase.getDefaultLayout(tab), - editable: [EntityTabs.SCHEMA, EntityTabs.OVERVIEW, EntityTabs.TERMS].includes( - tab - ), -}); - export const getTableDefaultTabs = () => { - const tabs = tableClassBase - .getTableDetailPageTabsIds() - .map(getCustomizeTabObject); + const tabs = tableClassBase.getTableDetailPageTabsIds(); + + return tabs; +}; + +export const getTopicDefaultTabs = () => { + const tabs = topicClassBase.getTopicDetailPageTabsIds(); return tabs; }; @@ -153,6 +148,8 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { return getGlossaryDefaultTabs(); case PageType.Table: return getTableDefaultTabs(); + case PageType.Topic: + return getTopicDefaultTabs(); case PageType.Container: default: return [ @@ -175,6 +172,8 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); case PageType.Table: return tableClassBase.getDefaultLayout(tab); + case PageType.Topic: + return topicClassBase.getDefaultLayout(tab); default: return []; } @@ -214,6 +213,8 @@ export const getCustomizableWidgetByPage = ( case PageType.Table: return tableClassBase.getCommonWidgetList(); + case PageType.Topic: + return topicClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; @@ -224,6 +225,8 @@ export const getDummyDataByPage = (pageType: PageType) => { switch (pageType) { case PageType.Table: return tableClassBase.getDummyData(); + case PageType.Topic: + return topicClassBase.getDummyData(); case PageType.LandingPage: default: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index a61f7f5746e7..f8752e7fece6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -31,8 +31,10 @@ import { Table, TableType, } from '../generated/entity/data/table'; +import { Tab } from '../generated/system/ui/uiCustomization'; import { TestSummary } from '../generated/tests/testCase'; import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; import { getTableDetailPageBaseTabs } from './TableUtils'; @@ -67,7 +69,7 @@ class TableClassBase { return getTableDetailPageBaseTabs(tableDetailsPageProps); } - public getTableDetailPageTabsIds(): EntityTabs[] { + public getTableDetailPageTabsIds(): Tab[] { return [ EntityTabs.SCHEMA, EntityTabs.ACTIVITY_FEED, @@ -78,7 +80,17 @@ class TableClassBase { EntityTabs.LINEAGE, EntityTabs.VIEW_DEFINITION, EntityTabs.CUSTOM_PROPERTIES, - ]; + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [ + EntityTabs.SCHEMA, + EntityTabs.OVERVIEW, + EntityTabs.TERMS, + ].includes(tab), + })); } public getDefaultLayout(tab: EntityTabs) { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts new file mode 100644 index 000000000000..4c05eb4a88a3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -0,0 +1,315 @@ +/* + * Copyright 2024 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 { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { Topic } from '../generated/entity/data/topic'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import i18n from './i18next/LocalUtil'; +import { getTopicDetailsPageTabs } from './TopicDetailsUtils'; + +export interface TopicDetailPageTabProps { + schemaCount: number; + schemaTab: JSX.Element; + activityFeedTab: JSX.Element; + sampleDataTab: JSX.Element; + queryViewerTab: JSX.Element; + lineageTab: JSX.Element; + customPropertiesTab: JSX.Element; + viewSampleDataPermission: boolean; + activeTab: EntityTabs; + feedCount: { + totalCount: number; + }; + labelMap?: Record; +} + +class TopicClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getTableDetailPageTabs( + tableDetailsPageProps: TopicDetailPageTabProps + ): TabProps[] { + return getTopicDetailsPageTabs(tableDetailsPageProps); + } + + public getTopicDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.SCHEMA, + EntityTabs.ACTIVITY_FEED, + EntityTabs.SAMPLE_DATA, + EntityTabs.CONFIG, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [ + EntityTabs.SCHEMA, + EntityTabs.OVERVIEW, + EntityTabs.TERMS, + ].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.TOPIC_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getAlertEnableStatus() { + return false; + } + + public getDummyData(): Topic { + return { + id: 'd68d2e86-41cd-4c6c-bd78-41db489d46d1', + name: 'address_book', + fullyQualifiedName: 'sample_kafka.address_book', + description: + 'All Protobuf record related events gets captured in this topic', + version: 0.7, + updatedAt: 1701432879105, + updatedBy: 'aniket', + service: { + id: '46f09e52-34de-4580-8133-a7e54e23c22b', + type: 'messagingService', + name: 'sample_kafka', + fullyQualifiedName: 'sample_kafka', + displayName: 'sample_kafka', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/messagingServices/46f09e52-34de-4580-8133-a7e54e23c22b', + }, + serviceType: 'Kafka', + messageSchema: { + schemaText: + // eslint-disable-next-line max-len + 'syntax = "proto2";\n\npackage tutorial;\n\nmessage Person {\n optional string name = 1;\n optional int32 id = 2;\n optional string email = 3;\n\n enum PhoneType {\n MOBILE = 0;\n HOME = 1;\n WORK = 2;\n }\n\n message PhoneNumber {\n optional string number = 1;\n optional PhoneType type = 2 [default = HOME];\n }\n\n repeated PhoneNumber phones = 4;\n}\n\nmessage AddressBook {\n repeated Person people = 1;\n}', + schemaType: 'Protobuf', + schemaFields: [ + { + name: 'AddressBook', + dataType: 'RECORD', + fullyQualifiedName: 'sample_kafka.address_book.AddressBook', + tags: [], + children: [ + { + name: 'people', + dataType: 'RECORD', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people', + tags: [], + children: [ + { + name: 'name', + dataType: 'STRING', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.name', + tags: [], + }, + { + name: 'id', + dataType: 'INT', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.id', + tags: [], + }, + { + name: 'email', + dataType: 'STRING', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.email', + tags: [], + }, + { + name: 'phones', + dataType: 'RECORD', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones', + tags: [], + children: [ + { + name: 'number', + dataType: 'STRING', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones.number', + tags: [], + }, + { + name: 'type', + dataType: 'ENUM', + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones.type', + tags: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + partitions: 128, + cleanupPolicies: ['compact', 'delete'], + replicationFactor: 4, + maximumMessageSize: 249, + retentionSize: 1931232624, + owners: [ + { + id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', + type: 'team', + name: 'Data', + fullyQualifiedName: 'Data', + description: '', + displayName: 'Data', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/ebac156e-6779-499c-8bbf-ab98a6562bc5', + }, + ], + followers: [ + { + id: '96546482-1b99-4293-9e0f-7194fe25bcbf', + type: 'user', + name: 'sonal.w', + fullyQualifiedName: '"sonal.w"', + displayName: 'admin', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/96546482-1b99-4293-9e0f-7194fe25bcbf', + }, + ], + tags: [], + href: 'http://sandbox-beta.open-metadata.org/api/v1/topics/d68d2e86-41cd-4c6c-bd78-41db489d46d1', + changeDescription: { + fieldsAdded: [], + fieldsUpdated: [ + { + name: 'deleted', + oldValue: true, + newValue: false, + }, + ], + fieldsDeleted: [], + previousVersion: 0.6, + }, + deleted: false, + domain: { + id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', + type: 'domain', + name: 'domain.with.dot', + fullyQualifiedName: '"domain.with.dot"', + description: 'domain.with.dot', + displayName: 'domain.with.dot', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/761f0a12-7b08-4889-acc3-b8d4d11a7865', + }, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + } as Topic; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TOPIC_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const topicClassBase = new TopicClassBase(); + +export default topicClassBase; +export { TopicClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts deleted file mode 100644 index 804965019ee6..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2022 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 { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../enums/entity.enum'; - -export const getTopicDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx new file mode 100644 index 000000000000..bad8f65576ac --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx @@ -0,0 +1,166 @@ +/* + * Copyright 2022 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 React from 'react'; +import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import i18n from './i18next/LocalUtil'; +import { TopicDetailPageTabProps } from './TopicClassBase'; + +export const getTopicDetailsPageDefaultLayout = (tab: EntityTabs) => { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.TABLE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + w: 2, + x: 6, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 3, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 4, + static: false, + }, + ]; + + default: + return []; + } +}; + +export const getTopicDetailsPageTabs = ({ + schemaCount, + schemaTab, + activityFeedTab, + sampleDataTab, + queryViewerTab, + lineageTab, + customPropertiesTab, + viewSampleDataPermission, + activeTab, + feedCount, +}: TopicDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.SCHEMA, + children: schemaTab, + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: activityFeedTab, + }, + { + label: ( + + ), + key: EntityTabs.SAMPLE_DATA, + children: !viewSampleDataPermission ? ( +
+ +
+ ) : ( + sampleDataTab + ), + }, + { + label: , + key: EntityTabs.CONFIG, + children: queryViewerTab, + }, + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: lineageTab, + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: customPropertiesTab, + }, + ]; +}; From 5eaff9a9686d8041b7e6f5d0d010ba7fa0a5861c Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:46:23 +0530 Subject: [PATCH 09/63] update topic with persona changes --- .../GenericProvider/GenericProvider.tsx | 14 +++++++-- .../SynonymsWidget/GenericWidget.tsx | 14 ++++++--- .../TopicDetails/TopicDetails.component.tsx | 31 ++++++++++--------- .../TopicDetails/TopicDetails.interface.ts | 2 +- .../Topic/TopicSchema/TopicSchema.test.tsx | 14 +-------- .../Topic/TopicSchema/TopicSchema.tsx | 23 +++++++++++--- .../TopicVersion/TopicVersion.component.tsx | 25 ++------------- .../TopicDetailsPage.component.tsx | 4 +-- 8 files changed, 61 insertions(+), 66 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx index 44886698508d..89f32dfeedfc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx @@ -22,6 +22,7 @@ interface GenericProviderProps { onUpdate: (updatedData: T) => Promise; isVersionView?: boolean; permissions: OperationPermission; + currentVersionData?: T; } interface GenericContextType { @@ -30,6 +31,7 @@ interface GenericContextType { onUpdate: (updatedData: T) => Promise; isVersionView?: boolean; permissions: OperationPermission; + currentVersionData?: T; } const createGenericContext = once(() => @@ -43,12 +45,20 @@ export const GenericProvider = ({ onUpdate, isVersionView, permissions, + currentVersionData, }: GenericProviderProps) => { const GenericContext = createGenericContext(); const values = useMemo( - () => ({ data, type, onUpdate, isVersionView, permissions }), - [data, type, onUpdate, isVersionView, permissions] + () => ({ + data, + type, + onUpdate, + isVersionView, + permissions, + currentVersionData, + }), + [data, type, onUpdate, isVersionView, permissions, currentVersionData] ); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 6d6b310f81d0..91059a7924a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -20,6 +20,7 @@ import { } from '../../../../enums/CustomizeDetailPage.enum'; import { EntityType } from '../../../../enums/entity.enum'; import { DataType, Table } from '../../../../generated/entity/data/table'; +import { Topic } from '../../../../generated/entity/data/topic'; import { EntityReference } from '../../../../generated/tests/testCase'; import { TagSource } from '../../../../generated/type/tagLabel'; import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; @@ -29,6 +30,7 @@ import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; import { getJoinsFromTableJoins } from '../../../../utils/TableUtils'; +import topicClassBase from '../../../../utils/TopicClassBase'; import { ExtensionTable } from '../../../common/CustomPropertyTable/ExtensionTable'; import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; @@ -576,11 +578,13 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { return ( - noop()} - /> + + data={topicClassBase.getDummyData()} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.TOPIC} + onUpdate={async () => noop()}> + + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index 5f296e6f77e9..c4407546be8d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -55,6 +55,7 @@ import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import SampleDataWithMessages from '../../Database/SampleDataWithMessages/SampleDataWithMessages'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -339,14 +340,8 @@ const TopicDetails: React.FC = ({ onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -529,15 +524,21 @@ const TopicDetails: React.FC = ({ onVersionClick={versionHandler} /> -
- - + + data={topicDetails} + permissions={topicPermissions} + type={EntityType.TOPIC} + onUpdate={onTopicUpdate}> + + + + <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts index 2425863c1674..7bf9640f4237 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts @@ -27,6 +27,6 @@ export interface TopicDetailsProps { unFollowTopicHandler: () => Promise; versionHandler: () => void; onUpdateVote: (data: QueryVote, id: string) => Promise; - onTopicUpdate: (updatedData: Topic, key: keyof Topic) => Promise; + onTopicUpdate: (updatedData: Topic, key?: keyof Topic) => Promise; handleToggleDelete: (version?: number) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx index 48d2a748cb99..a2da32452ab3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx @@ -22,21 +22,11 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { Topic } from '../../../generated/entity/data/topic'; -import { MESSAGE_SCHEMA } from '../TopicDetails/TopicDetails.mock'; import TopicSchema from './TopicSchema'; import { TopicSchemaFieldsProps } from './TopicSchema.interface'; -const mockOnUpdate = jest.fn(); - const mockProps: TopicSchemaFieldsProps = { - messageSchema: MESSAGE_SCHEMA as Topic['messageSchema'], - hasDescriptionEditAccess: true, isReadOnly: false, - onUpdate: mockOnUpdate, - hasTagEditAccess: true, - hasGlossaryTermEditAccess: true, - entityFqn: 'topic.fqn', onThreadLinkSelect: jest.fn(), }; @@ -162,9 +152,7 @@ describe('Topic Schema', () => { }); it('Should not render the edit action if isReadOnly', async () => { - render( - - ); + render(); const rows = await screen.findAllByRole('row'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index facc7ce3771b..0f477ceff060 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -24,7 +24,7 @@ import { import Table, { ColumnsType } from 'antd/lib/table'; import { Key } from 'antd/lib/table/interface'; import classNames from 'classnames'; -import { cloneDeep, groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; +import { cloneDeep, groupBy, isEmpty, isUndefined, noop, uniqBy } from 'lodash'; import { EntityTags, TagFilterOptions } from 'Models'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,12 +34,14 @@ import { EntityType } from '../../../enums/entity.enum'; import { DataTypeTopic, Field, + MessageSchemaObject, Topic, } from '../../../generated/entity/data/topic'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { getEntityName } from '../../../utils/EntityUtils'; +import { getVersionedSchema } from '../../../utils/SchemaVersionUtils'; import { getAllTags, searchTagInData, @@ -83,8 +85,19 @@ const TopicSchemaFields: FC = ({ isVersionView, permissions, onUpdate, + currentVersionData, } = useGenericContext(); - const { messageSchema } = topicDetails; + + const messageSchema = useMemo( + () => + isVersionView && currentVersionData?.changeDescription + ? getVersionedSchema( + currentVersionData?.['messageSchema'] as MessageSchemaObject, + currentVersionData?.changeDescription + ) + : topicDetails.messageSchema, + [currentVersionData, isVersionView, topicDetails] + ); const { hasDescriptionEditAccess, @@ -227,7 +240,7 @@ const TopicSchemaFields: FC = ({ index={index} isReadOnly={isReadOnly} onClick={() => setEditFieldDescription(record)} - onThreadLinkSelect={onThreadLinkSelect} + onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), }, @@ -256,7 +269,7 @@ const TopicSchemaFields: FC = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} + onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), filters: tagFilter.Classification, @@ -288,7 +301,7 @@ const TopicSchemaFields: FC = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} + onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), filters: tagFilter.Glossary, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx index b02d0ee8038e..2dcd1b198e49 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx @@ -20,17 +20,13 @@ import { useHistory, useParams } from 'react-router-dom'; import { getVersionPath } from '../../../constants/constants'; import { EntityField } from '../../../constants/Feeds.constants'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; -import { - ChangeDescription, - MessageSchemaObject, -} from '../../../generated/entity/data/topic'; +import { ChangeDescription } from '../../../generated/entity/data/topic'; import { TagSource } from '../../../generated/type/tagLabel'; import { getCommonExtraInfoForVersionDetails, getEntityVersionByField, getEntityVersionTags, } from '../../../utils/EntityVersionUtils'; -import { getVersionedSchema } from '../../../utils/SchemaVersionUtils'; import { stringToHTML } from '../../../utils/StringsUtils'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; @@ -77,15 +73,6 @@ const TopicVersion: FC = ({ [changeDescription, owners, tier, domain] ); - const messageSchemaDiff = useMemo( - () => - getVersionedSchema( - currentVersionData['messageSchema'] as MessageSchemaObject, - changeDescription - ), - [currentVersionData, changeDescription] - ); - useEffect(() => { setChangeDescription( currentVersionData.changeDescription as ChangeDescription @@ -156,7 +143,6 @@ const TopicVersion: FC = ({ @@ -206,14 +192,7 @@ const TopicVersion: FC = ({ ), }, ], - [ - description, - messageSchemaDiff, - currentVersionData, - entityPermissions, - schemaType, - tags, - ] + [description, currentVersionData, entityPermissions, schemaType, tags] ); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx index 46aad28920dc..0bff0d427c90 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx @@ -80,7 +80,7 @@ const TopicDetailsPage: FunctionComponent = () => { return patchTopicDetails(topicId, jsonPatch); }; - const onTopicUpdate = async (updatedData: Topic, key: keyof Topic) => { + const onTopicUpdate = async (updatedData: Topic, key?: keyof Topic) => { try { const res = await saveUpdatedTopicData(updatedData); @@ -95,8 +95,8 @@ const TopicDetailsPage: FunctionComponent = () => { return { ...previous, + ...res, version: res.version, - [key]: res[key], }; }); } catch (error) { From 4be2471b5548488e96acd1da2a3ec724f36b01c4 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:58:34 +0530 Subject: [PATCH 10/63] support dashboard data model --- .../DataModels/DataModelDetails.component.tsx | 165 ++-------- .../ModelTab/ModelTab.component.tsx | 42 ++- .../ModelTab/ModelTab.interface.tsx | 7 - .../SynonymsWidget/GenericWidget.tsx | 13 + .../ui/src/enums/CustomizeDetailPage.enum.ts | 2 + .../CustomizablePage/CustomizablePage.tsx | 26 +- .../CustomizeTableDetailPage.tsx | 4 +- .../StoredProcedure/StoredProcedurePage.tsx | 251 ++++----------- .../utils/CustomizePage/CustomizePageUtils.ts | 27 ++ .../ui/src/utils/DashboardDataModelBase.ts | 245 ++++++++++++++ ...ailsUtils.ts => DashboardDetailsUtils.tsx} | 136 +++++++- .../ui/src/utils/StoredProcedureBase.ts | 302 ++++++++++++++++++ .../ui/src/utils/StoredProceduresUtils.tsx | 186 ++++++++++- 13 files changed, 1041 insertions(+), 365 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts rename openmetadata-ui/src/main/resources/ui/src/utils/{DashboardDetailsUtils.ts => DashboardDetailsUtils.tsx} (52%) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index f8e88ef56e5e..8bd7f3a4c9bc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ -import { Card, Col, Row, Tabs } from 'antd'; +import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; import { isEmpty, isUndefined, toString } from 'lodash'; import { EntityTags } from 'Models'; @@ -24,8 +24,6 @@ import { } from '../../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../../constants/entity.constants'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../../context/LineageProvider/LineageProvider'; -import { CSMode } from '../../../../enums/codemirror.enum'; import { EntityTabs, EntityType } from '../../../../enums/entity.enum'; import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { TagLabel } from '../../../../generated/type/tagLabel'; @@ -33,25 +31,21 @@ import { useFqn } from '../../../../hooks/useFqn'; import { FeedCounts } from '../../../../interface/feed.interface'; import { restoreDataModel } from '../../../../rest/dataModelsAPI'; import { getFeedCounts } from '../../../../utils/CommonUtils'; +import { getDashboardDataModelDetailPageTabs } from '../../../../utils/DashboardDetailsUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getTagsWithoutTier } from '../../../../utils/TableUtils'; import { createTagObject } from '../../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor'; import EntityRightPanel from '../../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../../Lineage/Lineage.component'; +import { GenericProvider } from '../../../GenericProvider/GenericProvider'; import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../../SearchedData/SearchedData.interface'; import { DataModelDetailsProps } from './DataModelDetails.interface'; import ModelTab from './ModelTab/ModelTab.component'; @@ -255,14 +249,8 @@ const DataModelDetails = ({ onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -319,112 +307,19 @@ const DataModelDetails = ({ ]); const tabs = useMemo(() => { - const allTabs = [ - { - label: ( - - ), - key: EntityTabs.MODEL, - children: modelComponent, - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - ...(dataModelData?.sql - ? [ - { - label: ( - - ), - key: EntityTabs.SQL, - children: ( - - - - ), - }, - ] - : []), - { - label: ( - - ), - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: ( -
- - entityDetails={dataModelData} - entityType={EntityType.DASHBOARD_DATA_MODEL} - handleExtensionUpdate={handelExtensionUpdate} - hasEditAccess={ - dataModelPermissions.EditAll || - dataModelPermissions.EditCustomFields - } - hasPermission={dataModelPermissions.ViewAll} - isVersionView={false} - /> -
- ), - }, - ]; + const allTabs = getDashboardDataModelDetailPageTabs({ + modelComponent, + feedCount, + activeTab, + handleFeedCount, + editLineagePermission, + dataModelData, + dataModelPermissions, + deleted: deleted ?? false, + handelExtensionUpdate, + getEntityFeedCount, + fetchDataModel, + }); return allTabs; }, [ @@ -464,17 +359,23 @@ const DataModelDetails = ({ onVersionClick={versionHandler} /> -
- - handleTabChange(activeKey as EntityTabs) - } - /> - + + data={dataModelData} + permissions={dataModelPermissions} + type={EntityType.DASHBOARD_DATA_MODEL} + onUpdate={onUpdateDataModel}> + + + handleTabChange(activeKey as EntityTabs) + } + /> + + {threadLink ? ( { +const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { const { t } = useTranslation(); const [editColumnDescription, setEditColumnDescription] = useState(); + const { + data: { columns: data, fullyQualifiedName: entityFqn }, + permissions, + onUpdate, + } = useGenericContext(); + + const { + hasEditDescriptionPermission, + hasEditTagsPermission, + hasEditGlossaryTermPermission, + } = useMemo(() => { + return { + hasEditDescriptionPermission: + permissions.EditAll || permissions.EditDescription, + hasEditTagsPermission: permissions.EditAll || permissions.EditTags, + hasEditGlossaryTermPermission: + permissions.EditAll || permissions.EditGlossaryTerms, + }; + }, [permissions]); const tagFilter = useMemo(() => { const tags = getAllTags(data ?? []); @@ -125,7 +139,7 @@ const ModelTab = ({ fqn: record.fullyQualifiedName ?? '', field: record.description, }} - entityFqn={entityFqn} + entityFqn={entityFqn ?? ''} entityType={EntityType.DASHBOARD_DATA_MODEL} hasEditPermission={hasEditDescriptionPermission} index={index} @@ -146,7 +160,7 @@ const ModelTab = ({ onFilter: searchTagInData, render: (tags: TagLabel[], record: Column, index: number) => ( - entityFqn={entityFqn} + entityFqn={entityFqn ?? ''} entityType={EntityType.DASHBOARD_DATA_MODEL} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasEditTagsPermission} @@ -170,7 +184,7 @@ const ModelTab = ({ onFilter: searchTagInData, render: (tags: TagLabel[], record: Column, index: number) => ( - entityFqn={entityFqn} + entityFqn={entityFqn ?? ''} entityType={EntityType.DASHBOARD_DATA_MODEL} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasEditGlossaryTermPermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx index 02c4fd2f4b81..503ebd6529ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx @@ -11,15 +11,8 @@ * limitations under the License. */ import { ThreadType } from '../../../../../generated/api/feed/createThread'; -import { Column } from '../../../../../generated/entity/data/dashboardDataModel'; export interface ModelTabProps { - data: Column[]; - entityFqn: string; isReadOnly: boolean; - hasEditTagsPermission: boolean; - hasEditGlossaryTermPermission: boolean; - hasEditDescriptionPermission: boolean; onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; - onUpdate: (updatedDataModel: Column[]) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 91059a7924a9..70cd35b11142 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -19,6 +19,7 @@ import { GlossaryTermDetailPageWidgetKeys, } from '../../../../enums/CustomizeDetailPage.enum'; import { EntityType } from '../../../../enums/entity.enum'; +import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { DataType, Table } from '../../../../generated/entity/data/table'; import { Topic } from '../../../../generated/entity/data/topic'; import { EntityReference } from '../../../../generated/tests/testCase'; @@ -26,6 +27,7 @@ import { TagSource } from '../../../../generated/type/tagLabel'; import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import TableConstraints from '../../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; +import dashboardDataModelClassBase from '../../../../utils/DashboardDataModelBase'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; @@ -36,6 +38,7 @@ import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import RichTextEditorPreviewerV1 from '../../../common/RichTextEditor/RichTextEditorPreviewerV1'; import TagButton from '../../../common/TagButton/TagButton.component'; +import ModelTab from '../../../Dashboard/DataModel/DataModels/ModelTab/ModelTab.component'; import SchemaTable from '../../../Database/SchemaTable/SchemaTable.component'; import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import { GenericProvider } from '../../../GenericProvider/GenericProvider'; @@ -586,6 +589,16 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); + } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.DATA_MODEL)) { + return ( + + data={dashboardDataModelClassBase.getDummyData()} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.DASHBOARD_DATA_MODEL} + onUpdate={async () => noop()}> + + + ); } return widgetName; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index e89704dbbebd..6fc8ea75e3b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -32,6 +32,8 @@ export enum DetailPageWidgetKeys { TABLE_CONSTRAINTS = 'KnowledgePanel.TableConstraints', EMPTY_WIDGET_PLACEHOLDER = 'ExtraWidget.EmptyWidgetPlaceholder', PARTITIONED_KEYS = 'KnowledgePanel.PartitionedKeys', + STORED_PROCEDURE_CODE = 'KnowledgePanel.StoredProcedureCode', + DATA_MODEL = 'KnowledgePanel.DataModel', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx index ecd412887d73..64cd593909f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx @@ -266,32 +266,8 @@ export const CustomizablePage = () => { /> ); case PageType.Table: - return ( - - ); - case PageType.StoredProcedure: - return ( - - ); case PageType.Topic: - return ( - - ); + case PageType.StoredProcedure: case PageType.DashboardDataModel: return ( { const handleStoreProcedureUpdate = async ( updatedData: StoredProcedure, - key: keyof StoredProcedure + key?: keyof StoredProcedure ) => { try { const res = await saveUpdatedStoredProceduresData(updatedData); @@ -254,7 +244,7 @@ const StoredProcedurePage = () => { return { ...previous, version: res.version, - [key]: res[key], + ...res, }; }); } catch (error) { @@ -536,166 +526,55 @@ const StoredProcedurePage = () => { [storedProcedurePermissions, storedProcedure] ); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.CODE, - children: ( - - - - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={storedProcedure} - dataProducts={storedProcedure?.dataProducts ?? []} - domain={storedProcedure?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityFQN={decodedStoredProcedureFQN} - entityId={storedProcedure?.id ?? ''} - entityType={EntityType.STORED_PROCEDURE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: storedProcedure && ( - - entityDetails={storedProcedure} - entityType={EntityType.STORED_PROCEDURE} - handleExtensionUpdate={onExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> - ), - }, - ], - [ + const tabs = useMemo(() => { + return getStoredProcedureDetailsPageTabs({ + activeTab: activeTab as EntityTabs, + feedCount, + description: description ?? '', + decodedStoredProcedureFQN, + entityName, code, - tags, isEdit, - deleted, - feedCount.totalCount, - activeTab, - entityFQN, - entityName, - description, - storedProcedure, - decodedStoredProcedureFQN, + deleted: deleted ?? false, + owners: owners ?? [], + editDescriptionPermission, + onCancel, + onDescriptionEdit, + onDescriptionUpdate, + onThreadLinkSelect, + storedProcedure: storedProcedure as StoredProcedure, + tags: tags ?? [], editTagsPermission, editGlossaryTermsPermission, editLineagePermission, - editDescriptionPermission, editCustomAttributePermission, viewAllPermission, - handleFeedCount, - ] - ); + onExtensionUpdate, + handleTagSelection, + getEntityFeedCount: getEntityFeedCount, + fetchStoredProcedureDetails, + handleFeedCount: handleFeedCount, + }); + }, [ + code, + tags, + isEdit, + deleted, + feedCount.totalCount, + activeTab, + entityFQN, + entityName, + description, + storedProcedure, + decodedStoredProcedureFQN, + editTagsPermission, + editGlossaryTermsPermission, + editLineagePermission, + editDescriptionPermission, + editCustomAttributePermission, + viewAllPermission, + handleFeedCount, + ]); const updateVote = async (data: QueryVote, id: string) => { try { @@ -763,18 +642,24 @@ const StoredProcedurePage = () => { /> - {/* Entity Tabs */} -
- - handleTabChange(activeKey as EntityTabs) - } - /> - + + {/* Entity Tabs */} + + + handleTabChange(activeKey as EntityTabs) + } + /> + + <> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 934be103afac..e86d3463a213 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -16,7 +16,9 @@ import { EntityTabs } from '../../enums/entity.enum'; import { PageType, Tab } from '../../generated/system/ui/page'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; +import dashboardDataModelClassBase from '../DashboardDataModelBase'; import i18n from '../i18next/LocalUtil'; +import storedProcedureClassBase from '../StoredProcedureBase'; import tableClassBase from '../TableClassBase'; import topicClassBase from '../TopicClassBase'; @@ -140,6 +142,19 @@ export const getTopicDefaultTabs = () => { return tabs; }; +export const getStoredProcedureDefaultTabs = () => { + const tabs = storedProcedureClassBase.getStoredProcedureDetailPageTabsIds(); + + return tabs; +}; + +export const getDashboardDataModelDefaultTabs = () => { + const tabs = + dashboardDataModelClassBase.getDashboardDataModelDetailPageTabsIds(); + + return tabs; +}; + export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: @@ -150,6 +165,10 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { return getTableDefaultTabs(); case PageType.Topic: return getTopicDefaultTabs(); + case PageType.StoredProcedure: + return getStoredProcedureDefaultTabs(); + case PageType.DashboardDataModel: + return getDashboardDataModelDefaultTabs(); case PageType.Container: default: return [ @@ -174,6 +193,10 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return tableClassBase.getDefaultLayout(tab); case PageType.Topic: return topicClassBase.getDefaultLayout(tab); + case PageType.DashboardDataModel: + return dashboardDataModelClassBase.getDefaultLayout(tab); + case PageType.StoredProcedure: + return storedProcedureClassBase.getDefaultLayout(tab); default: return []; } @@ -227,6 +250,10 @@ export const getDummyDataByPage = (pageType: PageType) => { return tableClassBase.getDummyData(); case PageType.Topic: return topicClassBase.getDummyData(); + case PageType.StoredProcedure: + return storedProcedureClassBase.getDummyData(); + case PageType.DashboardDataModel: + return dashboardDataModelClassBase.getDummyData(); case PageType.LandingPage: default: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts new file mode 100644 index 000000000000..e40fad61fcd8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -0,0 +1,245 @@ +/* + * Copyright 2024 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 { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; +import { Tab } from '../generated/system/ui/page'; +import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import { getDashboardDataModelDetailPageTabs } from './DashboardDetailsUtils'; + +export interface DashboardDataModelDetailPageTabProps { + modelComponent: JSX.Element; + feedCount: { + totalCount: number; + }; + activeTab: EntityTabs; + handleFeedCount: (data: FeedCounts) => void; + editLineagePermission: boolean; + dataModelData: DashboardDataModel; + dataModelPermissions: OperationPermission; + deleted: boolean; + handelExtensionUpdate: ( + dashboardDataModel: DashboardDataModel + ) => Promise; + getEntityFeedCount: () => void; + fetchDataModel: () => void; +} + +class DashboardDataModelBase { + constructor() { + // Do nothing + } + + public getDashboardDataModelDetailPageTabs( + tabsProps: DashboardDataModelDetailPageTabProps + ): TabProps[] { + return getDashboardDataModelDetailPageTabs(tabsProps); + } + + public getDashboardDataModelDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.MODEL, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [EntityTabs.MODEL].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.MODEL: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 6, + i: DetailPageWidgetKeys.DATA_MODEL, + w: 6, + x: 0, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): DashboardDataModel { + return { + id: '3519340b-7a36-45d1-abdf-348a5f1c582c', + name: 'orders', + displayName: 'Orders', + fullyQualifiedName: 'sample_looker.model.orders', + description: 'Orders explore from Sample Data', + version: 0.1, + updatedAt: 1697265260863, + updatedBy: 'ingestion-bot', + href: 'http://sandbox-beta.open-metadata.org/api/v1/dashboard/datamodels/3519340b-7a36-45d1-abdf-348a5f1c582c', + owners: [], + dataProducts: [], + tags: [], + deleted: false, + followers: [], + service: { + id: '2d102aaa-e683-425c-a8bf-e4afa43dde99', + type: 'dashboardService', + name: 'sample_looker', + fullyQualifiedName: 'sample_looker', + displayName: 'sample_looker', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/dashboardServices/2d102aaa-e683-425c-a8bf-e4afa43dde99', + }, + serviceType: 'Looker', + dataModelType: 'LookMlExplore', + // eslint-disable-next-line max-len + sql: "SELECT CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END AS clinical_stage,\n COUNT(*) AS count\nFROM covid_vaccines\nGROUP BY CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END\nORDER BY count DESC\nLIMIT 10000\nOFFSET 0;\n", + columns: [ + { + name: '0. Pre-clinical', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Pre-clinical'", + fullyQualifiedName: 'sample_looker.model.orders."0. Pre-clinical"', + tags: [], + ordinalPosition: 1, + }, + { + name: '2. Phase II or Combined I/II', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + "Vaccine Candidates in phase: 'Phase II or Combined I/II'", + fullyQualifiedName: + 'sample_looker.model.orders."2. Phase II or Combined I/II"', + tags: [], + ordinalPosition: 2, + }, + { + name: '1. Phase I', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Phase I'", + fullyQualifiedName: 'sample_looker.model.orders."1. Phase I"', + tags: [], + ordinalPosition: 3, + }, + { + name: '3. Phase III', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Phase III'", + fullyQualifiedName: 'sample_looker.model.orders."3. Phase III"', + tags: [], + ordinalPosition: 4, + }, + { + name: '4. Authorized', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Authorize'", + fullyQualifiedName: 'sample_looker.model.orders."4. Authorized"', + tags: [], + ordinalPosition: 5, + }, + ], + domain: { + id: '52fc9c67-78b7-42bf-8147-69278853c230', + type: 'domain', + name: 'Design', + fullyQualifiedName: 'Design', + description: "

Here' the description for Product Design

", + displayName: 'Product Design ', + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/52fc9c67-78b7-42bf-8147-69278853c230', + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + } as DashboardDataModel; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const dashboardDataModelClassBase = new DashboardDataModelBase(); + +export default dashboardDataModelClassBase; +export { DashboardDataModelBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx similarity index 52% rename from openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts rename to openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 587248779288..6c8a4a5e56a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -11,13 +11,26 @@ * limitations under the License. */ +import { Card } from 'antd'; import { AxiosError } from 'axios'; +import React from 'react'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; +import Lineage from '../components/Lineage/Lineage.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { CSMode } from '../enums/codemirror.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Dashboard } from '../generated/entity/data/dashboard'; import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { getChartById } from '../rest/chartAPI'; import { sortTagsCaseInsensitive } from './CommonUtils'; +import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; +import i18next from './i18next/LocalUtil'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.DOMAIN},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; @@ -187,3 +200,124 @@ export const getDashboardDataModelDetailsPageDefaultLayout = ( return []; } }; + +export const getDashboardDataModelDetailPageTabs = ({ + modelComponent, + feedCount, + activeTab, + handleFeedCount, + editLineagePermission, + dataModelData, + dataModelPermissions, + deleted, + handelExtensionUpdate, + getEntityFeedCount, + fetchDataModel, +}: DashboardDataModelDetailPageTabProps): TabProps[] => { + return [ + { + label: ( + + ), + key: EntityTabs.MODEL, + children: modelComponent, + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + ...(dataModelData?.sql + ? [ + { + label: ( + + ), + key: EntityTabs.SQL, + children: ( + + + + ), + }, + ] + : []), + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: ( +
+ + entityDetails={dataModelData} + entityType={EntityType.DASHBOARD_DATA_MODEL} + handleExtensionUpdate={handelExtensionUpdate} + hasEditAccess={ + dataModelPermissions.EditAll || + dataModelPermissions.EditCustomFields + } + hasPermission={dataModelPermissions.ViewAll} + isVersionView={false} + /> +
+ ), + }, + ]; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts new file mode 100644 index 000000000000..f58dfbe003f3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -0,0 +1,302 @@ +/* + * Copyright 2024 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 { EntityTags } from 'Models'; +import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { Tag } from '../generated/entity/classification/tag'; +import { StoredProcedure } from '../generated/entity/data/storedProcedure'; +import { ThreadType } from '../generated/entity/feed/thread'; +import { EntityReference } from '../generated/entity/type'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import i18n from './i18next/LocalUtil'; +import { getStoredProcedureDetailsPageTabs } from './StoredProceduresUtils'; + +export interface StoredProcedureDetailPageTabProps { + activeTab: EntityTabs; + feedCount: { + totalCount: number; + }; + description: string; + decodedStoredProcedureFQN: string; + entityName: string; + code: string; + isEdit: boolean; + deleted: boolean; + owners: EntityReference[]; + editDescriptionPermission: boolean; + onCancel: () => void; + onDescriptionEdit: () => void; + onDescriptionUpdate: (value: string) => Promise; + onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; + storedProcedure: StoredProcedure; + tags: Tag[]; + editTagsPermission: boolean; + editGlossaryTermsPermission: boolean; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + labelMap?: Record; + onExtensionUpdate: (value: StoredProcedure) => Promise; + handleTagSelection: (selectedTags: EntityTags[]) => Promise; + getEntityFeedCount: () => void; + fetchStoredProcedureDetails: () => Promise; + handleFeedCount: (data: FeedCounts) => void; +} + +class StoredProcedureClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getStoredProcedureDetailPageTabs( + tabsProps: StoredProcedureDetailPageTabProps + ): TabProps[] { + return getStoredProcedureDetailsPageTabs(tabsProps); + } + + public getStoredProcedureDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.CODE, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [EntityTabs.CODE].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.CODE: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.TOPIC_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getAlertEnableStatus() { + return false; + } + + public getDummyData(): StoredProcedure { + return { + id: '5f3509ca-c8e5-449a-a7e8-b1c3b96e39e8', + name: 'calculate_average', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.calculate_average', + description: 'Procedure to calculate average', + storedProcedureCode: { + // eslint-disable-next-line max-len + code: 'CREATE OR REPLACE PROCEDURE calculate_average(numbers INT ARRAY) RETURNS FLOAT NOT NULL LANGUAGE SQL AS $$DECLARE sum_val INT = 0;count_val INT = 0;average_val FLOAT;BEGIN\n FOR num IN ARRAY numbers DO sum_val := sum_val + num;\n count_val := count_val + 1;\nEND FOR;\nIF count_val = 0 THEN\n average_val := 0.0;\nELSE\n average_val := sum_val / count_val;\nEND IF;\nRETURN average_val;\nEND;$$;', + }, + version: 0.5, + dataProducts: [], + updatedAt: 1709544812674, + updatedBy: 'admin', + href: 'http://sandbox-beta.open-metadata.org/api/v1/storedProcedures/5f3509ca-c8e5-449a-a7e8-b1c3b96e39e8', + changeDescription: { + fieldsAdded: [], + fieldsUpdated: [ + { + name: 'deleted', + oldValue: true, + newValue: false, + }, + ], + fieldsDeleted: [], + previousVersion: 0.4, + }, + databaseSchema: { + id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', + type: 'databaseSchema', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + displayName: 'shopify', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/databaseSchemas/9f127bdc-d060-4fac-ae7b-c635933fc2e0', + }, + database: { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + displayName: 'ecommerce_db', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', + }, + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', + }, + serviceType: 'BigQuery', + deleted: false, + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', + }, + { + id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + type: 'user', + name: 'ayush', + fullyQualifiedName: 'ayush', + displayName: 'Ayush Shah', + deleted: false, + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + }, + { + id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', + type: 'user', + name: 'ayush02shah12', + fullyQualifiedName: 'ayush02shah12', + displayName: 'Ayush Shah', + deleted: false, + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', + }, + { + id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', + type: 'user', + name: 'prajwal161998', + fullyQualifiedName: 'prajwal161998', + displayName: 'prajwal161998', + deleted: false, + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', + }, + ], + followers: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + tags: [], + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + inherited: true, + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + }, + } as StoredProcedure; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.STORED_PROCEDURE_CODE, + name: i18n.t('label.stored_procedure_code'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const storedProcedureClassBase = new StoredProcedureClassBase(); + +export default storedProcedureClassBase; +export { StoredProcedureClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index b4deb38f412c..233ec93200a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -10,8 +10,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Card, Col, Row } from 'antd'; +import { t } from 'i18next'; +import { isEmpty } from 'lodash'; +import React from 'react'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; +import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; +import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; +import Lineage from '../components/Lineage/Lineage.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { CSMode } from '../enums/codemirror.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { StoredProcedureDetailPageTabProps } from './StoredProcedureBase'; // eslint-disable-next-line max-len export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}, ${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; @@ -82,3 +99,170 @@ export const getStoredProceduresPageDefaultLayout = (tab: EntityTabs) => { return []; } }; + +export const getStoredProcedureDetailsPageTabs = ({ + activeTab, + feedCount, + description, + decodedStoredProcedureFQN, + entityName, + code, + isEdit, + deleted, + owners, + editDescriptionPermission, + onCancel, + onDescriptionEdit, + onDescriptionUpdate, + onThreadLinkSelect, + storedProcedure, + tags, + editTagsPermission, + editGlossaryTermsPermission, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + onExtensionUpdate, + handleTagSelection, + getEntityFeedCount, + fetchStoredProcedureDetails, + handleFeedCount, +}: StoredProcedureDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.CODE, + children: ( + +
+ + + + + + + + ), + ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, + }} + secondPanel={{ + children: ( +
+ + customProperties={storedProcedure} + dataProducts={storedProcedure?.dataProducts ?? []} + domain={storedProcedure?.domain} + editCustomAttributePermission={ + editCustomAttributePermission + } + editGlossaryTermsPermission={editGlossaryTermsPermission} + editTagPermission={editTagsPermission} + entityFQN={decodedStoredProcedureFQN} + entityId={storedProcedure?.id ?? ''} + entityType={EntityType.STORED_PROCEDURE} + selectedTags={tags} + viewAllPermission={viewAllPermission} + onExtensionUpdate={onExtensionUpdate} + onTagSelectionChange={handleTagSelection} + onThreadLinkSelect={onThreadLinkSelect} + /> +
+ ), + ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, + className: + 'entity-resizable-right-panel-container entity-resizable-panel-container', + }} + /> + + + ), + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + { + label: , + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: storedProcedure && ( + + entityDetails={storedProcedure} + entityType={EntityType.STORED_PROCEDURE} + handleExtensionUpdate={onExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> + ), + }, + ]; +}; From fd45499e53be34358aa878ac161bcf58b24050d3 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:36:23 +0530 Subject: [PATCH 11/63] support data assets with customization --- .../APIEndpointDetails/APIEndpointDetails.tsx | 15 - .../ClassificationDetails.interface.ts | 3 - .../ClassificationDetails.tsx | 7 - .../DashboardChartTable.tsx | 456 +++++++++++++ .../DashboardDetails.component.tsx | 613 +++--------------- .../DashboardDetails.interface.ts | 13 +- .../DashboardDetails.test.tsx | 9 +- .../DataModels/DataModelDetails.component.tsx | 14 +- .../DataAssetsHeader.component.tsx | 5 +- .../DataAssetsHeader.interface.ts | 1 + .../TestCaseResultTab.component.tsx | 6 - .../TableQueryRightPanel.component.tsx | 8 +- .../TableSchemaTab/TableSchemaTab.tsx | 11 +- .../DocumentationTab.component.tsx | 8 - .../EdgeInfoDrawer.component.tsx | 6 - .../CustomizeTabWidget/CustomizeTabWidget.tsx | 16 +- .../SynonymsWidget/GenericWidget.tsx | 22 + .../GlossaryDetails.component.tsx | 10 +- .../tabs/GlossaryOverviewTab.component.tsx | 9 - .../Metric/MetricDetails/MetricDetails.tsx | 16 - .../MlModelDetail/MlModelDetail.component.tsx | 14 - .../PipelineDetails.component.tsx | 18 - .../Bot/BotDetails/BotDetails.component.tsx | 6 - .../TeamDetails/TeamDetailsV1.interface.ts | 2 - .../Team/TeamDetails/TeamDetailsV1.tsx | 5 - .../Settings/Users/Users.component.tsx | 9 +- .../TopicDetails/TopicDetails.component.tsx | 17 +- .../CustomPropertyTable.tsx | 2 +- .../Description.interface.ts | 3 - .../EntityDescription/DescriptionV1.tsx | 35 +- .../ui/src/enums/CustomizeDetailPage.enum.ts | 3 + .../resources/ui/src/enums/entity.enum.ts | 1 + .../APICollectionPage/APICollectionPage.tsx | 17 +- .../APICollectionPage/APIEndpointsTab.tsx | 9 - .../AlertDetailsPage/AlertDetailsPage.tsx | 16 +- .../src/pages/ContainerPage/ContainerPage.tsx | 295 +++------ .../CustomizablePage/CustomizablePage.tsx | 6 + .../CustomizeTableDetailPage.tsx | 1 + .../DashboardDetailsPage.component.tsx | 75 +-- .../DatabaseDetailsPage.tsx | 233 +++---- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 9 - .../PersonaDetailsPage/PersonaDetailsPage.tsx | 10 - .../PoliciesDetailPage/PoliciesDetailPage.tsx | 6 - .../RolesDetailPage/RolesDetailPage.tsx | 6 - .../SearchIndexDetailsPage.tsx | 17 +- .../ServiceMainTabContent.tsx | 16 - .../ui/src/pages/TagPage/TagPage.tsx | 10 +- .../ui/src/pages/TagsPage/TagsPage.tsx | 14 - .../ui/src/pages/TeamsPage/TeamsPage.tsx | 12 - .../TestSuiteDetailsPage.component.tsx | 13 +- .../ui/src/utils/ContainerDetailUtils.ts | 167 ----- .../ui/src/utils/ContainerDetailUtils.tsx | 316 +++++++++ .../ui/src/utils/ContainerDetailsClassBase.ts | 249 +++++++ .../utils/CustomizePage/CustomizePageUtils.ts | 67 ++ .../ui/src/utils/DashboardDataModelBase.ts | 2 +- .../ui/src/utils/DashboardDataModelUtils.tsx | 148 +++++ .../ui/src/utils/DashboardDetailsClassBase.ts | 232 +++++++ .../ui/src/utils/DashboardDetailsUtils.tsx | 257 +++----- .../ui/src/utils/Database/Database.util.tsx | 156 ++++- .../src/utils/Database/DatabaseClassBase.ts | 295 +++++++++ .../ui/src/utils/DatabaseSchemaClassBase.ts | 203 +++++- .../src/utils/DatabaseSchemaDetailsUtils.tsx | 6 - .../ui/src/utils/PipelineClassBase.ts | 303 +++++++++ .../src/utils/SearchIndexDetailsClassBase.ts | 347 ++++++++++ .../ui/src/utils/StoredProceduresUtils.tsx | 6 - 65 files changed, 3229 insertions(+), 1653 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index 6e5feb6db968..a93a1f759210 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -77,7 +77,6 @@ const APIEndpointDetails: React.FC = ({ useParams<{ tab: EntityTabs }>(); const { fqn: decodedApiEndpointFqn } = useFqn(); const history = useHistory(); - const [isEdit, setIsEdit] = useState(false); const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -172,10 +171,6 @@ const APIEndpointDetails: React.FC = ({ } }; - const onDescriptionEdit = (): void => setIsEdit(true); - - const onCancel = () => setIsEdit(false); - const onDescriptionUpdate = async (updatedHTML: string) => { if (description !== updatedHTML) { const updatedApiEndpointDetails = { @@ -186,11 +181,7 @@ const APIEndpointDetails: React.FC = ({ await onApiEndpointUpdate(updatedApiEndpointDetails, 'description'); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } - } else { - setIsEdit(false); } }; const onOwnerUpdate = useCallback( @@ -315,11 +306,8 @@ const APIEndpointDetails: React.FC = ({ entityName={entityName} entityType={EntityType.API_ENDPOINT} hasEditAccess={editDescriptionPermission} - isEdit={isEdit} owner={apiEndpointDetails.owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -427,7 +415,6 @@ const APIEndpointDetails: React.FC = ({ ], [ - isEdit, activeTab, feedCount.totalCount, apiEndpointTags, @@ -436,8 +423,6 @@ const APIEndpointDetails: React.FC = ({ decodedApiEndpointFqn, fetchAPIEndpointDetails, deleted, - onCancel, - onDescriptionEdit, handleFeedCount, onExtensionUpdate, onThreadLinkSelect, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.interface.ts index 7f8241d2232c..6578dcdfaefb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.interface.ts @@ -20,15 +20,12 @@ export interface ClassificationDetailsProps { isVersionView?: boolean; currentClassification?: Classification; deleteTags?: DeleteTagsType; - isEditClassification?: boolean; isAddingTag?: boolean; disableEditButton?: boolean; handleAfterDeleteAction?: () => void; handleEditTagClick?: (selectedTag: Tag) => void; handleActionDeleteTag?: (record: Tag) => void; handleAddNewTagClick?: () => void; - handleEditDescriptionClick?: () => void; - handleCancelEditDescription?: () => void; handleUpdateClassification?: ( updatedClassification: Classification ) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.tsx index f544520a7546..ac3bae2413b3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Classifications/ClassificationDetails/ClassificationDetails.tsx @@ -72,7 +72,6 @@ const ClassificationDetails = forwardRef( { currentClassification, handleAfterDeleteAction, - isEditClassification, classificationPermissions, handleUpdateClassification, handleEditTagClick, @@ -80,10 +79,7 @@ const ClassificationDetails = forwardRef( isAddingTag, handleActionDeleteTag, handleAddNewTagClick, - handleEditDescriptionClick, - handleCancelEditDescription, disableEditButton, - isVersionView = false, }: Readonly, ref @@ -486,10 +482,7 @@ const ClassificationDetails = forwardRef( entityType={EntityType.CLASSIFICATION} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(tags)} - isEdit={isEditClassification} showCommentsIcon={false} - onCancel={handleCancelEditDescription} - onDescriptionEdit={handleEditDescriptionClick} onDescriptionUpdate={handleUpdateDescription} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx new file mode 100644 index 000000000000..950b151c580e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx @@ -0,0 +1,456 @@ +/* + * Copyright 2025 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 Icon, { FilterOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { AxiosError } from 'axios'; +import { compare, Operation } from 'fast-json-patch'; +import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; +import { EntityTags, TagFilterOptions } from 'Models'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg'; +import { DATA_ASSET_ICON_DIMENSION } from '../../../constants/constants'; +import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; +import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; +import { EntityType } from '../../../enums/entity.enum'; +import { TagLabel, TagSource } from '../../../generated/entity/data/chart'; +import { Dashboard } from '../../../generated/entity/data/dashboard'; +import { ThreadType } from '../../../generated/entity/feed/thread'; +import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component'; +import { updateChart } from '../../../rest/chartAPI'; +import { + fetchCharts, + sortTagsForCharts, +} from '../../../utils/DashboardDetailsUtils'; +import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; +import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import { + getAllTags, + searchTagInData, +} from '../../../utils/TableTags/TableTags.utils'; +import { createTagObject } from '../../../utils/TagsUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import Table from '../../common/Table/Table'; +import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; +import TableDescription from '../../Database/TableDescription/TableDescription.component'; +import TableTags from '../../Database/TableTags/TableTags.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import { ChartsPermissions } from '../DashboardDetails/DashboardDetails.interface'; + +interface DashboardChartTableProps { + onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; +} + +export const DashboardChartTable = ({ + onThreadLinkSelect, +}: DashboardChartTableProps) => { + const { t } = useTranslation(); + const { getEntityPermission } = usePermissionProvider(); + const { theme } = useApplicationStore(); + + const { data: dashboardDetails } = useGenericContext(); + const { charts: listChartIds } = dashboardDetails ?? {}; + + const [chartsPermissionsArray, setChartsPermissionsArray] = useState< + Array + >([]); + + const [charts, setCharts] = useState([]); + const [editChart, setEditChart] = useState<{ + chart: ChartType; + index: number; + }>(); + + const fetchChartPermissions = useCallback(async (id: string) => { + try { + const chartPermission = await getEntityPermission( + ResourceEntity.CHART, + id + ); + + return chartPermission; + } catch (error) { + return DEFAULT_ENTITY_PERMISSION; + } + }, []); + + const getAllChartsPermissions = useCallback( + async (charts: ChartType[]) => { + const permissionsArray: Array = []; + try { + await Promise.all( + charts.map(async (chart) => { + const chartPermissions = await fetchChartPermissions(chart.id); + permissionsArray.push({ + id: chart.id, + permissions: chartPermissions, + }); + }) + ); + + setChartsPermissionsArray(permissionsArray); + } catch { + showErrorToast( + t('server.fetch-entity-permissions-error', { + entity: t('label.chart'), + }) + ); + } + }, + [dashboardDetails] + ); + + useEffect(() => { + if (charts) { + getAllChartsPermissions(charts); + } + }, [charts]); + + const initializeCharts = useCallback(async () => { + try { + const res = await fetchCharts(listChartIds); + setCharts(res); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-fetch-error', { + entity: t('label.chart-plural'), + }) + ); + } + }, [listChartIds]); + + const handleUpdateChart = (chart: ChartType, index: number) => { + setEditChart({ chart, index }); + }; + + const closeEditChartModal = (): void => { + setEditChart(undefined); + }; + + const chartDescriptionUpdateHandler = async ( + index: number, + chartId: string, + patch: Array + ) => { + try { + const response = await updateChart(chartId, patch); + setCharts((prevCharts) => { + const charts = [...prevCharts]; + charts[index] = response; + + return charts; + }); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + + const onChartUpdate = async (chartDescription: string) => { + if (editChart) { + const updatedChart = { + ...editChart.chart, + description: chartDescription, + }; + const jsonPatch = compare(charts[editChart.index], updatedChart); + + try { + await chartDescriptionUpdateHandler( + editChart.index, + editChart.chart.id, + jsonPatch + ); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setEditChart(undefined); + } + } else { + setEditChart(undefined); + } + }; + + const tagFilter = useMemo(() => { + const tags = getAllTags(charts); + + return groupBy(uniqBy(tags, 'value'), (tag) => tag.source) as Record< + TagSource, + TagFilterOptions[] + >; + }, [charts]); + + const hasEditTagAccess = (record: ChartType) => { + const permissionsObject = chartsPermissionsArray?.find( + (chart) => chart.id === record.id + )?.permissions; + + return ( + !isUndefined(permissionsObject) && + (permissionsObject.EditTags || permissionsObject.EditAll) + ); + }; + + const hasEditGlossaryTermAccess = (record: ChartType) => { + const permissionsObject = chartsPermissionsArray?.find( + (chart) => chart.id === record.id + )?.permissions; + + return ( + !isUndefined(permissionsObject) && + (permissionsObject.EditGlossaryTerms || permissionsObject.EditAll) + ); + }; + + const chartTagUpdateHandler = async ( + chartId: string, + patch: Array + ) => { + try { + const res = await updateChart(chartId, patch); + + setCharts((prevCharts) => { + const charts = [...prevCharts].map((chart) => + chart.id === chartId ? res : chart + ); + + // Sorting tags as the response of PATCH request does not return the sorted order + // of tags, but is stored in sorted manner in the database + // which leads to wrong PATCH payload sent after further tags removal + return sortTagsForCharts(charts); + }); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-updating-error', { + entity: t('label.chart-plural'), + }) + ); + } + }; + + const handleChartTagSelection = async ( + selectedTags: Array, + editColumnTag: ChartType + ) => { + if (selectedTags && editColumnTag) { + const prevTags = editColumnTag.tags?.filter((tag) => + selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN) + ); + const newTags = createTagObject( + selectedTags.filter( + (selectedTag) => + !editColumnTag.tags?.some( + (tag) => tag.tagFQN === selectedTag.tagFQN + ) + ) + ); + + const updatedChart = { + ...editColumnTag, + tags: [...(prevTags as TagLabel[]), ...newTags], + }; + const jsonPatch = compare(editColumnTag, updatedChart); + await chartTagUpdateHandler(editColumnTag.id, jsonPatch); + } + }; + + const tableColumn: ColumnsType = useMemo( + () => [ + { + title: t('label.chart-entity', { + entity: t('label.name'), + }), + dataIndex: 'chartName', + key: 'chartName', + width: 220, + fixed: 'left', + sorter: getColumnSorter('name'), + render: (_, record) => { + const chartName = getEntityName(record); + + return record.sourceUrl ? ( +
+ + {chartName} + + + +
+ ) : ( + {chartName} + ); + }, + }, + { + title: t('label.chart-entity', { + entity: t('label.type'), + }), + dataIndex: 'chartType', + key: 'chartType', + width: 120, + }, + { + title: t('label.description'), + dataIndex: 'description', + key: 'description', + width: 350, + render: (_, record, index) => { + const permissionsObject = chartsPermissionsArray?.find( + (chart) => chart.id === record.id + )?.permissions; + + const editDescriptionPermissions = + !isUndefined(permissionsObject) && + (permissionsObject.EditDescription || permissionsObject.EditAll); + + return ( + handleUpdateChart(record, index)} + onThreadLinkSelect={onThreadLinkSelect} + /> + ); + }, + }, + { + title: t('label.tag-plural'), + dataIndex: 'tags', + key: 'tags', + accessor: 'tags', + width: 300, + filterIcon: (filtered) => ( + + ), + render: (tags: TagLabel[], record: ChartType, index: number) => { + return ( + + entityFqn={dashboardDetails?.fullyQualifiedName ?? ''} + entityType={EntityType.DASHBOARD} + handleTagSelection={handleChartTagSelection} + hasTagEditAccess={hasEditTagAccess(record)} + index={index} + isReadOnly={dashboardDetails?.deleted} + record={record} + tags={tags} + type={TagSource.Classification} + onThreadLinkSelect={onThreadLinkSelect} + /> + ); + }, + filters: tagFilter.Classification, + filterDropdown: ColumnFilter, + onFilter: searchTagInData, + }, + { + title: t('label.glossary-term-plural'), + dataIndex: 'tags', + key: 'glossary', + accessor: 'tags', + width: 300, + filterIcon: (filtered) => ( + + ), + render: (tags: TagLabel[], record: ChartType, index: number) => ( + + entityFqn={dashboardDetails?.fullyQualifiedName ?? ''} + entityType={EntityType.DASHBOARD} + handleTagSelection={handleChartTagSelection} + hasTagEditAccess={hasEditGlossaryTermAccess(record)} + index={index} + isReadOnly={dashboardDetails?.deleted} + record={record} + tags={tags} + type={TagSource.Glossary} + onThreadLinkSelect={onThreadLinkSelect} + /> + ), + filters: tagFilter.Glossary, + filterDropdown: ColumnFilter, + onFilter: searchTagInData, + }, + ], + [ + dashboardDetails?.deleted, + chartsPermissionsArray, + onThreadLinkSelect, + hasEditTagAccess, + handleUpdateChart, + handleChartTagSelection, + charts, + ] + ); + + useEffect(() => { + initializeCharts(); + }, [listChartIds]); + + if (isEmpty(charts)) { + return ; + } + + return ( + <> +
%^I>*L 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 Ormn2>uuReV)&Bwq_vsV> literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/glossary-term.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/glossary-term.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf20a830c71f84f875a86099b2aef082a2d3dfe GIT binary patch literal 22838 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 H8U_77Qqxwl literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-constraints.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-constraints.png new file mode 100644 index 0000000000000000000000000000000000000000..02e22a136e997be94e37b17f15ddf6f3438ff6f4 GIT binary patch literal 39223 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 P1O6$+m0nS}g5c^9Eq literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-schema.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/table-schema.png new file mode 100644 index 0000000000000000000000000000000000000000..b81c47fd380bd6b021658ef64a09ac023c8dcd10 GIT binary patch literal 173650 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 U8Ri-LJ}Sd+?m#Hh>R7h#*S7=7Uo>Ys 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 AQvd(} literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/tags.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/widgets/tags.png new file mode 100644 index 0000000000000000000000000000000000000000..0ccd29eb486c74b9d81b56bf2117a44c932c5e57 GIT binary patch literal 23231 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 literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 59ed77d8dc17..04db7374c545 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -24,6 +24,7 @@ import { EntityReference } from '../../../../generated/tests/testCase'; import { TagSource } from '../../../../generated/type/tagLabel'; import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import TableConstraints from '../../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; @@ -38,6 +39,8 @@ import DataProductsContainer from '../../../DataProducts/DataProductsContainer/D import { GenericProvider } from '../../../GenericProvider/GenericProvider'; import TagsViewer from '../../../Tag/TagsViewer/TagsViewer'; import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; +import GlossaryTermTab from '../../GlossaryTermTab/GlossaryTermTab.component'; +import { ModifiedGlossary, useGlossaryStore } from '../../useGlossary.store'; export const GenericWidget = (props: WidgetCommonProps) => { const handleRemoveClick = () => { @@ -46,6 +49,170 @@ export const GenericWidget = (props: WidgetCommonProps) => { } }; + const { setGlossaryChildTerms } = useGlossaryStore(); + + useMemo(() => { + if ( + props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.TERMS_TABLE) + ) { + setGlossaryChildTerms([ + { + id: 'ea7c8380-34a9-4ea9-93ea-a812c0e838d6', + name: 'Finance', + displayName: 'Finance', + description: + 'A finance department is the unit of a business responsible for obtaining and handling any monies on behalf of the organization', + + fullyQualifiedName: 'Business Department.Finance', + + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.9, + updatedAt: 1727894458563, + updatedBy: 'anandbhandari', + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/ea7c8380-34a9-4ea9-93ea-a812c0e838d6', + owners: [], + + status: 'Approved', + deleted: false, + + mutuallyExclusive: false, + childrenCount: 1, + }, + { + id: 'a8409ff4-b540-4ab0-9332-73f34125651c', + name: 'FOO', + displayName: '', + description: 'VCASCAS', + + fullyQualifiedName: 'Business Department.FOO', + synonyms: [], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.1, + updatedAt: 1724662513442, + updatedBy: 'teddy', + owners: [], + status: 'Approved', + deleted: false, + mutuallyExclusive: false, + childrenCount: 0, + }, + { + id: '5c415db9-0927-4815-b31b-ae8247ea6b0a', + name: 'Human resources', + displayName: 'Human resources', + description: + 'Human resources (HR) is the department in a company that handles all things related to employees.', + + fullyQualifiedName: 'Business Department.Human resources', + synonyms: ['Manpower', 'Human capital'], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaries/dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + }, + references: [], + version: 0.2, + updatedAt: 1701067069097, + updatedBy: 'sonal.w', + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/5c415db9-0927-4815-b31b-ae8247ea6b0a', + owners: [], + changeDescription: { + fieldsAdded: [], + fieldsUpdated: [ + { + name: 'status', + oldValue: 'Draft', + newValue: 'Approved', + }, + ], + fieldsDeleted: [], + previousVersion: 0.1, + }, + status: 'Approved', + deleted: false, + + mutuallyExclusive: false, + childrenCount: 0, + }, + { + id: 'e866ee75-711a-4649-968d-3ea889bd75b8', + name: 'Marketing', + displayName: 'Marketing', + description: + 'A marketing department is a division within a business that helps to promote its brand, products and services.', + style: {}, + fullyQualifiedName: 'Business Department.Marketing', + synonyms: ['Sell', 'Retails'], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaries/dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + }, + references: [], + version: 0.2, + updatedAt: 1700558309238, + updatedBy: 'shailesh', + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/e866ee75-711a-4649-968d-3ea889bd75b8', + owners: [], + + status: 'Rejected', + deleted: false, + + mutuallyExclusive: false, + childrenCount: 1, + }, + { + id: '288cfb46-a4c2-45a4-9dc0-321eac165812', + name: 'test_business_term', + displayName: 'Test Business Term', + description: 'this is test_business_term', + fullyQualifiedName: 'Business Department.test_business_term', + version: 0.2, + updatedAt: 1728547870161, + updatedBy: 'karan', + href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/288cfb46-a4c2-45a4-9dc0-321eac165812', + owners: [], + deleted: false, + mutuallyExclusive: false, + }, + ] as ModifiedGlossary[]); + } + + return () => setGlossaryChildTerms([]); + }, [props.widgetKey]); + const widgetName = startCase(props.widgetKey.replace('KnowledgePanel.', '')); const cardContent = useMemo(() => { @@ -382,6 +549,29 @@ export const GenericWidget = (props: WidgetCommonProps) => { showHeader={false} /> ); + } else if ( + props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.TERMS_TABLE) + ) { + return ( + + ); + } else if ( + props.widgetKey.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) + ) { + return ( + noop()} + /> + ); } return widgetName; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts index 5f8bd18798f8..affccd7311c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts @@ -67,7 +67,7 @@ export const OWNER_WIDGET: CommonWidgetType = { export const TERMS_TABLE_WIDGET: CommonWidgetType = { fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.TERMS_TABLE, - name: i18n.t('label.terms'), + name: i18n.t('label.term-plural'), data: { gridSizes: ['large'] }, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts index b0920e95f900..2b959870817d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts @@ -22,6 +22,22 @@ import MyDataImg from '../assets/img/my-data.png'; import RecentViewsImg from '../assets/img/recent-views.png'; import TotalAssetsMediumImg from '../assets/img/total-assets-medium.png'; import TotalAssetsImg from '../assets/img/total-assets.png'; +import CustomPropertyImg from '../assets/img/widgets/custom_properties.png'; +import DataProductImg from '../assets/img/widgets/data-products.png'; +import DescriptionLargeImg from '../assets/img/widgets/description-large.png'; +import DescriptionImg from '../assets/img/widgets/description.png'; +import DomainImg from '../assets/img/widgets/Domain.png'; +import FrequentlyJoinedTablesImg from '../assets/img/widgets/frequently-joined-tables.png'; +import GlossaryTermImg from '../assets/img/widgets/glossary-term.png'; +import OwnersImg from '../assets/img/widgets/owners.png'; +import ReferencesImg from '../assets/img/widgets/References.png'; +import RelatedTermsImg from '../assets/img/widgets/RelatedTerms.png'; +import ReviewersImg from '../assets/img/widgets/Reviewers.png'; +import SynonymsImg from '../assets/img/widgets/Synonyms.png'; +import TableConstraints from '../assets/img/widgets/table-constraints.png'; +import SchemaImg from '../assets/img/widgets/table-schema.png'; +import TagsImg from '../assets/img/widgets/tags.png'; +import TermsImg from '../assets/img/widgets/Terms.png'; import { MyDataWidget } from '../components/MyData/MyDataWidget/MyDataWidget.component'; import AnnouncementsWidget, { AnnouncementsWidgetProps, @@ -38,6 +54,10 @@ import { LandingPageWidgetKeys, WidgetWidths, } from '../enums/CustomizablePage.enum'; +import { + DetailPageWidgetKeys, + GlossaryTermDetailPageWidgetKeys, +} from '../enums/CustomizeDetailPage.enum'; import { WidgetCommonProps, WidgetConfig, @@ -216,6 +236,42 @@ class CustomizeMyDataPageClassBase { case LandingPageWidgetKeys.RECENTLY_VIEWED: { return RecentViewsImg; } + case DetailPageWidgetKeys.DESCRIPTION: + case GlossaryTermDetailPageWidgetKeys.DESCRIPTION: + if (size === WidgetWidths.large) { + return DescriptionLargeImg; + } + + return DescriptionImg; + case DetailPageWidgetKeys.CUSTOM_PROPERTIES: + case GlossaryTermDetailPageWidgetKeys.CUSTOM_PROPERTIES: + return CustomPropertyImg; + case GlossaryTermDetailPageWidgetKeys.DOMAIN: + return DomainImg; + case GlossaryTermDetailPageWidgetKeys.OWNER: + return OwnersImg; + case GlossaryTermDetailPageWidgetKeys.REFERENCES: + return ReferencesImg; + case GlossaryTermDetailPageWidgetKeys.RELATED_TERMS: + return RelatedTermsImg; + case GlossaryTermDetailPageWidgetKeys.REVIEWER: + return ReviewersImg; + case GlossaryTermDetailPageWidgetKeys.SYNONYMS: + return SynonymsImg; + case GlossaryTermDetailPageWidgetKeys.TERMS_TABLE: + return TermsImg; + case GlossaryTermDetailPageWidgetKeys.TAGS: + return TagsImg; + case DetailPageWidgetKeys.DATA_PRODUCTS: + return DataProductImg; + case DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES: + return FrequentlyJoinedTablesImg; + case DetailPageWidgetKeys.GLOSSARY_TERMS: + return GlossaryTermImg; + case DetailPageWidgetKeys.TABLE_SCHEMA: + return SchemaImg; + case DetailPageWidgetKeys.TABLE_CONSTRAINTS: + return TableConstraints; default: { return ''; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index fe60b6a98014..2580c20932b3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -24,8 +24,10 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { Constraint, + ConstraintType, DatabaseServiceType, DataType, + RelationshipType, Table, TableType, } from '../generated/entity/data/table'; @@ -130,12 +132,20 @@ class TableClassBase { y: 3, static: false, }, + { + h: 3, + i: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + w: 2, + x: 6, + y: 4, + static: false, + }, { h: 4, i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, w: 2, x: 6, - y: 4, + y: 6, static: false, }, ]; @@ -250,6 +260,20 @@ class TableClassBase { fullyQualifiedName: 'sample_data', deleted: false, }, + tableConstraints: [ + { + constraintType: ConstraintType.ForeignKey, + columns: ['post_id'], + referredColumns: ['mysql_sample.default.posts_db.Posts.post_id'], + relationshipType: RelationshipType.ManyToOne, + }, + { + constraintType: ConstraintType.ForeignKey, + columns: ['user_id'], + referredColumns: ['mysql_sample.default.posts_db.Users.user_id'], + relationshipType: RelationshipType.ManyToOne, + }, + ], serviceType: DatabaseServiceType.BigQuery, tags: [], followers: [], From af65b3d27e88c32d85f69d39317626790374eee3 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:28:58 +0530 Subject: [PATCH 03/63] fix schema tab --- .../components/Database/TableSchemaTab/TableSchemaTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx index ca5487460a11..ad2d6d8d425f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx @@ -35,9 +35,9 @@ import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/Freque import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import { postThread } from '../../../rest/feedsAPI'; -import customizeGlossaryTermPageClassBase from '../../../utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import { getEntityName } from '../../../utils/EntityUtils'; import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import tableClassBase from '../../../utils/TableClassBase'; import { getJoinsFromTableJoins, getTagsWithoutTier, @@ -81,7 +81,7 @@ export const TableSchemaTab = () => { const layout = useMemo(() => { if (!currentPersonaDocStore) { - return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); + return tableClassBase.getDefaultLayout(tab); } const page = currentPersonaDocStore?.data?.pages?.find( @@ -91,7 +91,7 @@ export const TableSchemaTab = () => { if (page) { return page.tabs.find((t: Tab) => t.id === tab)?.layout; } else { - return customizeGlossaryTermPageClassBase.getDefaultWidgetForTab(tab); + return tableClassBase.getDefaultLayout(tab); } }, [currentPersonaDocStore, tab]); const { From fb1a1d90608235a4fa3726410be2f1c6df387563 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:48:58 +0530 Subject: [PATCH 04/63] fix issue around save layout across tabs --- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 55 ++++++++++++------- .../utils/CustomizePage/CustomizePageUtils.ts | 4 +- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx index c29fd8e03ec0..06045ceb4de2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -55,9 +55,9 @@ type TargetKey = React.MouseEvent | React.KeyboardEvent | string; export const CustomizeTabWidget = () => { const { currentPage, currentPageType, updateCurrentPage } = useCustomizeStore(); - const [items, setItems] = useState( - currentPage?.tabs ?? getDefaultTabs(currentPageType as PageType) - ); + const items = useMemo(() => { + return currentPage?.tabs ?? getDefaultTabs(currentPageType as PageType); + }, [currentPage, currentPageType]); const [activeKey, setActiveKey] = useState( (items[0]?.id as EntityTabs) ?? null ); @@ -81,7 +81,15 @@ export const CustomizeTabWidget = () => { const onChange = (tabKey: string) => { const key = tabKey as EntityTabs; setActiveKey(key); - const newTab = items.find((item) => item.id === key); + const newTab = currentPage?.tabs?.find((item) => item.id === key); + + // Save current tab layout before changing + updateCurrentPage({ + ...currentPage, + tabs: items, + } as Page); + + // Update tabLayout with new tab selection setTabLayouts( getLayoutWithEmptyWidgetPlaceholder( isEmpty(newTab?.layout) @@ -95,15 +103,20 @@ export const CustomizeTabWidget = () => { const add = () => { const newActiveKey = uniqueId(`newTab`); - setItems((items) => [ - ...items, - { - name: 'New Tab', - layout: [], - id: newActiveKey, - editable: true, - } as Tab, - ]); + + updateCurrentPage({ + ...currentPage, + tabs: [ + ...items, + { + name: 'New Tab', + layout: [], + id: newActiveKey, + editable: true, + } as Tab, + ], + } as Page); + onChange(newActiveKey); }; @@ -123,7 +136,12 @@ export const CustomizeTabWidget = () => { newActiveKey = newPanes[0].id as EntityTabs; } } - setItems(newPanes); + + updateCurrentPage({ + ...currentPage, + tabs: newPanes, + } as Page); + onChange(newActiveKey ?? EntityTabs.OVERVIEW); }; @@ -147,7 +165,6 @@ export const CustomizeTabWidget = () => { const newItems = items.map((item) => item.id === editableItem.id ? editableItem : item ); - setItems(newItems); updateCurrentPage({ ...currentPage, tabs: newItems, @@ -232,7 +249,10 @@ export const CustomizeTabWidget = () => { const newItems = newOrder.map( (key) => items.find((item) => item.id === key) as Tab ); - setItems(newItems); + updateCurrentPage({ + ...currentPage, + tabs: newItems, + } as Page); updateCurrentPage({ ...currentPage, @@ -240,9 +260,6 @@ export const CustomizeTabWidget = () => { } as Page); }; - // eslint-disable-next-line no-console - console.log('widgets', tabLayouts, currentPage); - // call the hook to set the direction of the grid layout useGridLayoutDirection(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 16d5db2fc205..ed7c4e4c0204 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -13,7 +13,7 @@ import { TabsProps } from 'antd'; import { CommonWidgetType } from '../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../enums/entity.enum'; -import { PageType } from '../../generated/system/ui/page'; +import { PageType, Tab } from '../../generated/system/ui/page'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import i18n from '../i18next/LocalUtil'; @@ -145,7 +145,7 @@ export const getTableDefaultTabs = () => { return tabs; }; -export const getDefaultTabs = (pageType?: string) => { +export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: return getGlossaryTermDefaultTabs(); From 1195d3c67a832baa09ca191eba8a2dc0e73a4670 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:27:39 +0530 Subject: [PATCH 05/63] fix unit tests --- .../ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx index 63f0337fbaa7..bf428e9645bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ import { TabsProps } from 'antd'; -import { get, isUndefined, uniqueId } from 'lodash'; +import { isUndefined, uniqueId } from 'lodash'; import React from 'react'; import EmptyWidgetPlaceholder from '../../components/MyData/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder'; import { SIZE } from '../../enums/common.enum'; @@ -168,12 +168,14 @@ export const getAddWidgetHandler = }; export const getGlossaryTermDetailTabs = ( - defaultTabs: TabsProps['items'], + defaultTabs: Array< + NonNullable[number] & { isHidden?: boolean } + >, customizedTabs?: Tab[], defaultTabId: EntityTabs = EntityTabs.OVERVIEW ) => { if (!customizedTabs) { - return defaultTabs; + return defaultTabs.filter((data) => !data.isHidden); } const overviewTab = defaultTabs?.find((t) => t.key === defaultTabId); @@ -190,7 +192,7 @@ export const getGlossaryTermDetailTabs = ( ); }) ?? defaultTabs; - return newTabs.filter((data) => !get(data, 'isHidden', false)); + return newTabs.filter((data) => !data.isHidden); }; export const getTabLabelMap = (tabs?: Tab[]): Record => { From b4215b9e98e051baea82113e4a577a3ff953d26a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:57:45 +0530 Subject: [PATCH 06/63] fix tab label with customized one --- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 8 ++- .../resources/ui/src/utils/TableClassBase.ts | 1 + .../resources/ui/src/utils/TableUtils.tsx | 59 +++++++++++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 3381ef8c040a..f515b61b6231 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -86,7 +86,10 @@ import { defaultFields } from '../../utils/DatasetDetailsUtils'; import EntityLink from '../../utils/EntityLink'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; -import { getGlossaryTermDetailTabs } from '../../utils/GlossaryTerm/GlossaryTermUtil'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import tableClassBase from '../../utils/TableClassBase'; import { @@ -484,7 +487,7 @@ const TableDetailsPageV1: React.FC = () => { const schemaTab = useMemo(() => , []); const tabs = useMemo(() => { - // const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); const tabs = tableClassBase.getTableDetailPageTabs({ schemaTab, @@ -507,6 +510,7 @@ const TableDetailsPageV1: React.FC = () => { fetchTableDetails, testCaseSummary, isViewTableType, + labelMap: tabLabelMap, }); return getGlossaryTermDetailTabs( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 2580c20932b3..a61f7f5746e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -57,6 +57,7 @@ export interface TableDetailPageTabProps { fetchTableDetails: () => Promise; onExtensionUpdate: (updatedData: Table) => Promise; handleFeedCount: (data: FeedCounts) => void; + labelMap?: Record; } class TableClassBase { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 960d7a7094d7..e8acaac3887d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -742,10 +742,16 @@ export const getTableDetailPageBaseTabs = ({ fetchTableDetails, testCaseSummary, isViewTableType, + labelMap, }: TableDetailPageTabProps): TabProps[] => { return [ { - label: , + label: ( + + ), key: EntityTabs.SCHEMA, children: schemaTab, }, @@ -755,7 +761,11 @@ export const getTableDetailPageBaseTabs = ({ count={totalFeedCount} id={EntityTabs.ACTIVITY_FEED} isActive={activeTab === EntityTabs.ACTIVITY_FEED} - name={t('label.activity-feed-and-task-plural')} + name={get( + labelMap, + EntityTabs.ACTIVITY_FEED, + t('label.activity-feed-and-task-plural') + )} /> ), key: EntityTabs.ACTIVITY_FEED, @@ -775,7 +785,10 @@ export const getTableDetailPageBaseTabs = ({ }, { label: ( - + ), key: EntityTabs.SAMPLE_DATA, @@ -797,7 +810,11 @@ export const getTableDetailPageBaseTabs = ({ count={queryCount} id={EntityTabs.TABLE_QUERIES} isActive={activeTab === EntityTabs.TABLE_QUERIES} - name={t('label.query-plural')} + name={get( + labelMap, + EntityTabs.TABLE_QUERIES, + t('label.query-plural') + )} /> ), key: EntityTabs.TABLE_QUERIES, @@ -814,7 +831,11 @@ export const getTableDetailPageBaseTabs = ({ label: ( ), key: EntityTabs.PROFILER, @@ -833,7 +854,7 @@ export const getTableDetailPageBaseTabs = ({ label: ( ), key: EntityTabs.INCIDENTS, @@ -850,7 +871,12 @@ export const getTableDetailPageBaseTabs = ({ ), }, { - label: , + label: ( + + ), key: EntityTabs.LINEAGE, children: ( @@ -864,7 +890,12 @@ export const getTableDetailPageBaseTabs = ({ ), }, { - label: , + label: ( + + ), isHidden: !( tableDetails?.dataModel?.sql || tableDetails?.dataModel?.rawSql ), @@ -894,11 +925,13 @@ export const getTableDetailPageBaseTabs = ({ ? EntityTabs.VIEW_DEFINITION : EntityTabs.SCHEMA_DEFINITION } - name={ + name={get( + labelMap, + EntityTabs.VIEW_DEFINITION, isViewTableType ? t('label.view-definition') : t('label.schema-definition') - } + )} /> ), isHidden: isUndefined(tableDetails?.schemaDefinition), @@ -909,7 +942,11 @@ export const getTableDetailPageBaseTabs = ({ label: ( ), key: EntityTabs.CUSTOM_PROPERTIES, From b699cfa14099f3ddd2f02702202f3ee3ad38e875 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:12:13 +0530 Subject: [PATCH 07/63] misc fixes for persona edit --- .../TableSchemaTab/TableSchemaTab.tsx | 5 ++++ .../Persona/CustomizeUI/CustomizeUI.tsx | 24 +++++++++++++++++-- .../PersonaDetailsPage/PersonaDetailsPage.tsx | 2 +- .../SettingsNavigationPage.tsx | 8 +++---- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx index ad2d6d8d425f..ebbffd70e200 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx @@ -123,6 +123,7 @@ export const TableSchemaTab = () => { const { editTagsPermission, + editGlossaryTermsPermission, editDescriptionPermission, editCustomAttributePermission, editAllPermission, @@ -134,6 +135,9 @@ export const TableSchemaTab = () => { editDescriptionPermission: (tablePermissions.EditDescription || tablePermissions.EditAll) && !deleted, + editGlossaryTermsPermission: + (tablePermissions.EditGlossaryTerms || tablePermissions.EditAll) && + !deleted, editCustomAttributePermission: (tablePermissions.EditAll || tablePermissions.EditCustomFields) && !deleted, @@ -274,6 +278,7 @@ export const TableSchemaTab = () => { return ( { const history = useHistory(); + const location = useCustomLocation(); const { fqn: personaFQN } = useFqn(); + const activeCat = useMemo( + () => (location.hash?.replace('#', '') || '').split('.')[1] ?? '', + [location] + ); const [items, setItems] = React.useState(categories); @@ -36,10 +43,23 @@ export const CustomizeUI = () => { if (isEmpty(nestedItems)) { history.push(getCustomizePagePath(personaFQN, category)); } else { - setItems(nestedItems); + history.push({ + hash: location.hash + FQN_SEPARATOR_CHAR + category, + }); } }; + useEffect(() => { + if (!activeCat) { + setItems(categories); + + return; + } + + const nestedItems = getCustomizePageOptions(activeCat); + setItems(nestedItems); + }, [activeCat]); + return ( {items.map((value) => ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx index bdc96d78643e..8b75cd8c7368 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx @@ -56,7 +56,7 @@ export const PersonaDetailsPage = () => { ); const location = useCustomLocation(); const activeKey = useMemo( - () => location.hash?.replace('#', '') || 'users', + () => (location.hash?.replace('#', '') || 'users').split('.')[0], [location] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx index 900c6980c37e..fcb93d7da3c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx @@ -15,7 +15,7 @@ import Icon from '@ant-design/icons'; import { Button, Col, Row, Tree, TreeDataNode, TreeProps } from 'antd'; import { DataNode } from 'antd/lib/tree'; import { AxiosError } from 'axios'; -import { cloneDeep, isNil } from 'lodash'; +import { cloneDeep, isEmpty, isNil } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DeleteIcon } from '../../assets/svg/delete-white.svg'; @@ -57,9 +57,9 @@ export const SettingsNavigationPage = ({ const { t } = useTranslation(); const [saving, setSaving] = useState(false); const [targetKeys, setTargetKeys] = useState( - currentNavigation - ? getNestedKeysFromNavigationItems(currentNavigation) - : getNestedKeys(sidebarOptions) + isEmpty(currentNavigation) + ? getNestedKeys(sidebarOptions) + : getNestedKeysFromNavigationItems(currentNavigation ?? []) ); const treeData = filterAndArrangeTreeByKeys( From 0b64256b250ada51530c8db67385e4af5dbdf38b Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:15:46 +0530 Subject: [PATCH 08/63] update --- .../SynonymsWidget/GenericWidget.tsx | 9 + .../TopicSchema/TopicSchema.interface.tsx | 11 +- .../Topic/TopicSchema/TopicSchema.tsx | 42 ++- .../TopicVersion/TopicVersion.component.tsx | 5 - .../CustomizablePage/CustomizablePage.tsx | 28 ++ .../CustomizeTableDetailPage.tsx | 11 +- .../utils/CustomizePage/CustomizePageUtils.ts | 29 +- .../resources/ui/src/utils/TableClassBase.ts | 16 +- .../resources/ui/src/utils/TopicClassBase.ts | 315 ++++++++++++++++++ .../ui/src/utils/TopicDetailsUtils.ts | 82 ----- .../ui/src/utils/TopicDetailsUtils.tsx | 166 +++++++++ 11 files changed, 591 insertions(+), 123 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index c7bbee69cc14..6d6b310f81d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -39,6 +39,7 @@ import DataProductsContainer from '../../../DataProducts/DataProductsContainer/D import { GenericProvider } from '../../../GenericProvider/GenericProvider'; import TagsViewer from '../../../Tag/TagsViewer/TagsViewer'; import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; +import TopicSchemaFields from '../../../Topic/TopicSchema/TopicSchema'; import GlossaryTermTab from '../../GlossaryTermTab/GlossaryTermTab.component'; import { ModifiedGlossary, useGlossaryStore } from '../../useGlossary.store'; @@ -573,6 +574,14 @@ export const GenericWidget = (props: WidgetCommonProps) => { onUpdate={async () => noop()} /> ); + } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { + return ( + noop()} + /> + ); } return widgetName; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx index 45ef89fd6ea4..ae665073085f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx @@ -14,22 +14,15 @@ import { TableProps } from 'antd'; import { HTMLAttributes, ReactNode } from 'react'; import { ThreadType } from '../../../generated/api/feed/createThread'; -import { Field, Topic } from '../../../generated/entity/data/topic'; +import { Field } from '../../../generated/entity/data/topic'; export interface TopicSchemaFieldsProps extends HTMLAttributes> { - messageSchema: Topic['messageSchema']; - hasDescriptionEditAccess: boolean; - hasTagEditAccess: boolean; - hasGlossaryTermEditAccess: boolean; isReadOnly: boolean; - entityFqn: string; - isVersionView?: boolean; schemaTypePlaceholder?: ReactNode; defaultExpandAllRows?: boolean; showSchemaDisplayTypeSwitch?: boolean; - onUpdate?: (updatedMessageSchema: Topic['messageSchema']) => Promise; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; + onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; } export enum SchemaViewType { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index 0fc6f1927963..facc7ce3771b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -31,9 +31,14 @@ import { useTranslation } from 'react-i18next'; import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants'; import { CSMode } from '../../../enums/codemirror.enum'; import { EntityType } from '../../../enums/entity.enum'; -import { DataTypeTopic, Field } from '../../../generated/entity/data/topic'; +import { + DataTypeTopic, + Field, + Topic, +} from '../../../generated/entity/data/topic'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { useFqn } from '../../../hooks/useFqn'; import { getEntityName } from '../../../utils/EntityUtils'; import { getAllTags, @@ -52,6 +57,7 @@ import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import { SchemaViewType, @@ -59,16 +65,9 @@ import { } from './TopicSchema.interface'; const TopicSchemaFields: FC = ({ - messageSchema, className, - hasDescriptionEditAccess, isReadOnly, - onUpdate, - hasTagEditAccess, - hasGlossaryTermEditAccess, - entityFqn, onThreadLinkSelect, - isVersionView = false, schemaTypePlaceholder, }) => { const { theme } = useApplicationStore(); @@ -78,6 +77,29 @@ const TopicSchemaFields: FC = ({ const [viewType, setViewType] = useState( SchemaViewType.FIELDS ); + const { fqn: entityFqn } = useFqn(); + const { + data: topicDetails, + isVersionView, + permissions, + onUpdate, + } = useGenericContext(); + const { messageSchema } = topicDetails; + + const { + hasDescriptionEditAccess, + hasTagEditAccess, + hasGlossaryTermEditAccess, + } = useMemo( + () => ({ + hasDescriptionEditAccess: + permissions.EditAll ?? permissions.EditDescription, + hasTagEditAccess: permissions.EditAll ?? permissions.EditTags, + hasGlossaryTermEditAccess: + permissions.EditAll ?? permissions.EditGlossaryTerms, + }), + [permissions] + ); const schemaAllRowKeys = useMemo(() => { return getAllRowKeysByKeyName( @@ -97,7 +119,7 @@ const TopicSchemaFields: FC = ({ selectedTags, schema?.schemaFields ); - await onUpdate(schema); + await onUpdate({ ...topicDetails, messageSchema: schema }); } }; @@ -109,7 +131,7 @@ const TopicSchemaFields: FC = ({ updatedDescription, schema?.schemaFields ); - await onUpdate(schema); + await onUpdate({ ...topicDetails, messageSchema: schema }); setEditFieldDescription(undefined); } else { setEditFieldDescription(undefined); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx index c0ab9fde730a..b02d0ee8038e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx @@ -156,11 +156,6 @@ const TopicVersion: FC = ({

+ {editChart && ( + + )} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index 0bd223649700..f59f36d32797 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -11,72 +11,49 @@ * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; -import Icon from '@ant-design/icons/lib/components/Icon'; -import { Col, Row, Table, Tabs, Typography } from 'antd'; -import { ColumnsType } from 'antd/lib/table'; +import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { compare } from 'fast-json-patch'; -import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; -import { EntityTags, TagFilterOptions } from 'Models'; +import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg'; -import { - DATA_ASSET_ICON_DIMENSION, - getEntityDetailsPath, -} from '../../../constants/constants'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; +import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { ThreadType } from '../../../generated/entity/feed/thread'; -import { TagSource } from '../../../generated/type/schema'; +import { Page } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/uiCustomization'; import { TagLabel } from '../../../generated/type/tagLabel'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { restoreDashboard } from '../../../rest/dashboardAPI'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; -import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; -import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import dashboardDetailsClassBase from '../../../utils/DashboardDetailsClassBase'; +import { getEntityName } from '../../../utils/EntityUtils'; import { - getAllTags, - searchTagInData, -} from '../../../utils/TableTags/TableTags.utils'; + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; -import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; -import TableDescription from '../../Database/TableDescription/TableDescription.component'; -import TableTags from '../../Database/TableTags/TableTags.component'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../Lineage/Lineage.component'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; -import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../SearchedData/SearchedData.interface'; -import { - ChartsPermissions, - ChartType, - DashboardDetailsProps, -} from './DashboardDetails.interface'; +import { DashboardDetailsProps } from './DashboardDetails.interface'; const DashboardDetails = ({ updateDashboardDetailsState, @@ -85,8 +62,6 @@ const DashboardDetails = ({ fetchDashboard, followDashboardHandler, unFollowDashboardHandler, - chartDescriptionUpdateHandler, - chartTagUpdateHandler, versionHandler, createThread, onUpdateVote, @@ -94,19 +69,16 @@ const DashboardDetails = ({ handleToggleDelete, }: DashboardDetailsProps) => { const { t } = useTranslation(); - const { currentUser, theme } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const history = useHistory(); const { tab: activeTab = EntityTabs.DETAILS } = useParams<{ tab: EntityTabs }>(); - + const [customizedPage, setCustomizedPage] = useState(null); const { fqn: decodedDashboardFQN } = useFqn(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const [isEdit, setIsEdit] = useState(false); - const [editChart, setEditChart] = useState<{ - chart: ChartType; - index: number; - }>(); + const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -118,9 +90,6 @@ const DashboardDetails = ({ const [dashboardPermissions, setDashboardPermissions] = useState( DEFAULT_ENTITY_PERMISSION ); - const [chartsPermissionsArray, setChartsPermissionsArray] = useState< - Array - >([]); const { owners, @@ -171,19 +140,6 @@ const DashboardDetails = ({ } }, [dashboardDetails.id]); - const fetchChartPermissions = useCallback(async (id: string) => { - try { - const chartPermission = await getEntityPermission( - ResourceEntity.CHART, - id - ); - - return chartPermission; - } catch (error) { - return DEFAULT_ENTITY_PERMISSION; - } - }, []); - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -195,38 +151,6 @@ const DashboardDetails = ({ getEntityFeedCount(); }, [decodedDashboardFQN]); - const getAllChartsPermissions = useCallback( - async (charts: ChartType[]) => { - const permissionsArray: Array = []; - try { - await Promise.all( - charts.map(async (chart) => { - const chartPermissions = await fetchChartPermissions(chart.id); - permissionsArray.push({ - id: chart.id, - permissions: chartPermissions, - }); - }) - ); - - setChartsPermissionsArray(permissionsArray); - } catch { - showErrorToast( - t('server.fetch-entity-permissions-error', { - entity: t('label.chart'), - }) - ); - } - }, - [dashboardDetails] - ); - - useEffect(() => { - if (charts) { - getAllChartsPermissions(charts); - } - }, [charts]); - const handleTabChange = (activeKey: string) => { if (activeKey !== activeTab) { history.push( @@ -325,62 +249,6 @@ const DashboardDetails = ({ ? await unFollowDashboardHandler() : await followDashboardHandler(); }; - const handleUpdateChart = (chart: ChartType, index: number) => { - setEditChart({ chart, index }); - }; - - const closeEditChartModal = (): void => { - setEditChart(undefined); - }; - const onChartUpdate = async (chartDescription: string) => { - if (editChart) { - const updatedChart = { - ...editChart.chart, - description: chartDescription, - }; - const jsonPatch = compare(charts[editChart.index], updatedChart); - - try { - await chartDescriptionUpdateHandler( - editChart.index, - editChart.chart.id, - jsonPatch - ); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setEditChart(undefined); - } - } else { - setEditChart(undefined); - } - }; - - const handleChartTagSelection = async ( - selectedTags: Array, - editColumnTag: ChartType - ) => { - if (selectedTags && editColumnTag) { - const prevTags = editColumnTag.tags?.filter((tag) => - selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN) - ); - const newTags = createTagObject( - selectedTags.filter( - (selectedTag) => - !editColumnTag.tags?.some( - (tag) => tag.tagFQN === selectedTag.tagFQN - ) - ) - ); - - const updatedChart = { - ...editColumnTag, - tags: [...(prevTags as TagLabel[]), ...newTags], - }; - const jsonPatch = compare(editColumnTag, updatedChart); - await chartTagUpdateHandler(editColumnTag.id, jsonPatch); - } - }; const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { setThreadLink(link); @@ -393,28 +261,6 @@ const DashboardDetails = ({ setThreadLink(''); }; - const hasEditTagAccess = (record: ChartType) => { - const permissionsObject = chartsPermissionsArray?.find( - (chart) => chart.id === record.id - )?.permissions; - - return ( - !isUndefined(permissionsObject) && - (permissionsObject.EditTags || permissionsObject.EditAll) - ); - }; - - const hasEditGlossaryTermAccess = (record: ChartType) => { - const permissionsObject = chartsPermissionsArray?.find( - (chart) => chart.id === record.id - )?.permissions; - - return ( - !isUndefined(permissionsObject) && - (permissionsObject.EditGlossaryTerms || permissionsObject.EditAll) - ); - }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); @@ -431,163 +277,6 @@ const DashboardDetails = ({ [] ); - const tagFilter = useMemo(() => { - const tags = getAllTags(charts); - - return groupBy(uniqBy(tags, 'value'), (tag) => tag.source) as Record< - TagSource, - TagFilterOptions[] - >; - }, [charts]); - - const tableColumn: ColumnsType = useMemo( - () => [ - { - title: t('label.chart-entity', { - entity: t('label.name'), - }), - dataIndex: 'chartName', - key: 'chartName', - width: 220, - fixed: 'left', - sorter: getColumnSorter('name'), - render: (_, record) => { - const chartName = getEntityName(record); - - return record.sourceUrl ? ( -
- - {chartName} - - - -
- ) : ( - {chartName} - ); - }, - }, - { - title: t('label.chart-entity', { - entity: t('label.type'), - }), - dataIndex: 'chartType', - key: 'chartType', - width: 120, - }, - { - title: t('label.description'), - dataIndex: 'description', - key: 'description', - width: 350, - render: (_, record, index) => { - const permissionsObject = chartsPermissionsArray?.find( - (chart) => chart.id === record.id - )?.permissions; - - const editDescriptionPermissions = - !isUndefined(permissionsObject) && - (permissionsObject.EditDescription || permissionsObject.EditAll); - - return ( - handleUpdateChart(record, index)} - onThreadLinkSelect={onThreadLinkSelect} - /> - ); - }, - }, - { - title: t('label.tag-plural'), - dataIndex: 'tags', - key: 'tags', - accessor: 'tags', - width: 300, - filterIcon: (filtered) => ( - - ), - render: (tags: TagLabel[], record: ChartType, index: number) => { - return ( - - entityFqn={decodedDashboardFQN} - entityType={EntityType.DASHBOARD} - handleTagSelection={handleChartTagSelection} - hasTagEditAccess={hasEditTagAccess(record)} - index={index} - isReadOnly={deleted} - record={record} - tags={tags} - type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} - /> - ); - }, - filters: tagFilter.Classification, - filterDropdown: ColumnFilter, - onFilter: searchTagInData, - }, - { - title: t('label.glossary-term-plural'), - dataIndex: 'tags', - key: 'glossary', - accessor: 'tags', - width: 300, - filterIcon: (filtered) => ( - - ), - render: (tags: TagLabel[], record: ChartType, index: number) => ( - - entityFqn={decodedDashboardFQN} - entityType={EntityType.DASHBOARD} - handleTagSelection={handleChartTagSelection} - hasTagEditAccess={hasEditGlossaryTermAccess(record)} - index={index} - isReadOnly={deleted} - record={record} - tags={tags} - type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} - /> - ), - filters: tagFilter.Glossary, - filterDropdown: ColumnFilter, - onFilter: searchTagInData, - }, - ], - [ - deleted, - chartsPermissionsArray, - onThreadLinkSelect, - hasEditTagAccess, - handleUpdateChart, - handleChartTagSelection, - charts, - ] - ); - const { editTagsPermission, editGlossaryTermsPermission, @@ -622,172 +311,80 @@ const DashboardDetails = ({ [dashboardPermissions, deleted] ); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.DETAILS, - children: ( - -
- - - - {isEmpty(charts) ? ( - - ) : ( -
- )} - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={dashboardDetails} - dataProducts={dashboardDetails?.dataProducts ?? []} - domain={dashboardDetails?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityFQN={decodedDashboardFQN} - entityId={dashboardDetails.id} - entityType={EntityType.DASHBOARD} - selectedTags={dashboardTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: dashboardDetails && ( -
- - entityDetails={dashboardDetails} - entityType={EntityType.DASHBOARD} - handleExtensionUpdate={onExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ - feedCount.totalCount, - activeTab, - isEdit, - tableColumn, - dashboardDetails, - charts, - deleted, + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = dashboardDetailsClassBase.getDashboardDetailPageTabs({ entityName, - dashboardTags, - onCancel, - handleFeedCount, - onDescriptionEdit, - onDescriptionUpdate, - onThreadLinkSelect, - handleTagSelection, + editDescriptionPermission, editTagsPermission, editGlossaryTermsPermission, editLineagePermission, - editDescriptionPermission, editCustomAttributePermission, - editAllPermission, viewAllPermission, - ] - ); + dashboardDetails, + charts: charts ?? [], + deleted: deleted ?? false, + dashboardTags, + handleFeedCount, + onThreadLinkSelect, + handleTagSelection, + onDescriptionUpdate, + onExtensionUpdate, + feedCount, + activeTab, + getEntityFeedCount, + fetchDashboard, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.DETAILS + ); + }, [ + feedCount.totalCount, + activeTab, + isEdit, + dashboardDetails, + charts, + deleted, + entityName, + dashboardTags, + onCancel, + handleFeedCount, + onDescriptionEdit, + onDescriptionUpdate, + onThreadLinkSelect, + handleTagSelection, + editTagsPermission, + editGlossaryTermsPermission, + editLineagePermission, + editDescriptionPermission, + editCustomAttributePermission, + editAllPermission, + viewAllPermission, + onExtensionUpdate, + ]); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Dashboard) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); return ( -
- - + + data={dashboardDetails} + permissions={dashboardPermissions} + type={EntityType.DASHBOARD} + onUpdate={onDashboardUpdate}> + + + + - {editChart && ( - - )} <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts index 31c42ebb5465..721219b27935 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts @@ -11,11 +11,11 @@ * limitations under the License. */ -import { Operation } from 'fast-json-patch'; import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { CreateThread } from '../../../generated/api/feed/createThread'; import { Chart } from '../../../generated/entity/data/chart'; import { Dashboard } from '../../../generated/entity/data/dashboard'; +import { EntityReference } from '../../../generated/type/entityReference'; import { DataAssetWithDomains } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; @@ -29,21 +29,12 @@ export interface ChartsPermissions { } export interface DashboardDetailsProps { updateDashboardDetailsState?: (data: DataAssetWithDomains) => void; - charts: Array; + charts: Array; dashboardDetails: Dashboard; fetchDashboard: () => void; createThread: (data: CreateThread) => Promise; followDashboardHandler: () => Promise; unFollowDashboardHandler: () => Promise; - chartDescriptionUpdateHandler: ( - index: number, - chartId: string, - patch: Array - ) => Promise; - chartTagUpdateHandler: ( - chartId: string, - patch: Array - ) => Promise; versionHandler: () => void; onDashboardUpdate: ( updatedDashboard: Dashboard, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx index a87e3f79ea4a..e0c2f542021f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx @@ -19,8 +19,7 @@ import { } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EntityTabs } from '../../../enums/entity.enum'; -import { ChartType } from '../../../generated/entity/data/chart'; +import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { mockGlossaryList } from '../../../mocks/Glossary.mock'; import { mockTagList } from '../../../mocks/Tags.mock'; @@ -49,20 +48,16 @@ const mockUserTeam = [ const dashboardDetailsProps: DashboardDetailsProps = { charts: [ { - sourceUrl: 'http://localhost', - chartType: ChartType.Area, displayName: 'Test chart', id: '1', deleted: false, name: '', - service: { id: '', type: '' }, + type: EntityType.CHART, }, ], dashboardDetails: {} as Dashboard, followDashboardHandler: jest.fn(), unFollowDashboardHandler: jest.fn(), - chartDescriptionUpdateHandler: jest.fn(), - chartTagUpdateHandler: jest.fn(), onDashboardUpdate: jest.fn(), versionHandler: jest.fn(), createThread: jest.fn(), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 8bd7f3a4c9bc..0854437b5651 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -31,7 +31,7 @@ import { useFqn } from '../../../../hooks/useFqn'; import { FeedCounts } from '../../../../interface/feed.interface'; import { restoreDataModel } from '../../../../rest/dataModelsAPI'; import { getFeedCounts } from '../../../../utils/CommonUtils'; -import { getDashboardDataModelDetailPageTabs } from '../../../../utils/DashboardDetailsUtils'; +import { getDashboardDataModelDetailPageTabs } from '../../../../utils/DashboardDataModelUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getTagsWithoutTier } from '../../../../utils/TableUtils'; import { createTagObject } from '../../../../utils/TagsUtils'; @@ -72,7 +72,6 @@ const DataModelDetails = ({ const { fqn: decodedDataModelFQN } = useFqn(); - const [isEditDescription, setIsEditDescription] = useState(false); const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -206,11 +205,6 @@ const DataModelDetails = ({ }; }, [dataModelPermissions, deleted]); - const onDescriptionUpdate = async (value: string) => { - await handleUpdateDescription(value); - - setIsEditDescription(false); - }; const handelExtensionUpdate = useCallback( async (updatedDataModel: DashboardDataModel) => { await onUpdateDataModel( @@ -240,12 +234,9 @@ const DataModelDetails = ({ entityType={EntityType.DASHBOARD_DATA_MODEL} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(dataModelData.columns)} - isEdit={isEditDescription} owner={owners} showActions={!deleted} - onCancel={() => setIsEditDescription(false)} - onDescriptionEdit={() => setIsEditDescription(true)} - onDescriptionUpdate={onDescriptionUpdate} + onDescriptionUpdate={handleUpdateDescription} onThreadLinkSelect={onThreadLinkSelect} /> { const { currentUser } = useApplicationStore(); const USER_ID = currentUser?.id ?? ''; @@ -330,7 +331,7 @@ export const DataAssetsHeader = ({ }; useEffect(() => { - if (dataAsset.fullyQualifiedName && !isTourPage) { + if (dataAsset.fullyQualifiedName && !isTourPage && !isCustomizedView) { fetchActiveAnnouncement(); fetchDQFailureCount(); } @@ -338,7 +339,7 @@ export const DataAssetsHeader = ({ const asset = dataAsset as Container; fetchContainerParent(asset.parent?.fullyQualifiedName ?? ''); } - }, [dataAsset.fullyQualifiedName, isTourPage]); + }, [dataAsset.fullyQualifiedName, isTourPage, isCustomizedView]); const { extraInfo, breadcrumbs }: DataAssetHeaderInfo = useMemo( () => diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts index ee816b9ae9e0..146d00123cf0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts @@ -116,6 +116,7 @@ export type DataAssetsHeaderProps = { onUpdateRetentionPeriod?: (value: string) => Promise; extraDropdownContent?: ManageButtonProps['extraDropdownContent']; onMetricUpdate?: (updatedData: Metric, key: keyof Metric) => Promise; + isCustomizedView?: boolean; } & ( | DataAssetTable | DataAssetTopic diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx index 379d942f481a..690b1384e38e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx @@ -52,7 +52,6 @@ const TestCaseResultTab = () => { } = useTestCaseStore(); const additionalComponent = testCaseResultTabClassBase.getAdditionalComponents(); - const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); const [isParameterEdit, setIsParameterEdit] = useState(false); const { permissions } = usePermissionProvider(); const hasEditPermission = useMemo(() => { @@ -106,8 +105,6 @@ const TestCaseResultTab = () => { ); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsDescriptionEdit(false); } } } @@ -169,10 +166,7 @@ const TestCaseResultTab = () => { description={testCaseData?.description} entityType={EntityType.TEST_CASE} hasEditAccess={hasEditPermission} - isEdit={isDescriptionEdit} showCommentsIcon={false} - onCancel={() => setIsDescriptionEdit(false)} - onDescriptionEdit={() => setIsDescriptionEdit(true)} onDescriptionUpdate={handleDescriptionChange} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx index d9b58e217783..a7aaa93b3c74 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx @@ -13,7 +13,7 @@ import Icon from '@ant-design/icons'; import { Button, Col, Drawer, Row, Space, Tooltip, Typography } from 'antd'; -import React, { useState } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; @@ -40,8 +40,6 @@ const TableQueryRightPanel = ({ const { t } = useTranslation(); const { EditAll, EditDescription, EditOwners, EditTags } = permission; - const [isEditDescription, setIsEditDescription] = useState(false); - const handleUpdateOwner = async (owners: Query['owners']) => { const updatedData = { ...query, @@ -56,7 +54,6 @@ const TableQueryRightPanel = ({ description, }; await onQueryUpdate(updatedData, 'description'); - setIsEditDescription(false); }; const handleTagSelection = async (tags?: TagLabel[]) => { if (tags) { @@ -121,10 +118,7 @@ const TableQueryRightPanel = ({ description={query?.description || ''} entityType={EntityType.QUERY} hasEditAccess={EditDescription || EditAll} - isEdit={isEditDescription} showCommentsIcon={false} - onCancel={() => setIsEditDescription(false)} - onDescriptionEdit={() => setIsEditDescription(true)} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx index ebbffd70e200..1bd3514bfab6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx @@ -13,7 +13,7 @@ import { AxiosError } from 'axios'; import { isEmpty, isEqual, noop } from 'lodash'; import { EntityTags } from 'Models'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import RGL, { WidthProvider } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; @@ -61,7 +61,6 @@ export const TableSchemaTab = () => { const { currentPersonaDocStore } = useCustomizeStore(); const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: tableFqn } = useFqn(); - const [isEdit, setIsEdit] = useState(false); const [threadLink, setThreadLink] = useState(''); const [threadType, setThreadType] = useState( ThreadType.Conversation @@ -159,9 +158,6 @@ export const TableSchemaTab = () => { [tablePermissions, deleted] ); - const onDescriptionEdit = useCallback(() => setIsEdit(true), []); - const onCancel = useCallback(() => setIsEdit(false), []); - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { setThreadLink(link); if (threadType) { @@ -192,16 +188,11 @@ export const TableSchemaTab = () => { entityType={EntityType.TABLE} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(columns)} - isEdit={isEdit} owner={owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={async (value) => { if (value !== description) { await onUpdate({ ...tableDetails, description: value }); - } else { - onCancel(); } }} onThreadLinkSelect={onThreadLinkSelect} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx index a61d3c64c339..1e55f3828096 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx @@ -59,8 +59,6 @@ const DocumentationTab = ({ permissions, }: DocumentationTabProps) => { const { t } = useTranslation(); - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); const [editDomainType, setEditDomainType] = useState(false); const resourceType = type === DocumentationEntity.DOMAIN @@ -122,9 +120,6 @@ const DocumentationTab = ({ description: updatedHTML, }; onUpdate(updatedTableDetails); - setIsDescriptionEditable(false); - } else { - setIsDescriptionEditable(false); } }; @@ -179,10 +174,7 @@ const DocumentationTab = ({ entityName={getEntityName(domain)} entityType={EntityType.DOMAIN} hasEditAccess={editDescriptionPermission} - isEdit={isDescriptionEditable} showCommentsIcon={false} - onCancel={() => setIsDescriptionEditable(false)} - onDescriptionEdit={() => setIsDescriptionEditable(true)} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EdgeInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EdgeInfoDrawer.component.tsx index 83799b3bcf46..aaebb5c5b5f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EdgeInfoDrawer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EdgeInfoDrawer.component.tsx @@ -53,8 +53,6 @@ const EdgeInfoDrawer = ({ const [edgeData, setEdgeData] = useState(); const [mysqlQuery, setMysqlQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); const [showSqlQueryModal, setShowSqlQueryModal] = useState(false); const { t } = useTranslation(); @@ -162,7 +160,6 @@ const EdgeInfoDrawer = ({ }; await onEdgeDetailsUpdate?.(updatedEdgeDetails); } - setIsDescriptionEditable(false); }, [edgeDescription, edgeEntity, edge] ); @@ -244,10 +241,7 @@ const EdgeInfoDrawer = ({ entityName="Edge" entityType={EntityType.LINEAGE_EDGE} hasEditAccess={hasEditAccess} - isEdit={isDescriptionEditable} showCommentsIcon={false} - onCancel={() => setIsDescriptionEditable(false)} - onDescriptionEdit={() => setIsDescriptionEditable(true)} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx index 06045ceb4de2..c0abcfaba3ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -57,7 +57,7 @@ export const CustomizeTabWidget = () => { useCustomizeStore(); const items = useMemo(() => { return currentPage?.tabs ?? getDefaultTabs(currentPageType as PageType); - }, [currentPage, currentPageType]); + }, [currentPage, currentPageType, currentPage?.tabs]); const [activeKey, setActiveKey] = useState( (items[0]?.id as EntityTabs) ?? null ); @@ -78,16 +78,18 @@ export const CustomizeTabWidget = () => { const [isWidgetModalOpen, setIsWidgetModalOpen] = useState(false); const [placeholderWidgetKey, setPlaceholderWidgetKey] = useState(''); - const onChange = (tabKey: string) => { + const onChange = (tabKey: string, updatePage = true) => { const key = tabKey as EntityTabs; setActiveKey(key); const newTab = currentPage?.tabs?.find((item) => item.id === key); // Save current tab layout before changing - updateCurrentPage({ - ...currentPage, - tabs: items, - } as Page); + if (updatePage) { + updateCurrentPage({ + ...currentPage, + tabs: items, + } as Page); + } // Update tabLayout with new tab selection setTabLayouts( @@ -117,7 +119,7 @@ export const CustomizeTabWidget = () => { ], } as Page); - onChange(newActiveKey); + onChange(newActiveKey, false); }; const remove = (targetKey: TargetKey) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 70cd35b11142..8de29cbba8d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -19,6 +19,7 @@ import { GlossaryTermDetailPageWidgetKeys, } from '../../../../enums/CustomizeDetailPage.enum'; import { EntityType } from '../../../../enums/entity.enum'; +import { Container } from '../../../../generated/entity/data/container'; import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { DataType, Table } from '../../../../generated/entity/data/table'; import { Topic } from '../../../../generated/entity/data/topic'; @@ -27,6 +28,7 @@ import { TagSource } from '../../../../generated/type/tagLabel'; import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import TableConstraints from '../../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; +import containerDetailsClassBase from '../../../../utils/ContainerDetailsClassBase'; import dashboardDataModelClassBase from '../../../../utils/DashboardDataModelBase'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; @@ -38,6 +40,8 @@ import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import RichTextEditorPreviewerV1 from '../../../common/RichTextEditor/RichTextEditorPreviewerV1'; import TagButton from '../../../common/TagButton/TagButton.component'; +import ContainerChildren from '../../../Container/ContainerChildren/ContainerChildren'; +import { DashboardChartTable } from '../../../Dashboard/DashboardChartTable/DashboardChartTable'; import ModelTab from '../../../Dashboard/DataModel/DataModels/ModelTab/ModelTab.component'; import SchemaTable from '../../../Database/SchemaTable/SchemaTable.component'; import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; @@ -599,6 +603,24 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); + } else if ( + props.widgetKey.startsWith(DetailPageWidgetKeys.CONTAINER_CHILDREN) + ) { + return ( + + data={containerDetailsClassBase.getDummyData()} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.CONTAINER} + onUpdate={async () => noop()}> + + + ); + } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { + return ; } return widgetName; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 4c9d7b630071..337cb6f0b662 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -83,8 +83,6 @@ const GlossaryDetails = ({ FEED_COUNT_INITIAL_DATA ); const { selectedPersona } = useApplicationStore(); - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); const { currentPersonaDocStore } = useCustomizeStore(); // Since we are rendering this component for all customized tabs we need tab ID to get layout form store const { tab: activeTab = EntityTabs.TERMS } = @@ -133,9 +131,6 @@ const GlossaryDetails = ({ description: updatedHTML, }; await handleGlossaryUpdate(updatedGlossaryDetails); - setIsDescriptionEditable(false); - } else { - setIsDescriptionEditable(false); } }; @@ -218,11 +213,8 @@ const GlossaryDetails = ({ entityType={EntityType.GLOSSARY} hasEditAccess={permissions.EditDescription || permissions.EditAll} isDescriptionExpanded={isEmpty(glossary.children)} - isEdit={isDescriptionEditable} owner={glossary?.owners} showActions={!glossary.deleted} - onCancel={() => setIsDescriptionEditable(false)} - onDescriptionEdit={() => setIsDescriptionEditable(true)} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -287,7 +279,7 @@ const GlossaryDetails = ({ {widgets} ); - }, [permissions, glossary, termsLoading, isDescriptionEditable, widgets]); + }, [permissions, glossary, termsLoading, widgets]); const tabs = useMemo(() => { const tabLabelMap = getTabLabelMap(customizedPage?.tabs); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index 6bff51c34d78..0cb4967c371a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -58,8 +58,6 @@ const GlossaryOverviewTab = ({ editCustomAttributePermission, onExtensionUpdate, }: Props) => { - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); const [tagsUpdating, setTagsUpdating] = useState(); const { currentPersonaDocStore } = useCustomizeStore(); // Since we are rendering this component for all customized tabs we need tab ID to get layout form store @@ -97,9 +95,6 @@ const GlossaryOverviewTab = ({ description: updatedHTML, }; onUpdate(updatedTableDetails); - setIsDescriptionEditable(false); - } else { - setIsDescriptionEditable(false); } }; @@ -155,18 +150,14 @@ const GlossaryOverviewTab = ({ entityName={getEntityName(selectedData)} entityType={EntityType.GLOSSARY_TERM} hasEditAccess={permissions.EditDescription || permissions.EditAll} - isEdit={isDescriptionEditable} owner={selectedData?.owners} showActions={!selectedData.deleted} - onCancel={() => setIsDescriptionEditable(false)} - onDescriptionEdit={() => setIsDescriptionEditable(true)} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> ); }, [ glossaryDescription, - isDescriptionEditable, selectedData, onDescriptionUpdate, onThreadLinkSelect, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index 95b2dcde5b68..c898ee93c909 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -78,7 +78,6 @@ const MetricDetails: React.FC = ({ useParams<{ tab: EntityTabs }>(); const { fqn: decodedMetricFqn } = useFqn(); const history = useHistory(); - const [isEdit, setIsEdit] = useState(false); const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -167,10 +166,6 @@ const MetricDetails: React.FC = ({ } }; - const onDescriptionEdit = (): void => setIsEdit(true); - - const onCancel = () => setIsEdit(false); - const onDescriptionUpdate = async (updatedHTML: string) => { if (description !== updatedHTML) { const updatedMetricDetails = { @@ -181,11 +176,7 @@ const MetricDetails: React.FC = ({ await onMetricUpdate(updatedMetricDetails, 'description'); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } - } else { - setIsEdit(false); } }; const onOwnerUpdate = useCallback( @@ -304,11 +295,8 @@ const MetricDetails: React.FC = ({ entityName={entityName} entityType={EntityType.METRIC} hasEditAccess={editDescriptionPermission} - isEdit={isEdit} owner={metricDetails.owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -431,9 +419,7 @@ const MetricDetails: React.FC = ({ ), }, ], - [ - isEdit, activeTab, feedCount.totalCount, metricTags, @@ -442,8 +428,6 @@ const MetricDetails: React.FC = ({ decodedMetricFqn, fetchMetricDetails, deleted, - onCancel, - onDescriptionEdit, handleFeedCount, onExtensionUpdate, onThreadLinkSelect, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index fedce78f5e5d..e90f87ae3fca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -83,7 +83,6 @@ const MlModelDetail: FC = ({ const { fqn: decodedMlModelFqn } = useFqn(); - const [isEdit, setIsEdit] = useState(false); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -167,10 +166,6 @@ const MlModelDetail: FC = ({ } }; - const onDescriptionEdit = () => setIsEdit(true); - - const onCancel = () => setIsEdit(false); - const onDescriptionUpdate = async (updatedHTML: string) => { if (mlModelDetail.description !== updatedHTML) { const updatedMlModelDetails = { @@ -178,9 +173,6 @@ const MlModelDetail: FC = ({ description: updatedHTML, }; await descriptionUpdateHandler(updatedMlModelDetails); - setIsEdit(false); - } else { - setIsEdit(false); } }; @@ -413,11 +405,8 @@ const MlModelDetail: FC = ({ isDescriptionExpanded={isEmpty( mlModelDetail.mlFeatures )} - isEdit={isEdit} owner={mlModelDetail.owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={handleThreadLinkSelect} /> @@ -542,16 +531,13 @@ const MlModelDetail: FC = ({ mlModelDetail, mlModelName, mlModelPermissions, - isEdit, getMlHyperParameters, getMlModelStore, - onCancel, handleFeedCount, onExtensionUpdate, onFeaturesUpdate, handleThreadLinkSelect, onDescriptionUpdate, - onDescriptionEdit, deleted, editTagsPermission, editGlossaryTermsPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index 6410b38089d7..f3241357bfd8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -131,8 +131,6 @@ const PipelineDetails = ({ // local state variables - const [isEdit, setIsEdit] = useState(false); - const [editTask, setEditTask] = useState<{ task: Task; index: number; @@ -272,13 +270,6 @@ const PipelineDetails = ({ } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const onCancel = () => { - setIsEdit(false); - }; - const onDescriptionUpdate = async (updatedHTML: string) => { if (description !== updatedHTML) { const updatedPipelineDetails = { @@ -286,9 +277,6 @@ const PipelineDetails = ({ description: updatedHTML, }; await descriptionUpdateHandler(updatedPipelineDetails); - setIsEdit(false); - } else { - setIsEdit(false); } }; @@ -616,11 +604,8 @@ const PipelineDetails = ({ entityType={EntityType.PIPELINE} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(tasksInternal)} - isEdit={isEdit} owner={owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -768,7 +753,6 @@ const PipelineDetails = ({ description, activeTab, feedCount.totalCount, - isEdit, deleted, owners, entityName, @@ -780,8 +764,6 @@ const PipelineDetails = ({ handleFeedCount, handleTagSelection, onExtensionUpdate, - onCancel, - onDescriptionEdit, onDescriptionUpdate, onThreadLinkSelect, editDescriptionPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx index 8c3857e4f0c3..e3c8cb07172c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx @@ -56,7 +56,6 @@ const BotDetails: FC = ({ }) => { const [displayName, setDisplayName] = useState(botData.displayName); const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false); - const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); const [selectedRoles, setSelectedRoles] = useState>([]); const [roles, setRoles] = useState>([]); const { getResourceLimit, config } = useLimitStore(); @@ -114,8 +113,6 @@ const BotDetails: FC = ({ const handleDescriptionChange = async (description: string) => { await updateBotsDetails({ description }); - - setIsDescriptionEdit(false); }; const prepareSelectedRoles = () => { @@ -203,10 +200,7 @@ const BotDetails: FC = ({ entityName={getEntityName(botData)} entityType={EntityType.BOT} hasEditAccess={descriptionPermission || editAllPermission} - isEdit={isDescriptionEdit} showCommentsIcon={false} - onCancel={() => setIsDescriptionEdit(false)} - onDescriptionEdit={() => setIsDescriptionEdit(true)} onDescriptionUpdate={handleDescriptionChange} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.interface.ts index 29ed52af800a..4bea44f63408 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.interface.ts @@ -22,13 +22,11 @@ export interface TeamDetailsProp { assetsCount: number; currentTeam: Team; teams?: Team[]; - isDescriptionEditable: boolean; isTeamMemberLoading: number; isFetchingAdvancedDetails: boolean; isFetchingAllTeamAdvancedDetails: boolean; entityPermissions: OperationPermission; handleAddTeam: (value: boolean) => void; - descriptionHandler: (value: boolean) => void; onDescriptionUpdate: (value: string) => Promise; updateTeamHandler: (data: Team, fetchTeam?: boolean) => Promise; handleAddUser: (data: Array) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index 66ed0664ee21..66533266a218 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -115,14 +115,12 @@ import { UserTab } from './UserTab/UserTab.component'; const TeamDetailsV1 = ({ assetsCount, currentTeam, - isDescriptionEditable, isTeamMemberLoading, childTeams, onTeamExpand, handleAddTeam, updateTeamHandler, onDescriptionUpdate, - descriptionHandler, showDeletedTeam, onShowDeletedTeamChange, handleJoinTeamClick, @@ -1162,10 +1160,7 @@ const TeamDetailsV1 = ({ entityName={getEntityName(currentTeam)} entityType={EntityType.TEAM} hasEditAccess={editDescriptionPermission} - isEdit={isDescriptionEditable} showCommentsIcon={false} - onCancel={() => descriptionHandler(false)} - onDescriptionEdit={() => descriptionHandler(true)} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx index fe03f1924c7c..39c261445939 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx @@ -70,7 +70,6 @@ const Users = ({ const [previewAsset, setPreviewAsset] = useState(); - const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); const { t } = useTranslation(); const { getResourceLimit } = useLimitStore(); @@ -256,10 +255,8 @@ const Users = ({ const handleDescriptionChange = useCallback( async (description: string) => { await updateUserDetails({ description }, 'description'); - - setIsDescriptionEdit(false); }, - [updateUserDetails, setIsDescriptionEdit] + [updateUserDetails] ); const descriptionRenderComponent = useMemo( @@ -270,10 +267,7 @@ const Users = ({ entityName={getEntityName(userData as unknown as EntityReference)} entityType={EntityType.USER} hasEditAccess={isLoggedInUser} - isEdit={isDescriptionEdit} showCommentsIcon={false} - onCancel={() => setIsDescriptionEdit(false)} - onDescriptionEdit={() => setIsDescriptionEdit(true)} onDescriptionUpdate={handleDescriptionChange} /> ) : ( @@ -293,7 +287,6 @@ const Users = ({ [ userData, isAdminUser, - isDescriptionEdit, isLoggedInUser, getEntityName, handleDescriptionChange, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index c4407546be8d..2f7e26f7996f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -83,7 +83,7 @@ const TopicDetails: React.FC = ({ useParams<{ tab: EntityTabs }>(); const { fqn: decodedTopicFQN } = useFqn(); const history = useHistory(); - const [isEdit, setIsEdit] = useState(false); + const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -188,10 +188,6 @@ const TopicDetails: React.FC = ({ } }; - const onDescriptionEdit = (): void => setIsEdit(true); - - const onCancel = () => setIsEdit(false); - const onDescriptionUpdate = async (updatedHTML: string) => { if (description !== updatedHTML) { const updatedTopicDetails = { @@ -202,11 +198,7 @@ const TopicDetails: React.FC = ({ await onTopicUpdate(updatedTopicDetails, 'description'); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } - } else { - setIsEdit(false); } }; const onOwnerUpdate = useCallback( @@ -331,11 +323,8 @@ const TopicDetails: React.FC = ({ isDescriptionExpanded={isEmpty( topicDetails.messageSchema?.schemaFields )} - isEdit={isEdit} owner={topicDetails.owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -467,9 +456,7 @@ const TopicDetails: React.FC = ({ ), }, ], - [ - isEdit, activeTab, feedCount.totalCount, topicTags, @@ -478,8 +465,6 @@ const TopicDetails: React.FC = ({ decodedTopicFQN, fetchTopic, deleted, - onCancel, - onDescriptionEdit, handleFeedCount, onExtensionUpdate, onThreadLinkSelect, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx index 9acbaa5c0d33..9b2869dcfd10 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx @@ -156,7 +156,7 @@ export const CustomPropertyTable = ({ if ( maxDataCap && customProp.length >= maxDataCap && - entityDetails.fullyQualifiedName + entityDetails?.fullyQualifiedName ) { return ( void; - onDescriptionEdit?: () => void; - onCancel?: () => void; onDescriptionUpdate?: (value: string) => Promise; onSuggest?: (value: string) => void; onEntityFieldSelect?: (value: string) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index fb7e03adbb47..8efa87efa42c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -15,7 +15,7 @@ import Icon from '@ant-design/icons'; import { Card, Space, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; import { t } from 'i18next'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useHistory } from 'react-router'; import { ReactComponent as CommentIcon } from '../../../assets/svg/comment.svg'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; @@ -41,11 +41,8 @@ const { Text } = Typography; const DescriptionV1 = ({ hasEditAccess, - onDescriptionEdit, description = '', - isEdit, className, - onCancel, onDescriptionUpdate, isReadOnly = false, removeBlur = false, @@ -63,6 +60,7 @@ const DescriptionV1 = ({ const history = useHistory(); const { suggestions = [], selectedUserSuggestions = [] } = useSuggestionsContext(); + const [isEditDescription, setIsEditDescription] = useState(false); const handleRequestDescription = useCallback(() => { history.push( @@ -76,6 +74,25 @@ const DescriptionV1 = ({ ); }, [entityType, entityFqn]); + // Callback to handle the edit button from description + const handleEditDescription = useCallback(() => { + setIsEditDescription(true); + }, []); + + // Callback to handle the cancel button from modal + const handleCancelEditDescription = useCallback(() => { + setIsEditDescription(false); + }, []); + + // Callback to handle the description change from modal + const handleDescriptionChange = useCallback( + async (description: string) => { + await onDescriptionUpdate?.(description); + setIsEditDescription(false); + }, + [onDescriptionUpdate] + ); + const { entityLink, entityLinkWithoutField } = useMemo(() => { const entityLink = getEntityFeedLink( entityType, @@ -135,7 +152,7 @@ const DescriptionV1 = ({ component={EditIcon} data-testid="edit-description" style={{ color: DE_ACTIVE_COLOR }} - onClick={onDescriptionEdit} + onClick={handleEditDescription} /> )} @@ -161,7 +178,7 @@ const DescriptionV1 = ({ [ isReadOnly, hasEditAccess, - onDescriptionEdit, + handleEditDescription, taskActionButton, showCommentsIcon, onThreadLinkSelect, @@ -222,9 +239,9 @@ const DescriptionV1 = ({ entity: t('label.description'), })} value={description} - visible={Boolean(isEdit)} - onCancel={onCancel} - onSave={onDescriptionUpdate} + visible={Boolean(isEditDescription)} + onCancel={handleCancelEditDescription} + onSave={handleDescriptionChange} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 6fc8ea75e3b5..2d8c9aa8ee93 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -23,6 +23,8 @@ export enum DetailPageWidgetKeys { DESCRIPTION = 'KnowledgePanel.Description', TABLE_SCHEMA = 'KnowledgePanel.TableSchema', TOPIC_SCHEMA = 'KnowledgePanel.TopicSchema', + DATABASE_SCHEMA = 'KnowledgePanel.DatabaseSchema', + CHARTS_TABLE = 'KnowledgePanel.ChartsTable', FREQUENTLY_JOINED_TABLES = 'KnowledgePanel.FrequentlyJoinedTables', DATA_PRODUCTS = 'KnowledgePanel.DataProducts', TAGS = 'KnowledgePanel.Tags', @@ -34,6 +36,7 @@ export enum DetailPageWidgetKeys { PARTITIONED_KEYS = 'KnowledgePanel.PartitionedKeys', STORED_PROCEDURE_CODE = 'KnowledgePanel.StoredProcedureCode', DATA_MODEL = 'KnowledgePanel.DataModel', + CONTAINER_CHILDREN = 'KnowledgePanel.ContainerChildren', } export enum GlossaryTermDetailPageWidgetKeys { 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 6ae69752d416..872f28874a9c 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 @@ -210,6 +210,7 @@ export enum EntityTabs { GLOSSARY_TERMS = 'glossary_terms', ASSETS = 'assets', EXPRESSION = 'expression', + DASHBOARD = 'dashboard', } export enum EntityAction { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index 04476b83ece8..b30a574fdb7a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -127,7 +127,7 @@ const APICollectionPage: FunctionComponent = () => { const [apiEndpointsLoading, setAPIEndpointsLoading] = useState(true); const [isAPICollectionLoading, setIsAPICollectionLoading] = useState(true); - const [isEdit, setIsEdit] = useState(false); + const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -254,14 +254,6 @@ const APICollectionPage: FunctionComponent = () => { [decodedAPICollectionFQN, showDeletedEndpoints, apiCollection] ); - const onDescriptionEdit = useCallback((): void => { - setIsEdit(true); - }, []); - - const onEditCancel = useCallback(() => { - setIsEdit(false); - }, []); - const saveUpdatedAPICollectionData = useCallback( (updatedData: APICollection) => { let jsonPatch: Operation[] = []; @@ -293,11 +285,7 @@ const APICollectionPage: FunctionComponent = () => { } } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } - } else { - setIsEdit(false); } }, [apiCollection] @@ -592,11 +580,8 @@ const APICollectionPage: FunctionComponent = () => { description={apiCollection?.description ?? ''} editDescriptionPermission={editDescriptionPermission} endpointPaginationHandler={endpointPaginationHandler} - isEdit={isEdit} pagingInfo={pagingInfo} showDeletedEndpoints={showDeletedEndpoints} - onCancel={onEditCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onShowDeletedEndpointsChange={handleShowDeletedEndPoints} onThreadLinkSelect={onThreadLinkSelect} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index 3e72fda34f31..2e8b5319433d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -37,13 +37,10 @@ interface APIEndpointsTabProps { apiEndpointsLoading: boolean; description: string; editDescriptionPermission?: boolean; - isEdit?: boolean; showDeletedEndpoints?: boolean; apiEndpoints: APIEndpoint[]; currentEndpointsPage: number; endpointPaginationHandler: NextPreviousProps['pagingHandler']; - onCancel?: () => void; - onDescriptionEdit?: () => void; onDescriptionUpdate?: (updatedHTML: string) => Promise; onThreadLinkSelect?: (link: string) => void; onShowDeletedEndpointsChange?: (value: boolean) => void; @@ -56,12 +53,9 @@ function APIEndpointsTab({ apiEndpointsLoading, description, editDescriptionPermission = false, - isEdit = false, apiEndpoints, currentEndpointsPage, endpointPaginationHandler, - onCancel, - onDescriptionEdit, onDescriptionUpdate, onThreadLinkSelect, showDeletedEndpoints = false, @@ -137,10 +131,7 @@ function APIEndpointsTab({ entityType={EntityType.API_COLLECTION} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(apiEndpoints)} - isEdit={isEdit} showActions={!apiCollectionDetails.deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx index 466b2ecfaf4c..ac25ba284bf5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx @@ -85,8 +85,7 @@ function AlertDetailsPage({ const [ownerLoading, setOwnerLoading] = useState(false); const [alertEventCountsLoading, setAlertEventCountsLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showDescriptionModal, setShowDescriptionModal] = - useState(false); + const [alertPermission, setAlertPermission] = useState( DEFAULT_ENTITY_PERMISSION ); @@ -110,14 +109,6 @@ function AlertDetailsPage({ [alertPermission] ); - const onDescriptionEdit = useCallback(() => { - setShowDescriptionModal(true); - }, []); - - const onCancel = useCallback(() => { - setShowDescriptionModal(false); - }, []); - const fetchResourcePermission = useCallback(async () => { try { if (fqn) { @@ -255,8 +246,6 @@ function AlertDetailsPage({ setAlertDetails(updatedAlert); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setShowDescriptionModal(false); } }, [fqn, history, alertDetails] @@ -420,10 +409,7 @@ function AlertDetailsPage({ description={alertDetails?.description} entityType={EntityType.EVENT_SUBSCRIPTION} hasEditAccess={editDescriptionPermission} - isEdit={showDescriptionModal} showCommentsIcon={false} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 93338df8fbfd..0b3a745b04e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -19,33 +19,22 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; -import ContainerChildren from '../../components/Container/ContainerChildren/ContainerChildren'; -import ContainerDataModel from '../../components/Container/ContainerDataModel/ContainerDataModel'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../components/Lineage/Lineage.component'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../components/SearchedData/SearchedData.interface'; +import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, ROUTES, } from '../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; -import LineageProvider from '../../context/LineageProvider/LineageProvider'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -62,12 +51,15 @@ import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { Container } from '../../generated/entity/data/container'; import { ThreadType } from '../../generated/entity/feed/thread'; +import { Page } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/uiCustomization'; import { Include } from '../../generated/type/include'; import { TagLabel } from '../../generated/type/tagLabel'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { postThread } from '../../rest/feedsAPI'; import { addContainerFollower, @@ -83,16 +75,20 @@ import { getFeedCounts, sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; +import containerDetailsClassBase from '../../utils/ContainerDetailsClassBase'; import { getEntityName } from '../../utils/EntityUtils'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; - const ContainerPage = () => { const history = useHistory(); const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const { getEntityPermissionByFqn } = usePermissionProvider(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab } = useParams<{ tab: EntityTabs }>(); @@ -104,7 +100,7 @@ const ContainerPage = () => { const [isChildrenLoading, setIsChildrenLoading] = useState(false); const [hasError, setHasError] = useState(false); const [isEditDescription, setIsEditDescription] = useState(false); - + const [customizedPage, setCustomizedPage] = useState(null); const [containerData, setContainerData] = useState(); const [containerChildrenData, setContainerChildrenData] = useState< Container['children'] @@ -572,212 +568,77 @@ const ContainerPage = () => { } }; - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: isDataModelEmpty ? EntityTabs.CHILDREN : EntityTabs.SCHEMA, - children: ( - - - - setIsEditDescription(false)} - onDescriptionEdit={() => setIsEditDescription(true)} - onDescriptionUpdate={handleUpdateDescription} - onThreadLinkSelect={onThreadLinkSelect} - /> - - {isDataModelEmpty ? ( - - ) : ( - - )} - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={containerData} - dataProducts={containerData?.dataProducts ?? []} - domain={containerData?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={ - editTagsPermission && !containerData?.deleted - } - entityFQN={decodedContainerName} - entityId={containerData?.id ?? ''} - entityType={EntityType.CONTAINER} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={handleExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - ...(isDataModelEmpty - ? [] - : [ - { - label: ( - - ), - key: EntityTabs.CHILDREN, - children: ( - -
- - - - ), - }, - ]), - - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - fetchContainerDetail(decodedContainerName) - } - onUpdateFeedCount={handleFeedCount} - /> - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: containerData && ( -
- - entityDetails={containerData} - entityType={EntityType.CONTAINER} - handleExtensionUpdate={handleExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + if (!containerData) { + return []; + } + + const tabs = containerDetailsClassBase.getContainerDetailPageTabs({ isDataModelEmpty, - containerData, - description, - decodedContainerName, + description: description ?? '', decodedContainerName, entityName, editDescriptionPermission, editTagsPermission, editGlossaryTermsPermission, - isEditDescription, editLineagePermission, editCustomAttributePermission, viewAllPermission, - deleted, - owners, - isChildrenLoading, - tags, - feedCount.totalCount, - containerChildrenData, + containerChildrenData: containerChildrenData ?? [], + fetchContainerChildren, + isChildrenLoading: isChildrenLoading ?? false, + feedCount: feedCount ?? { totalCount: 0 }, + getEntityFeedCount, handleFeedCount, - handleUpdateDataModel, + tab, + deleted: deleted ?? false, + owners: owners ?? [], + containerData, + tags: tags ?? [], + fetchContainerDetail, + labelMap: tabLabelMap, + onThreadLinkSelect, handleUpdateDescription, + handleUpdateDataModel, handleTagSelection, - onThreadLinkSelect, handleExtensionUpdate, - ] - ); + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.CHILDREN + ); + }, [ + isDataModelEmpty, + containerData, + description, + decodedContainerName, + decodedContainerName, + entityName, + editDescriptionPermission, + editTagsPermission, + editGlossaryTermsPermission, + isEditDescription, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + deleted, + owners, + isChildrenLoading, + tags, + feedCount.totalCount, + containerChildrenData, + handleFeedCount, + handleUpdateDataModel, + handleUpdateDescription, + handleTagSelection, + onThreadLinkSelect, + handleExtensionUpdate, + customizedPage?.tabs, + ]); const updateVote = async (data: QueryVote, id: string) => { try { @@ -801,6 +662,24 @@ const ContainerPage = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Container) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + // Effects useEffect(() => { fetchResourcePermission(decodedContainerName); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx index 64cd593909f8..670f75e0c292 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx @@ -269,6 +269,12 @@ export const CustomizablePage = () => { case PageType.Topic: case PageType.StoredProcedure: case PageType.DashboardDataModel: + case PageType.Dashboard: + case PageType.Pipeline: + case PageType.DatabaseSchema: + case PageType.Database: + case PageType.Container: + case PageType.SearchIndex: return (
{ {} as Dashboard ); const [isLoading, setLoading] = useState(false); - const [charts, setCharts] = useState([]); const [isError, setIsError] = useState(false); const [dashboardPermissions, setDashboardPermissions] = useState( DEFAULT_ENTITY_PERMISSION ); - const { id: dashboardId, version } = dashboardDetails; + const { id: dashboardId, version, charts } = dashboardDetails; const fetchResourcePermission = async (entityFqn: string) => { setLoading(true); @@ -123,7 +117,7 @@ const DashboardDetailsPage = () => { } const res = await getDashboardByFqn(dashboardFQN, { fields }); - const { id, fullyQualifiedName, charts: ChartIds, serviceType } = res; + const { id, fullyQualifiedName, serviceType } = res; setDashboardDetails(res); addToRecentViewed({ @@ -135,19 +129,6 @@ const DashboardDetailsPage = () => { id: id, }); - fetchCharts(ChartIds) - .then((chart) => { - setCharts(chart); - }) - .catch((error: AxiosError) => { - showErrorToast( - error, - t('server.entity-fetch-error', { - entity: t('label.chart-plural'), - }) - ); - }); - setLoading(false); } catch (error) { if ((error as AxiosError).response?.status === 404) { @@ -231,50 +212,6 @@ const DashboardDetailsPage = () => { } }; - const onChartUpdate = async ( - index: number, - chartId: string, - patch: Array - ) => { - try { - const response = await updateChart(chartId, patch); - setCharts((prevCharts) => { - const charts = [...prevCharts]; - charts[index] = response; - - return charts; - }); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const handleChartTagSelection = async ( - chartId: string, - patch: Array - ) => { - try { - const res = await updateChart(chartId, patch); - - setCharts((prevCharts) => { - const charts = [...prevCharts].map((chart) => - chart.id === chartId ? res : chart - ); - - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - return sortTagsForCharts(charts); - }); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.entity-updating-error', { - entity: t('label.chart-plural'), - }) - ); - } - }; - const versionHandler = () => { version && history.push( @@ -356,9 +293,7 @@ const DashboardDetailsPage = () => { return ( fetchDashboardDetail(dashboardFQN)} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index d406fdd97126..4896b0586278 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -27,20 +27,13 @@ import React, { import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import { DatabaseSchemaTable } from '../../components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; import ProfilerSettings from '../../components/Database/Profiler/ProfilerSettings/ProfilerSettings'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -51,7 +44,6 @@ import { ROUTES, } from '../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -67,8 +59,11 @@ import { import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { Database } from '../../generated/entity/data/database'; +import { Page } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/uiCustomization'; import { Include } from '../../generated/type/include'; import { useLocationSearch } from '../../hooks/LocationSearch/useLocationSearch'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; import { @@ -78,6 +73,7 @@ import { restoreDatabase, updateDatabaseVotes, } from '../../rest/databaseAPI'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { postThread } from '../../rest/feedsAPI'; import { getEntityMissingError, @@ -85,8 +81,13 @@ import { sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { getQueryFilterForDatabase } from '../../utils/Database/Database.util'; +import databaseClassBase from '../../utils/Database/DatabaseClassBase'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; @@ -98,11 +99,13 @@ const DatabaseDetails: FunctionComponent = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const { withinPageSearch } = useLocationSearch<{ withinPageSearch: string }>(); + const { selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedDatabaseFQN } = useFqn(); const [isLoading, setIsLoading] = useState(true); + const [customizedPage, setCustomizedPage] = useState(null); const [database, setDatabase] = useState({} as Database); const [serviceType, setServiceType] = useState(); @@ -112,7 +115,6 @@ const DatabaseDetails: FunctionComponent = () => { ); const [isDatabaseDetailsLoading, setIsDatabaseDetailsLoading] = useState(true); - const [isEdit, setIsEdit] = useState(false); const [description, setDescription] = useState(''); const [databaseId, setDatabaseId] = useState(''); @@ -231,10 +233,6 @@ const DatabaseDetails: FunctionComponent = () => { }); }; - const onCancel = () => { - setIsEdit(false); - }; - const saveUpdatedDatabaseData = (updatedData: Database) => { let jsonPatch: Operation[] = []; if (database) { @@ -260,18 +258,10 @@ const DatabaseDetails: FunctionComponent = () => { } } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } - } else { - setIsEdit(false); } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const activeTabHandler = (key: string) => { if (key !== activeTab) { history.push({ @@ -512,150 +502,57 @@ const DatabaseDetails: FunctionComponent = () => { })); }, []); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.SCHEMA, - children: ( - -
- - - - - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={database} - dataProducts={database?.dataProducts ?? []} - domain={database?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityFQN={decodedDatabaseFQN} - entityId={database?.id ?? ''} - entityType={EntityType.DATABASE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={settingsUpdateHandler} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: database && ( -
- - entityDetails={database} - entityType={EntityType.DATABASE} - handleExtensionUpdate={settingsUpdateHandler} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - isVersionView={false} - /> -
- ), - }, - ], - [ - tags, - isEdit, + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = databaseClassBase.getDatabaseDetailPageTabs({ + activeTab, database, description, - databaseName, decodedDatabaseFQN, - activeTab, - databasePermission, - schemaInstanceCount, - feedCount.totalCount, - editTagsPermission, - editGlossaryTermsPermission, editDescriptionPermission, - editCustomAttributePermission, + editGlossaryTermsPermission, + editTagsPermission, viewAllPermission, - deleted, + tags, + schemaInstanceCount, + feedCount, handleFeedCount, - ] - ); + getEntityFeedCount, + onDescriptionUpdate, + onThreadLinkSelect, + handleTagSelection, + settingsUpdateHandler, + deleted: database.deleted ?? false, + editCustomAttributePermission, + getDetailsByFQN, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.CHILDREN + ); + }, [ + tags, + database, + description, + databaseName, + decodedDatabaseFQN, + activeTab, + databasePermission, + schemaInstanceCount, + feedCount.totalCount, + editTagsPermission, + editGlossaryTermsPermission, + editDescriptionPermission, + editCustomAttributePermission, + viewAllPermission, + deleted, + handleFeedCount, + customizedPage?.tabs, + ]); const updateVote = async (data: QueryVote, id: string) => { try { @@ -674,6 +571,24 @@ const DatabaseDetails: FunctionComponent = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Database) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + if (isLoading || isDatabaseDetailsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index fafbefdef5f0..99f96e5210e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -42,13 +42,10 @@ interface SchemaTablesTabProps { tableDataLoading: boolean; description: string; editDescriptionPermission?: boolean; - isEdit?: boolean; showDeletedTables?: boolean; tableData: Table[]; currentTablesPage: number; tablePaginationHandler: NextPreviousProps['pagingHandler']; - onCancel?: () => void; - onDescriptionEdit?: () => void; onDescriptionUpdate?: (updatedHTML: string) => Promise; onThreadLinkSelect?: (link: string) => void; onShowDeletedTablesChange?: (value: boolean) => void; @@ -61,12 +58,9 @@ function SchemaTablesTab({ tableDataLoading, description, editDescriptionPermission = false, - isEdit = false, tableData, currentTablesPage, tablePaginationHandler, - onCancel, - onDescriptionEdit, onDescriptionUpdate, onThreadLinkSelect, showDeletedTables = false, @@ -172,10 +166,7 @@ function SchemaTablesTab({ entityType={EntityType.DATABASE_SCHEMA} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(tableData)} - isEdit={isEdit} showActions={!databaseSchemaDetails.deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx index 8b75cd8c7368..d473018c870f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaDetailsPage/PersonaDetailsPage.tsx @@ -49,7 +49,6 @@ export const PersonaDetailsPage = () => { const history = useHistory(); const [personaDetails, setPersonaDetails] = useState(); const [isLoading, setIsLoading] = useState(true); - const [isEdit, setIsEdit] = useState(false); const { t } = useTranslation(); const [entityPermission, setEntityPermission] = useState( DEFAULT_ENTITY_PERMISSION @@ -112,8 +111,6 @@ export const PersonaDetailsPage = () => { setPersonaDetails(response); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } }; @@ -129,8 +126,6 @@ export const PersonaDetailsPage = () => { setPersonaDetails(response); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } }; @@ -146,8 +141,6 @@ export const PersonaDetailsPage = () => { setPersonaDetails(response); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); } }, [personaDetails] @@ -242,10 +235,7 @@ export const PersonaDetailsPage = () => { hasEditAccess description={personaDetails.description} entityType={EntityType.PERSONA} - isEdit={isEdit} showCommentsIcon={false} - onCancel={() => setIsEdit(false)} - onDescriptionEdit={() => setIsEdit(true)} onDescriptionUpdate={handleDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx index aa8f34590812..205b4c47b9e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx @@ -77,7 +77,6 @@ const PoliciesDetailPage = () => { const [policy, setPolicy] = useState({} as Policy); const [isLoading, setLoading] = useState(false); const [isloadingOnSave, setIsloadingOnSave] = useState(false); - const [editDescription, setEditDescription] = useState(false); const [selectedEntity, setEntity] = useState<{ attribute: Attribute; record: EntityReference }>(); @@ -124,8 +123,6 @@ const PoliciesDetailPage = () => { setPolicy({ ...policy, description: data.description }); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setEditDescription(false); } }; @@ -345,10 +342,7 @@ const PoliciesDetailPage = () => { entityFqn={policy.fullyQualifiedName} entityName={policyName} entityType={EntityType.POLICY} - isEdit={editDescription} showCommentsIcon={false} - onCancel={() => setEditDescription(false)} - onDescriptionEdit={() => setEditDescription(true)} onDescriptionUpdate={handleDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx index ed1a89b2abbe..c7888753484a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx @@ -59,7 +59,6 @@ const RolesDetailPage = () => { const [role, setRole] = useState({} as Role); const [isLoading, setLoading] = useState(false); const [isLoadingOnSave, setIsLoadingOnSave] = useState(false); - const [editDescription, setEditDescription] = useState(false); const [selectedEntity, setEntity] = useState<{ attribute: Attribute; record: EntityReference }>(); @@ -105,8 +104,6 @@ const RolesDetailPage = () => { setRole({ ...role, description: data.description }); } catch (error) { showErrorToast(error as AxiosError); - } finally { - setEditDescription(false); } }; @@ -266,10 +263,7 @@ const RolesDetailPage = () => { entityFqn={role.fullyQualifiedName} entityName={roleName} entityType={EntityType.ROLE} - isEdit={editDescription} showCommentsIcon={false} - onCancel={() => setEditDescription(false)} - onDescriptionEdit={() => setEditDescription(true)} onDescriptionUpdate={handleDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index d152cb21aa30..ae5f5ce0724a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -99,7 +99,7 @@ function SearchIndexDetailsPage() { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [isEdit, setIsEdit] = useState(false); + const [threadLink, setThreadLink] = useState(''); const [threadType, setThreadType] = useState( ThreadType.Conversation @@ -136,13 +136,6 @@ function SearchIndexDetailsPage() { } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const onCancel = () => { - setIsEdit(false); - }; - const { tier, searchIndexTags, @@ -312,9 +305,6 @@ function SearchIndexDetailsPage() { description: updatedHTML, }; await onSearchIndexUpdate(updatedSearchIndexDetails, 'description'); - setIsEdit(false); - } else { - setIsEdit(false); } }; @@ -400,11 +390,8 @@ function SearchIndexDetailsPage() { isDescriptionExpanded={isEmpty( searchIndexDetails?.fields )} - isEdit={isEdit} owner={searchIndexDetails?.owners} showActions={!searchIndexDetails?.deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> @@ -565,10 +552,8 @@ function SearchIndexDetailsPage() { editLineagePermission, editCustomAttributePermission, viewAllPermission, - isEdit, searchIndexDetails, searchIndexDetails?.extension, - onDescriptionEdit, onDescriptionUpdate, editTagsPermission, editGlossaryTermsPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx index bc8ec9e475ba..4ed637ba3258 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx @@ -84,11 +84,8 @@ function ServiceMainTabContent({ const { serviceCategory } = useParams<{ serviceCategory: ServiceTypes; }>(); - const { fqn: serviceFQN } = useFqn(); const { permissions } = usePermissionProvider(); - - const [isEdit, setIsEdit] = useState(false); const [pageData, setPageData] = useState([]); const tier = getTierTags(serviceDetails?.tags ?? []); @@ -131,19 +128,9 @@ function ServiceMainTabContent({ await onDescriptionUpdate(updatedHTML); } catch (e) { // Error - } finally { - setIsEdit(false); } }, []); - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - - const onCancel = () => { - setIsEdit(false); - }; - const handleDisplayNameUpdate = useCallback( async (entityData: EntityName, id?: string) => { try { @@ -249,11 +236,8 @@ function ServiceMainTabContent({ entityName={serviceName} entityType={entityType} hasEditAccess={editDescriptionPermission} - isEdit={isEdit} showActions={!serviceDetails.deleted} showCommentsIcon={false} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={handleDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx index cbf30878fe85..ea34585a3a47 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx @@ -110,8 +110,7 @@ const TagPage = () => { const [isLoading, setIsLoading] = useState(false); const [tagItem, setTagItem] = useState(); const [assetModalVisible, setAssetModalVisible] = useState(false); - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); + const [isNameEditing, setIsNameEditing] = useState(false); const [isStyleEditing, setIsStyleEditing] = useState(false); const [isDelete, setIsDelete] = useState(false); @@ -210,9 +209,6 @@ const TagPage = () => { } catch (error) { showErrorToast(error as AxiosError); } - setIsDescriptionEditable(false); - } else { - setIsDescriptionEditable(false); } } }; @@ -445,11 +441,8 @@ const TagPage = () => { entityName={getEntityName(tagItem)} entityType={EntityType.TAG} hasEditAccess={editDescriptionPermission} - isEdit={isDescriptionEditable} showActions={!tagItem?.deleted} showCommentsIcon={false} - onCancel={() => setIsDescriptionEditable(false)} - onDescriptionEdit={() => setIsDescriptionEditable(true)} onDescriptionUpdate={onDescriptionUpdate} /> @@ -540,7 +533,6 @@ const TagPage = () => { assetCount, assetTabRef, handleAssetSave, - isDescriptionEditable, editTagsPermission, editDescriptionPermission, ]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx index 8a5efd7a36e5..397f5d65a5ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagsPage/TagsPage.tsx @@ -82,8 +82,6 @@ const TagsPage = () => { ); const [currentClassification, setCurrentClassification] = useState(); - const [isEditClassification, setIsEditClassification] = - useState(false); const [isAddingClassification, setIsAddingClassification] = useState(false); const [isAddingTag, setIsAddingTag] = useState(false); @@ -370,7 +368,6 @@ const TagsPage = () => { ); } } finally { - setIsEditClassification(false); setIsUpdateLoading(false); } } @@ -485,14 +482,6 @@ const TagsPage = () => { setIsAddingTag(true); }, []); - const handleEditDescriptionClick = useCallback(() => { - setIsEditClassification(true); - }, []); - - const handleCancelEditDescription = useCallback(() => { - setIsEditClassification(false); - }, []); - useEffect(() => { if (currentClassification) { fetchCurrentClassificationPermission(); @@ -737,12 +726,9 @@ const TagsPage = () => { handleActionDeleteTag={handleActionDeleteTag} handleAddNewTagClick={handleAddNewTagClick} handleAfterDeleteAction={handleAfterDeleteAction} - handleCancelEditDescription={handleCancelEditDescription} - handleEditDescriptionClick={handleEditDescriptionClick} handleEditTagClick={handleEditTagClick} handleUpdateClassification={handleUpdateClassification} isAddingTag={isAddingTag} - isEditClassification={isEditClassification} ref={classificationDetailsRef} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/TeamsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/TeamsPage.tsx index dde3fe706b2d..f544a0fcf86a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/TeamsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/TeamsPage.tsx @@ -65,8 +65,6 @@ const TeamsPage = () => { const [showDeletedTeam, setShowDeletedTeam] = useState(false); const [isPageLoading, setIsPageLoading] = useState(true); - const [isDescriptionEditable, setIsDescriptionEditable] = - useState(false); const [isAddingTeam, setIsAddingTeam] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -86,10 +84,6 @@ const TeamsPage = () => { [entityPermissions] ); - const descriptionHandler = (value: boolean) => { - setIsDescriptionEditable(value); - }; - const handleAddTeam = (value: boolean) => { setIsAddingTeam(value); }; @@ -432,11 +426,7 @@ const TeamsPage = () => { } } catch (error) { showErrorToast(error as AxiosError); - } finally { - descriptionHandler(false); } - } else { - descriptionHandler(false); } }; @@ -517,13 +507,11 @@ const TeamsPage = () => { assetsCount={assets} childTeams={childTeams} currentTeam={selectedTeam} - descriptionHandler={descriptionHandler} entityPermissions={entityPermissions} handleAddTeam={handleAddTeam} handleAddUser={addUsersToTeam} handleJoinTeamClick={handleJoinTeamClick} handleLeaveTeamClick={handleLeaveTeamClick} - isDescriptionEditable={isDescriptionEditable} isFetchingAdvancedDetails={isFetchingAdvancedDetails} isFetchingAllTeamAdvancedDetails={isFetchAllTeamAdvancedDetails} isTeamMemberLoading={isDataLoading} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx index 9444aef2916c..21488f9211af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TestSuiteDetailsPage/TestSuiteDetailsPage.component.tsx @@ -83,7 +83,7 @@ const TestSuiteDetailsPage = () => { history.push(getDataQualityPagePath(DataQualityPageTabs.TEST_SUITES)); }; const [testSuite, setTestSuite] = useState(); - const [isDescriptionEditable, setIsDescriptionEditable] = useState(false); + const [isTestCaseLoading, setIsTestCaseLoading] = useState(true); const [testCaseResult, setTestCaseResult] = useState>([]); @@ -135,10 +135,6 @@ const TestSuiteDetailsPage = () => { return updateTestSuiteById(testSuiteId as string, jsonPatch); }; - const descriptionHandler = (value: boolean) => { - setIsDescriptionEditable(value); - }; - const fetchTestSuitePermission = async () => { setIsLoading(true); try { @@ -280,11 +276,7 @@ const TestSuiteDetailsPage = () => { } } catch (error) { showErrorToast(error as AxiosError); - } finally { - descriptionHandler(false); } - } else { - descriptionHandler(false); } }; @@ -490,10 +482,7 @@ const TestSuiteDetailsPage = () => { entityName={getEntityName(testSuite)} entityType={EntityType.TEST_SUITE} hasEditAccess={isAdminUser} - isEdit={isDescriptionEditable} showCommentsIcon={false} - onCancel={() => descriptionHandler(false)} - onDescriptionEdit={() => descriptionHandler(true)} onDescriptionUpdate={onDescriptionUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.ts deleted file mode 100644 index 656ba18bc230..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2023 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 { isEmpty, omit } from 'lodash'; -import { EntityTags } from 'Models'; -import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; -import { Column, ContainerDataModel } from '../generated/entity/data/container'; -import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; - -const getUpdatedContainerColumnTags = ( - containerColumn: Column, - newContainerColumnTags: EntityTags[] = [] -) => { - const newTagsFqnList = newContainerColumnTags.map((newTag) => newTag.tagFQN); - - const prevTags = containerColumn?.tags?.filter((tag) => - newTagsFqnList.includes(tag.tagFQN) - ); - - const prevTagsFqnList = prevTags?.map((prevTag) => prevTag.tagFQN); - - const newTags: EntityTags[] = newContainerColumnTags.reduce((prev, curr) => { - const isExistingTag = prevTagsFqnList?.includes(curr.tagFQN); - - return isExistingTag - ? prev - : [ - ...prev, - { - ...omit(curr, 'isRemovable'), - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ]; - }, [] as EntityTags[]); - - return [...(prevTags as TagLabel[]), ...newTags]; -}; - -export const updateContainerColumnTags = ( - containerColumns: ContainerDataModel['columns'] = [], - changedColumnFQN: string, - newColumnTags: EntityTags[] = [] -) => { - containerColumns.forEach((containerColumn) => { - if (containerColumn.fullyQualifiedName === changedColumnFQN) { - containerColumn.tags = getUpdatedContainerColumnTags( - containerColumn, - newColumnTags - ); - } else { - const hasChildren = !isEmpty(containerColumn.children); - - // stop condition - if (hasChildren) { - updateContainerColumnTags( - containerColumn.children, - changedColumnFQN, - newColumnTags - ); - } - } - }); -}; - -export const updateContainerColumnDescription = ( - containerColumns: ContainerDataModel['columns'] = [], - changedColumnFQN: string, - description: string -) => { - containerColumns.forEach((containerColumn) => { - if (containerColumn.fullyQualifiedName === changedColumnFQN) { - containerColumn.description = description; - } else { - const hasChildren = !isEmpty(containerColumn.children); - - // stop condition - if (hasChildren) { - updateContainerColumnDescription( - containerColumn.children, - changedColumnFQN, - description - ); - } - } - }); -}; - -export const getContainerDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - -// eslint-disable-next-line max-len -export const ContainerFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.FOLLOWERS},${TabSpecificField.DATAMODEL}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}`; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx new file mode 100644 index 000000000000..4a55fd509c7f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -0,0 +1,316 @@ +/* + * Copyright 2023 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 { Col, Row } from 'antd'; +import { t } from 'i18next'; +import { isEmpty, omit } from 'lodash'; +import { EntityTags } from 'Models'; +import React from 'react'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; +import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import ContainerChildren from '../components/Container/ContainerChildren/ContainerChildren'; +import ContainerDataModel from '../components/Container/ContainerDataModel/ContainerDataModel'; +import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; +import Lineage from '../components/Lineage/Lineage.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { + Column, + ContainerDataModel as ContainerDataModelType, +} from '../generated/entity/data/container'; +import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; +import { ContainerDetailPageTabProps } from './ContainerDetailsClassBase'; + +const getUpdatedContainerColumnTags = ( + containerColumn: Column, + newContainerColumnTags: EntityTags[] = [] +) => { + const newTagsFqnList = newContainerColumnTags.map((newTag) => newTag.tagFQN); + + const prevTags = containerColumn?.tags?.filter((tag) => + newTagsFqnList.includes(tag.tagFQN) + ); + + const prevTagsFqnList = prevTags?.map((prevTag) => prevTag.tagFQN); + + const newTags: EntityTags[] = newContainerColumnTags.reduce((prev, curr) => { + const isExistingTag = prevTagsFqnList?.includes(curr.tagFQN); + + return isExistingTag + ? prev + : [ + ...prev, + { + ...omit(curr, 'isRemovable'), + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ]; + }, [] as EntityTags[]); + + return [...(prevTags as TagLabel[]), ...newTags]; +}; + +export const updateContainerColumnTags = ( + containerColumns: ContainerDataModelType['columns'] = [], + changedColumnFQN: string, + newColumnTags: EntityTags[] = [] +) => { + containerColumns.forEach((containerColumn) => { + if (containerColumn.fullyQualifiedName === changedColumnFQN) { + containerColumn.tags = getUpdatedContainerColumnTags( + containerColumn, + newColumnTags + ); + } else { + const hasChildren = !isEmpty(containerColumn.children); + + // stop condition + if (hasChildren) { + updateContainerColumnTags( + containerColumn.children, + changedColumnFQN, + newColumnTags + ); + } + } + }); +}; + +export const updateContainerColumnDescription = ( + containerColumns: ContainerDataModelType['columns'] = [], + changedColumnFQN: string, + description: string +) => { + containerColumns.forEach((containerColumn) => { + if (containerColumn.fullyQualifiedName === changedColumnFQN) { + containerColumn.description = description; + } else { + const hasChildren = !isEmpty(containerColumn.children); + + // stop condition + if (hasChildren) { + updateContainerColumnDescription( + containerColumn.children, + changedColumnFQN, + description + ); + } + } + }); +}; + +// eslint-disable-next-line max-len +export const ContainerFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.FOLLOWERS},${TabSpecificField.DATAMODEL}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}`; + +export const getContainerDetailPageTabs = ({ + isDataModelEmpty, + description, + decodedContainerName, + entityName, + editDescriptionPermission, + editGlossaryTermsPermission, + editTagsPermission, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + containerChildrenData, + fetchContainerChildren, + isChildrenLoading, + onThreadLinkSelect, + handleUpdateDescription, + handleUpdateDataModel, + handleTagSelection, + handleExtensionUpdate, + feedCount, + getEntityFeedCount, + handleFeedCount, + tab, + deleted, + owners, + containerData, + fetchContainerDetail, + tags, +}: ContainerDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: isDataModelEmpty ? EntityTabs.CHILDREN : EntityTabs.SCHEMA, + children: ( + +
+ + + + {isDataModelEmpty ? ( + + ) : ( + + )} + + ), + ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, + }} + secondPanel={{ + children: ( +
+ + customProperties={containerData} + dataProducts={containerData?.dataProducts ?? []} + domain={containerData?.domain} + editCustomAttributePermission={ + editCustomAttributePermission + } + editGlossaryTermsPermission={editGlossaryTermsPermission} + editTagPermission={ + editTagsPermission && !containerData?.deleted + } + entityFQN={decodedContainerName} + entityId={containerData?.id ?? ''} + entityType={EntityType.CONTAINER} + selectedTags={tags} + viewAllPermission={viewAllPermission} + onExtensionUpdate={handleExtensionUpdate} + onTagSelectionChange={handleTagSelection} + onThreadLinkSelect={onThreadLinkSelect} + /> +
+ ), + ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, + className: + 'entity-resizable-right-panel-container entity-resizable-panel-container', + }} + /> + + + ), + }, + ...(isDataModelEmpty + ? [] + : [ + { + label: ( + + ), + key: EntityTabs.CHILDREN, + children: ( + +
+ + + + ), + }, + ]), + + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + fetchContainerDetail(decodedContainerName) + } + onUpdateFeedCount={handleFeedCount} + /> + ), + }, + { + label: , + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: containerData && ( +
+ + entityDetails={containerData} + entityType={EntityType.CONTAINER} + handleExtensionUpdate={handleExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts new file mode 100644 index 000000000000..a4276b10f745 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -0,0 +1,249 @@ +/* + * Copyright 2024 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 { EntityTags } from 'Models'; +import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { + Container, + ContainerDataModel, + FileFormat, + StorageServiceType, +} from '../generated/entity/data/container'; +import { ThreadType } from '../generated/entity/feed/thread'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { getContainerDetailPageTabs } from './ContainerDetailUtils'; + +import { EntityReference } from '../generated/entity/type'; +import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import i18n from './i18next/LocalUtil'; + +export interface ContainerDetailPageTabProps { + isDataModelEmpty: boolean; + description: string; + decodedContainerName: string; + entityName: string; + editDescriptionPermission: boolean; + editGlossaryTermsPermission: boolean; + editTagsPermission: boolean; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + containerChildrenData: EntityReference[]; + fetchContainerChildren: () => void; + isChildrenLoading: boolean; + onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; + handleUpdateDescription: (description: string) => Promise; + handleUpdateDataModel: (dataModel?: ContainerDataModel) => Promise; + handleTagSelection: (tags: EntityTags[]) => Promise; + handleExtensionUpdate: (updatedContainer: Container) => Promise; + feedCount: { totalCount: number }; + getEntityFeedCount: () => void; + handleFeedCount: (data: FeedCounts) => void; + tab: EntityTabs; + owners: EntityReference[]; + deleted: boolean; + containerData: Container; + tags: EntityTags[]; + fetchContainerDetail: (containerFQN: string) => Promise; + labelMap?: Record; +} + +class ContainerDetailsClassBase { + public getContainerDetailPageTabs( + tabProps: ContainerDetailPageTabProps + ): TabProps[] { + return getContainerDetailPageTabs(tabProps); + } + + public getContainerDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.CHILDREN, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.CHILDREN, + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.CHILDREN: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 3, + i: DetailPageWidgetKeys.CONTAINER_CHILDREN, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): Container { + return { + id: '4e90debf-d063-49fd-9a5d-71ee43e6840a', + name: 'departments', + fullyQualifiedName: 's3_storage_sample.departments', + displayName: 'Company departments', + description: 'Bucket containing company department information. asd', + version: 0.3, + updatedAt: 1722838506844, + updatedBy: 'sachin', + href: 'http://test-argo.getcollate.io/api/v1/containers/4e90debf-d063-49fd-9a5d-71ee43e6840a', + service: { + id: '5354aaf3-063e-47aa-9f1d-bae19755e905', + type: 'storageService', + name: 's3_storage_sample', + fullyQualifiedName: 's3_storage_sample', + displayName: 's3_storage_sample', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/services/storageServices/5354aaf3-063e-47aa-9f1d-bae19755e905', + }, + children: [ + { + id: '11e8f1c5-77c8-4a27-a546-c6561baeba18', + type: 'container', + name: 'engineering', + fullyQualifiedName: 's3_storage_sample.departments.engineering', + description: 'Bucket containing engineering department information', + displayName: 'Engineering department', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/containers/11e8f1c5-77c8-4a27-a546-c6561baeba18', + }, + { + id: 'c704e3d2-33ec-4cf0-a3fc-5e8d181c2723', + type: 'container', + name: 'finance', + fullyQualifiedName: 's3_storage_sample.departments.finance', + description: 'Bucket containing finance department information', + displayName: 'Finance department', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/containers/c704e3d2-33ec-4cf0-a3fc-5e8d181c2723', + }, + { + id: 'ffe5b6be-57cd-4cdc-9e0a-09677658160c', + type: 'container', + name: 'media', + fullyQualifiedName: 's3_storage_sample.departments.media', + description: 'Bucket containing media department information', + displayName: 'Media department', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/containers/ffe5b6be-57cd-4cdc-9e0a-09677658160c', + }, + ], + prefix: '/departments/', + numberOfObjects: 2, + size: 2048, + fileFormats: [FileFormat.CSV], + serviceType: StorageServiceType.S3, + deleted: false, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + name: i18n.t('label.frequently-joined-table-plural'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + name: i18n.t('label.table-constraints'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const containerDetailsClassBase = new ContainerDetailsClassBase(); + +export default containerDetailsClassBase; +export { ContainerDetailsClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index e86d3463a213..756bf9a70bbd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -14,10 +14,16 @@ import { TabsProps } from 'antd'; import { CommonWidgetType } from '../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../enums/entity.enum'; import { PageType, Tab } from '../../generated/system/ui/page'; +import containerDetailsClassBase from '../ContainerDetailsClassBase'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import dashboardDataModelClassBase from '../DashboardDataModelBase'; +import dashboardDetailsClassBase from '../DashboardDetailsClassBase'; +import databaseClassBase from '../Database/DatabaseClassBase'; +import databaseSchemaClassBase from '../DatabaseSchemaClassBase'; import i18n from '../i18next/LocalUtil'; +import pipelineClassBase from '../PipelineClassBase'; +import searchIndexClassBase from '../SearchIndexDetailsClassBase'; import storedProcedureClassBase from '../StoredProcedureBase'; import tableClassBase from '../TableClassBase'; import topicClassBase from '../TopicClassBase'; @@ -125,6 +131,10 @@ export const getTabLabelFromId = (tab: EntityTabs) => { return i18n.t('label.view-definition'); case EntityTabs.DBT: return i18n.t('label.dbt-lowercase'); + case EntityTabs.CHILDREN: + return i18n.t('label.children'); + case EntityTabs.DETAILS: + return i18n.t('label.detail-plural'); default: return ''; } @@ -155,6 +165,30 @@ export const getDashboardDataModelDefaultTabs = () => { return tabs; }; +export const getDatabaseDefaultTabs = () => { + return databaseClassBase.getDatabaseDetailPageTabsIds(); +}; + +export const getPipelineDefaultTabs = () => { + return pipelineClassBase.getPipelineDetailPageTabsIds(); +}; + +export const getDatabaseSchemaDefaultTabs = () => { + return databaseSchemaClassBase.getDatabaseSchemaPageTabsIds(); +}; + +export const getSearchIndexDefaultTabs = () => { + return searchIndexClassBase.getSearchIndexDetailPageTabsIds(); +}; + +export const getContainerDefaultTabs = () => { + return containerDetailsClassBase.getContainerDetailPageTabsIds(); +}; + +export const getDashboardDefaultTabs = () => { + return dashboardDetailsClassBase.getDashboardDetailPageTabsIds(); +}; + export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: @@ -170,6 +204,17 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { case PageType.DashboardDataModel: return getDashboardDataModelDefaultTabs(); case PageType.Container: + return getContainerDefaultTabs(); + case PageType.Database: + return getDatabaseDefaultTabs(); + case PageType.SearchIndex: + return getSearchIndexDefaultTabs(); + case PageType.DatabaseSchema: + return getDatabaseSchemaDefaultTabs(); + case PageType.Pipeline: + return getPipelineDefaultTabs(); + case PageType.Dashboard: + return getDashboardDefaultTabs(); default: return [ { @@ -197,6 +242,16 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return dashboardDataModelClassBase.getDefaultLayout(tab); case PageType.StoredProcedure: return storedProcedureClassBase.getDefaultLayout(tab); + case PageType.Database: + return databaseClassBase.getDefaultLayout(tab); + case PageType.DatabaseSchema: + return databaseSchemaClassBase.getDefaultLayout(tab); + case PageType.Pipeline: + return pipelineClassBase.getDefaultLayout(tab); + case PageType.SearchIndex: + return searchIndexClassBase.getDefaultLayout(tab); + case PageType.Container: + return containerDetailsClassBase.getDefaultLayout(tab); default: return []; } @@ -254,6 +309,18 @@ export const getDummyDataByPage = (pageType: PageType) => { return storedProcedureClassBase.getDummyData(); case PageType.DashboardDataModel: return dashboardDataModelClassBase.getDummyData(); + case PageType.Container: + return containerDetailsClassBase.getDummyData(); + case PageType.Database: + return databaseClassBase.getDummyData(); + case PageType.DatabaseSchema: + return databaseSchemaClassBase.getDummyData(); + case PageType.Pipeline: + return pipelineClassBase.getDummyData(); + case PageType.SearchIndex: + return searchIndexClassBase.getDummyData(); + case PageType.Dashboard: + return dashboardDetailsClassBase.getDummyData(); case PageType.LandingPage: default: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index e40fad61fcd8..aed8e629e991 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -25,7 +25,7 @@ import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel' import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; -import { getDashboardDataModelDetailPageTabs } from './DashboardDetailsUtils'; +import { getDashboardDataModelDetailPageTabs } from './DashboardDataModelUtils'; export interface DashboardDataModelDetailPageTabProps { modelComponent: JSX.Element; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx new file mode 100644 index 000000000000..563455ad0b5a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2025 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 { Card } from 'antd'; + +import React from 'react'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; +import Lineage from '../components/Lineage/Lineage.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { CSMode } from '../enums/codemirror.enum'; +import { EntityTabs, EntityType } from '../enums/entity.enum'; +import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; +import i18n from './i18next/LocalUtil'; + +export const getDashboardDataModelDetailPageTabs = ({ + modelComponent, + feedCount, + activeTab, + handleFeedCount, + editLineagePermission, + dataModelData, + dataModelPermissions, + deleted, + handelExtensionUpdate, + getEntityFeedCount, + fetchDataModel, +}: DashboardDataModelDetailPageTabProps): TabProps[] => { + return [ + { + label: ( + + ), + key: EntityTabs.MODEL, + children: modelComponent, + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + ...(dataModelData?.sql + ? [ + { + label: ( + + ), + key: EntityTabs.SQL, + children: ( + + + + ), + }, + ] + : []), + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: ( +
+ + entityDetails={dataModelData} + entityType={EntityType.DASHBOARD_DATA_MODEL} + handleExtensionUpdate={handelExtensionUpdate} + hasEditAccess={ + dataModelPermissions.EditAll || + dataModelPermissions.EditCustomFields + } + hasPermission={dataModelPermissions.ViewAll} + isVersionView={false} + /> +
+ ), + }, + ]; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts new file mode 100644 index 000000000000..8f10a27ba7ee --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2024 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 { EntityTags } from 'Models'; +import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { Tag } from '../generated/entity/classification/tag'; +import { + Dashboard, + DashboardServiceType, +} from '../generated/entity/data/dashboard'; +import { EntityReference } from '../generated/entity/type'; +import { Tab } from '../generated/system/ui/page'; +import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import { getDashboardDetailPageTabs } from './DashboardDetailsUtils'; + +export interface DashboardDetailsTabsProps { + dashboardDetails: Dashboard; + charts: EntityReference[]; + entityName: string; + editDescriptionPermission: boolean; + editTagsPermission: boolean; + editGlossaryTermsPermission: boolean; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + dashboardTags: Tag[]; + handleFeedCount: (data: FeedCounts) => void; + onDescriptionUpdate: (value: string) => Promise; + onThreadLinkSelect: (value: string) => void; + handleTagSelection: (selectedTags: EntityTags[]) => Promise; + onExtensionUpdate: (data: Dashboard) => Promise; + feedCount: FeedCounts; + activeTab: EntityTabs; + deleted: boolean; + getEntityFeedCount: () => void; + fetchDashboard: () => void; + labelMap: Record; +} + +class DashboardDetailsClassBase { + constructor() { + // Do nothing + } + + public getDashboardDetailPageTabs( + tabsProps: DashboardDetailsTabsProps + ): TabProps[] { + return getDashboardDetailPageTabs(tabsProps); + } + + public getDashboardDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.DETAILS, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.DETAILS, + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.DETAILS: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 6, + i: DetailPageWidgetKeys.CHARTS_TABLE, + w: 6, + x: 0, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): Dashboard { + return { + id: '574c383c-735f-44c8-abbb-355f87c8b19f', + name: 'customers', + displayName: 'Customers dashboard', + fullyQualifiedName: 'SampleLookerService.customers', + description: 'This is a sample Dashboard for Looker', + version: 0.1, + updatedAt: 1736493713236, + updatedBy: 'admin', + sourceUrl: 'http://localhost:808/looker/dashboard/1/', + charts: [ + { + id: '81cdc1f3-66ae-462f-bf3e-b5fbbfe7792f', + type: 'chart', + name: 'chart_1', + fullyQualifiedName: 'SampleLookerService.chart_1', + description: 'This is a sample Chart for Looker', + displayName: 'Chart 1', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/charts/81cdc1f3-66ae-462f-bf3e-b5fbbfe7792f', + }, + { + id: '6f5057aa-8d7c-41a7-ab93-76bf8ed2bc27', + type: 'chart', + name: 'chart_2', + fullyQualifiedName: 'SampleLookerService.chart_2', + description: 'This is a sample Chart for Looker', + displayName: 'Chart 2', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/charts/6f5057aa-8d7c-41a7-ab93-76bf8ed2bc27', + }, + ], + href: 'http://test-argo.getcollate.io/api/v1/dashboards/574c383c-735f-44c8-abbb-355f87c8b19f', + owners: [], + followers: [], + tags: [], + service: { + id: 'fb4df3ed-75b9-45d3-a2df-da07785893d7', + type: 'dashboardService', + name: 'SampleLookerService', + fullyQualifiedName: 'SampleLookerService', + displayName: 'SampleLookerService', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/services/dashboardServices/fb4df3ed-75b9-45d3-a2df-da07785893d7', + }, + serviceType: DashboardServiceType.Looker, + usageSummary: { + dailyStats: { + count: 0, + percentileRank: 0, + }, + weeklyStats: { + count: 0, + percentileRank: 0, + }, + monthlyStats: { + count: 0, + percentileRank: 0, + }, + date: new Date('2025-02-03'), + }, + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const dashboardDetailsClassBase = new DashboardDetailsClassBase(); + +export default dashboardDetailsClassBase; +export { DashboardDetailsClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 6c8a4a5e56a5..71b0509c11ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -11,26 +11,30 @@ * limitations under the License. */ -import { Card } from 'antd'; +import { Col, Row } from 'antd'; import { AxiosError } from 'axios'; +import { t } from 'i18next'; +import { isEmpty } from 'lodash'; import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; +import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; -import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; +import { DashboardChartTable } from '../components/Dashboard/DashboardChartTable/DashboardChartTable'; +import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; -import { CSMode } from '../enums/codemirror.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Dashboard } from '../generated/entity/data/dashboard'; import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { getChartById } from '../rest/chartAPI'; import { sortTagsCaseInsensitive } from './CommonUtils'; -import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; -import i18next from './i18next/LocalUtil'; +import { DashboardDetailsTabsProps } from './DashboardDetailsClassBase'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.DOMAIN},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; @@ -67,7 +71,7 @@ export const fetchCharts = async (charts: Dashboard['charts']) => { export const getDashboardDetailsPageDefaultLayout = (tab: EntityTabs) => { switch (tab) { - case EntityTabs.SCHEMA: + case EntityTabs.DETAILS: return [ { h: 2, @@ -77,14 +81,6 @@ export const getDashboardDetailsPageDefaultLayout = (tab: EntityTabs) => { y: 0, static: false, }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, { h: 1, i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, @@ -132,99 +128,93 @@ export const getDashboardDetailsPageDefaultLayout = (tab: EntityTabs) => { } }; -export const getDashboardDataModelDetailsPageDefaultLayout = ( - tab: EntityTabs -) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - -export const getDashboardDataModelDetailPageTabs = ({ - modelComponent, +export const getDashboardDetailPageTabs = ({ + dashboardDetails, + charts, + entityName, + editDescriptionPermission, + editTagsPermission, + editGlossaryTermsPermission, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + dashboardTags, + handleFeedCount, + onDescriptionUpdate, + onThreadLinkSelect, + handleTagSelection, + onExtensionUpdate, feedCount, activeTab, - handleFeedCount, - editLineagePermission, - dataModelData, - dataModelPermissions, deleted, - handelExtensionUpdate, getEntityFeedCount, - fetchDataModel, -}: DashboardDataModelDetailPageTabProps): TabProps[] => { + fetchDashboard, +}: DashboardDetailsTabsProps): TabProps[] => { return [ { label: ( - + + ), + key: EntityTabs.DETAILS, + children: ( + +
+ + + + + + ), + ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, + }} + secondPanel={{ + children: ( +
+ + customProperties={dashboardDetails} + dataProducts={dashboardDetails?.dataProducts ?? []} + domain={dashboardDetails?.domain} + editCustomAttributePermission={ + editCustomAttributePermission + } + editGlossaryTermsPermission={editGlossaryTermsPermission} + editTagPermission={editTagsPermission} + entityFQN={dashboardDetails.fullyQualifiedName ?? ''} + entityId={dashboardDetails.id} + entityType={EntityType.DASHBOARD} + selectedTags={dashboardTags} + viewAllPermission={viewAllPermission} + onExtensionUpdate={onExtensionUpdate} + onTagSelectionChange={handleTagSelection} + onThreadLinkSelect={onThreadLinkSelect} + /> +
+ ), + ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, + className: + 'entity-resizable-right-panel-container entity-resizable-panel-container', + }} + /> + + ), - key: EntityTabs.MODEL, - children: modelComponent, }, { label: ( @@ -232,7 +222,7 @@ export const getDashboardDataModelDetailPageTabs = ({ count={feedCount.totalCount} id={EntityTabs.ACTIVITY_FEED} isActive={activeTab === EntityTabs.ACTIVITY_FEED} - name={i18next.t('label.activity-feed-and-task-plural')} + name={t('label.activity-feed-and-task-plural')} /> ), key: EntityTabs.ACTIVITY_FEED, @@ -240,56 +230,23 @@ export const getDashboardDataModelDetailPageTabs = ({ ), }, - ...(dataModelData?.sql - ? [ - { - label: ( - - ), - key: EntityTabs.SQL, - children: ( - - - - ), - }, - ] - : []), { - label: ( - - ), + label: , key: EntityTabs.LINEAGE, children: ( @@ -299,22 +256,18 @@ export const getDashboardDataModelDetailPageTabs = ({ label: ( ), key: EntityTabs.CUSTOM_PROPERTIES, - children: ( -
- - entityDetails={dataModelData} - entityType={EntityType.DASHBOARD_DATA_MODEL} - handleExtensionUpdate={handelExtensionUpdate} - hasEditAccess={ - dataModelPermissions.EditAll || - dataModelPermissions.EditCustomFields - } - hasPermission={dataModelPermissions.ViewAll} - isVersionView={false} + children: dashboardDetails && ( +
+ + entityDetails={dashboardDetails} + entityType={EntityType.DASHBOARD} + handleExtensionUpdate={onExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} />
), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 884e7cbf134f..4487af6c1d9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -10,18 +10,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Typography } from 'antd'; +import { Col, Row, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { t } from 'i18next'; -import { isUndefined, toLower } from 'lodash'; +import { isEmpty, isUndefined, toLower } from 'lodash'; import React from 'react'; import { Link } from 'react-router-dom'; +import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component'; +import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import RichTextEditorPreviewerV1 from '../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { DatabaseSchemaTable } from '../../components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; +import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; import { getEntityDetailsPath, NO_DATA_PLACEHOLDER, } from '../../constants/constants'; +import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs, @@ -33,6 +42,7 @@ import { EntityReference } from '../../generated/entity/type'; import { UsageDetails } from '../../generated/type/entityUsage'; import { getEntityName } from '../EntityUtils'; import { getUsagePercentile } from '../TableUtils'; +import { DatabaseDetailPageTabProps } from './DatabaseClassBase'; export const getQueryFilterForDatabase = ( serviceType: string, @@ -187,3 +197,145 @@ export const getDatabaseDetailsPageDefaultLayout = (tab: EntityTabs) => { return []; } }; + +export const getDatabasePageBaseTabs = ({ + activeTab, + database, + description, + decodedDatabaseFQN, + editDescriptionPermission, + editGlossaryTermsPermission, + editTagsPermission, + viewAllPermission, + tags, + schemaInstanceCount, + feedCount, + handleFeedCount, + getEntityFeedCount, + onDescriptionUpdate, + onThreadLinkSelect, + handleTagSelection, + settingsUpdateHandler, + deleted, + editCustomAttributePermission, + getDetailsByFQN, +}: DatabaseDetailPageTabProps): TabProps[] => { + return [ + { + label: ( + + ), + key: EntityTabs.SCHEMA, + children: ( + +
+ + + + + + + + + + + ), + ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, + }} + secondPanel={{ + children: ( +
+ + customProperties={database} + dataProducts={database?.dataProducts ?? []} + domain={database?.domain} + editCustomAttributePermission={ + editCustomAttributePermission + } + editGlossaryTermsPermission={editGlossaryTermsPermission} + editTagPermission={editTagsPermission} + entityFQN={decodedDatabaseFQN} + entityId={database?.id ?? ''} + entityType={EntityType.DATABASE} + selectedTags={tags} + viewAllPermission={viewAllPermission} + onExtensionUpdate={settingsUpdateHandler} + onTagSelectionChange={handleTagSelection} + onThreadLinkSelect={onThreadLinkSelect} + /> +
+ ), + ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, + className: + 'entity-resizable-right-panel-container entity-resizable-panel-container', + }} + /> + + + ), + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: database && ( +
+ + entityDetails={database} + entityType={EntityType.DATABASE} + handleExtensionUpdate={settingsUpdateHandler} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + isVersionView={false} + /> +
+ ), + }, + ]; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts new file mode 100644 index 000000000000..b49ba3ab0286 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -0,0 +1,295 @@ +/* + * Copyright 2024 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 { EntityTags } from 'Models'; +import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { + Database, + DatabaseServiceType, + State, +} from '../../generated/entity/data/database'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { LabelType, TagSource } from '../../generated/type/tagLabel'; +import { FeedCounts } from '../../interface/feed.interface'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; +import { getDatabasePageBaseTabs } from './Database.util'; + +export interface DatabaseDetailPageTabProps { + activeTab: EntityTabs; + database: Database; + description: string; + decodedDatabaseFQN: string; + editDescriptionPermission: boolean; + editGlossaryTermsPermission: boolean; + editTagsPermission: boolean; + viewAllPermission: boolean; + tags: EntityTags[]; + schemaInstanceCount: number; + feedCount: FeedCounts; + handleFeedCount: (data: FeedCounts) => void; + getEntityFeedCount: () => void; + onDescriptionUpdate: (updatedHTML: string) => Promise; + onThreadLinkSelect: (link: string) => void; + handleTagSelection: (selectedTags: EntityTags[]) => Promise; + settingsUpdateHandler: ( + data: Database, + key?: keyof Database + ) => Promise; + deleted: boolean; + editCustomAttributePermission: boolean; + getDetailsByFQN: () => void; + labelMap?: Record; +} + +class DatabaseClassBase { + public getDatabaseDetailPageTabs( + tabProps: DatabaseDetailPageTabProps + ): TabProps[] { + return getDatabasePageBaseTabs(tabProps); + } + + public getDatabaseDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.SCHEMA, + EntityTabs.ACTIVITY_FEED, + EntityTabs.SAMPLE_DATA, + EntityTabs.TABLE_QUERIES, + EntityTabs.PROFILER, + EntityTabs.INCIDENTS, + EntityTabs.LINEAGE, + EntityTabs.VIEW_DEFINITION, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [ + EntityTabs.SCHEMA, + EntityTabs.OVERVIEW, + EntityTabs.TERMS, + ].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.DATABASE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): Database { + return { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + displayName: '', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + dataProducts: [], + tags: [ + { + tagFQN: 'KnowledgeCenter.QuickLink', + name: 'QuickLink', + description: 'Knowledge Quick Link.', + style: { + color: '#c415d1', + }, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ], + version: 1.2, + updatedAt: 1736405710107, + updatedBy: 'prajwal.p', + href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', + }, + { + id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + type: 'user', + name: 'ayush', + fullyQualifiedName: 'ayush', + displayName: 'Ayush Shah', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + }, + { + id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', + type: 'user', + name: 'ayush02shah12', + fullyQualifiedName: 'ayush02shah12', + displayName: 'Ayush Shah', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', + }, + { + id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', + type: 'user', + name: 'prajwal161998', + fullyQualifiedName: 'prajwal161998', + displayName: 'prajwal161998', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', + }, + ], + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', + }, + serviceType: DatabaseServiceType.BigQuery, + changeDescription: { + fieldsAdded: [ + { + name: 'owners', + newValue: + '[{"id":"f7971c49-bca7-48fb-bb1a-821a1e2c5802","type":"user","name":"prajwal161998","fullyQualifiedName":"prajwal161998","displayName":"prajwal161998","deleted":false}]', + }, + ], + fieldsUpdated: [], + fieldsDeleted: [], + previousVersion: 1.1, + }, + default: false, + deleted: false, + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + name: i18n.t('label.frequently-joined-table-plural'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + name: i18n.t('label.table-constraints'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const databaseClassBase = new DatabaseClassBase(); + +export default databaseClassBase; +export { DatabaseClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 5223f5697bf2..af92cc9d8887 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -16,11 +16,18 @@ import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; +import { + DatabaseSchema, + DatabaseServiceType, + State, +} from '../generated/entity/data/databaseSchema'; import { Table } from '../generated/entity/data/table'; import { ThreadType } from '../generated/entity/feed/thread'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { LabelType, TagSource } from '../generated/type/tagLabel'; import { UsePagingInterface } from '../hooks/paging/usePaging'; import { FeedCounts } from '../interface/feed.interface'; +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import { getDataBaseSchemaPageBaseTabs } from './DatabaseSchemaDetailsUtils'; export interface DatabaseSchemaPageTabProps { @@ -66,12 +73,77 @@ class DatabaseSchemaClassBase { return getDataBaseSchemaPageBaseTabs(databaseSchemaTabData); } - public getDatabaseSchemaPageTabsIds(): EntityTabs[] { + public getDatabaseSchemaPageTabsIds(): Tab[] { return [ EntityTabs.SCHEMA, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, - ]; + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [EntityTabs.SCHEMA].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.DATABASE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } } public getDatabaseSchemaPageDefaultLayout = (tab: EntityTabs) => { @@ -140,6 +212,131 @@ class DatabaseSchemaClassBase { return []; } }; + + public getDummyData(): DatabaseSchema { + return { + id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + dataProducts: [], + version: 1.1, + updatedAt: 1736405774154, + updatedBy: 'prajwal.p', + href: 'http://sandbox-beta.open-metadata.org/api/v1/databaseSchemas/9f127bdc-d060-4fac-ae7b-c635933fc2e0', + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', + }, + { + id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + type: 'user', + name: 'ayush', + fullyQualifiedName: 'ayush', + displayName: 'Ayush Shah', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', + }, + { + id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', + type: 'user', + name: 'ayush02shah12', + fullyQualifiedName: 'ayush02shah12', + displayName: 'Ayush Shah', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', + }, + { + id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', + type: 'user', + name: 'prajwal161998', + fullyQualifiedName: 'prajwal161998', + displayName: 'prajwal161998', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', + }, + ], + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', + }, + serviceType: DatabaseServiceType.BigQuery, + database: { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + displayName: 'ecommerce_db', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', + }, + usageSummary: { + dailyStats: { + count: 21, + percentileRank: 0, + }, + weeklyStats: { + count: 21, + percentileRank: 0, + }, + monthlyStats: { + count: 21, + percentileRank: 0, + }, + date: new Date('2023-11-10'), + }, + tags: [ + { + tagFQN: 'PersonalData.Personal', + name: 'Personal', + description: + 'Data that can be used to directly or indirectly identify a person.', + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ], + deleted: false, + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + }, + votes: { + upVotes: 0, + downVotes: 1, + upVoters: [], + downVoters: [ + { + id: 'f14f17bf-0923-4234-8e73-2dcc051f2adc', + type: 'user', + name: 'admin', + fullyQualifiedName: 'admin', + displayName: 'admin', + deleted: false, + }, + ], + }, + }; + } } const databaseSchemaClassBase = new DatabaseSchemaClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index 858fa95834c9..2d48f016794b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -38,7 +38,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ databaseSchema, description, editDescriptionPermission, - isEdit, showDeletedTables, tableDataLoading, editGlossaryTermsPermission, @@ -48,12 +47,10 @@ export const getDataBaseSchemaPageBaseTabs = ({ tags, viewAllPermission, storedProcedureCount, - onEditCancel, handleExtensionUpdate, handleTagSelection, onThreadLinkSelect, tablePaginationHandler, - onDescriptionEdit, onDescriptionUpdate, handleShowDeletedTables, getEntityFeedCount, @@ -85,14 +82,11 @@ export const getDataBaseSchemaPageBaseTabs = ({ databaseSchemaDetails={databaseSchema} description={description} editDescriptionPermission={editDescriptionPermission} - isEdit={isEdit} pagingInfo={pagingInfo} showDeletedTables={showDeletedTables} tableData={tableData} tableDataLoading={tableDataLoading} tablePaginationHandler={tablePaginationHandler} - onCancel={onEditCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onShowDeletedTablesChange={handleShowDeletedTables} onThreadLinkSelect={onThreadLinkSelect} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts new file mode 100644 index 000000000000..1839ce992624 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -0,0 +1,303 @@ +/* + * Copyright 2024 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 { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { + Pipeline, + PipelineServiceType, + State, + StatusType, +} from '../generated/entity/data/pipeline'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { LabelType, TagSource } from '../generated/type/tagLabel'; + +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import i18n from './i18next/LocalUtil'; + +export interface PipelineDetailPageTabProps { + labelMap?: Record; +} + +class PipelineClassBase { + public getPipelineDetailPageTabs( + _tabProps: PipelineDetailPageTabProps + ): TabProps[] { + return []; + // getDatabaseDetailPageBaseTabs(tableDetailsPageProps); + } + + public getPipelineDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.SCHEMA, + EntityTabs.ACTIVITY_FEED, + EntityTabs.SAMPLE_DATA, + EntityTabs.TABLE_QUERIES, + EntityTabs.PROFILER, + EntityTabs.INCIDENTS, + EntityTabs.LINEAGE, + EntityTabs.VIEW_DEFINITION, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [ + EntityTabs.SCHEMA, + EntityTabs.OVERVIEW, + EntityTabs.TERMS, + ].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.DATABASE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): Pipeline { + return { + id: '60670fe2-eb0a-41a2-a683-d32964e7e61b', + name: 'dim_address_etl', + displayName: 'dim_address etl', + fullyQualifiedName: 'sample_airflow.dim_address_etl', + description: 'dim_address ETL pipeline', + dataProducts: [], + version: 0.2, + updatedAt: 1726204840178, + updatedBy: 'ashish', + sourceUrl: 'http://localhost:8080/tree?dag_id=dim_address_etl', + tasks: [ + { + name: 'dim_address_task', + displayName: 'dim_address Task', + fullyQualifiedName: 'sample_airflow.dim_address_etl.dim_address_task', + description: + 'Airflow operator to perform ETL and generate dim_address table', + sourceUrl: + 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=dim_address_task', + downstreamTasks: ['assert_table_exists'], + taskType: 'PrestoOperator', + tags: [ + { + tagFQN: '"collection.new"."I.have.dots"', + name: 'I.have.dots', + displayName: '', + description: 'asd', + source: TagSource.Glossary, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + { + tagFQN: 'test Classification.test tag 1', + name: 'test tag 1', + displayName: '', + description: 'test tag 1', + style: { + color: '#da1010', + iconURL: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJUAAACUCAMAAACtIJvYAAAAA1BMVEX/AAAZ4gk3AAAALElEQVR4nO3BMQEAAADCoPVP7WkJoAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAGVrgAAQdwvSEAAAAASUVORK5CYII=', + }, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ], + }, + { + name: 'assert_table_exists', + displayName: 'Assert Table Exists', + fullyQualifiedName: + 'sample_airflow.dim_address_etl.assert_table_exists', + description: 'Assert if a table exists', + sourceUrl: + 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists', + downstreamTasks: [], + taskType: 'HiveOperator', + tags: [], + }, + ], + pipelineStatus: { + timestamp: 1723014798482, + executionStatus: StatusType.Pending, + taskStatus: [ + { + name: 'dim_address_task', + executionStatus: StatusType.Pending, + }, + { + name: 'assert_table_exists', + executionStatus: StatusType.Pending, + }, + ], + }, + followers: [ + { + id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + type: 'user', + name: 'ashish', + fullyQualifiedName: 'ashish', + description: + '

data driven car

Hybrid Modal

', + displayName: 'Ashish', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/users/e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + }, + ], + tags: [ + { + tagFQN: 'Tier.Tier1', + name: 'Tier1', + displayName: 'Priority 1', + description: + // eslint-disable-next-line max-len + '**Critical Source of Truth business data assets of an organization**\n\n- Used in critical metrics and dashboards to drive business and product decisions\n\n- Used in critical compliance reporting to regulators, govt entities, and third party\n\n- Used in brand or revenue impacting online user-facing experiences (search results, advertisement, promotions, and experimentation)\n\n- Other high impact use, such as ML models and fraud detection\n\n- Source used to derive other critical Tier-1 datasets', + style: {}, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ], + href: 'http://test-argo.getcollate.io/api/v1/pipelines/60670fe2-eb0a-41a2-a683-d32964e7e61b', + owners: [], + service: { + id: 'f83c2b6e-0d09-4839-98df-5571cc17c829', + type: 'pipelineService', + name: 'sample_airflow', + fullyQualifiedName: 'sample_airflow', + displayName: 'sample_airflow', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/services/pipelineServices/f83c2b6e-0d09-4839-98df-5571cc17c829', + }, + serviceType: PipelineServiceType.Airflow, + deleted: false, + scheduleInterval: '5 * * * *', + domain: { + id: '6b440596-144b-417b-b7ee-95cf0b0d7de4', + type: 'domain', + name: 'Version -1.6.2 Domain', + fullyQualifiedName: '"Version -1.6.2 Domain"', + description: '

Version -1.6.2 Domain

', + displayName: 'Version -1.6.2 Domain', + inherited: true, + href: 'http://test-argo.getcollate.io/api/v1/domains/6b440596-144b-417b-b7ee-95cf0b0d7de4', + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + name: i18n.t('label.frequently-joined-table-plural'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + name: i18n.t('label.table-constraints'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const pipelineClassBase = new PipelineClassBase(); + +export default pipelineClassBase; +export { PipelineClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts new file mode 100644 index 000000000000..ba293c730fd2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -0,0 +1,347 @@ +/* + * Copyright 2024 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 { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../enums/entity.enum'; +import { + DataType, + IndexType, + SearchIndex, + SearchServiceType, + State, +} from '../generated/entity/data/searchIndex'; +import { Tab } from '../generated/system/ui/uiCustomization'; +import { LabelType, TagSource } from '../generated/type/tagLabel'; + +import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; +import i18n from './i18next/LocalUtil'; + +export interface SearchIndexDetailPageTabProps { + labelMap?: Record; +} + +class SearchIndexClassBase { + public getSearchIndexDetailPageTabs( + _tabProps: SearchIndexDetailPageTabProps + ): TabProps[] { + return []; + } + + public getSearchIndexDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.SCHEMA, + EntityTabs.ACTIVITY_FEED, + EntityTabs.SAMPLE_DATA, + EntityTabs.TABLE_QUERIES, + EntityTabs.PROFILER, + EntityTabs.INCIDENTS, + EntityTabs.LINEAGE, + EntityTabs.VIEW_DEFINITION, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: [EntityTabs.SCHEMA].includes(tab), + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.DATABASE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): SearchIndex { + return { + id: '2de6c0f1-c85a-4c90-b74b-b40869cc446c', + name: 'table_search_index', + fullyQualifiedName: 'elasticsearch_sample.table_search_index', + displayName: 'TableSearchIndex', + description: 'Table Search Index', + version: 0.2, + updatedAt: 1726204919320, + updatedBy: 'ashish', + service: { + id: 'dde0ee41-f45f-4cd4-8826-07cd298905f2', + type: 'searchService', + name: 'elasticsearch_sample', + fullyQualifiedName: 'elasticsearch_sample', + displayName: 'elasticsearch_sample', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/services/storageServices/dde0ee41-f45f-4cd4-8826-07cd298905f2', + }, + serviceType: SearchServiceType.ElasticSearch, + fields: [ + { + name: 'name', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity Name.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.name', + tags: [], + }, + { + name: 'displayName', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity DisplayName.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.displayName', + tags: [], + }, + { + name: 'description', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity Description.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.description', + tags: [], + }, + { + name: 'columns', + dataType: DataType.Nested, + dataTypeDisplay: 'nested', + description: 'Table Columns.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.columns', + tags: [], + children: [ + { + name: 'name', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column Name.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.name', + tags: [], + }, + { + name: 'displayName', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column DisplayName.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.displayName', + tags: [], + }, + { + name: 'description', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column Description.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.description', + tags: [], + }, + ], + }, + { + name: 'databaseSchema', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Database Schema that this table belongs to.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.databaseSchema', + tags: [], + }, + ], + indexType: IndexType.Index, + owners: [], + followers: [ + { + id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + type: 'user', + name: 'ashish', + fullyQualifiedName: 'ashish', + description: + '

data driven car

Hybrid Modal

', + displayName: 'Ashish', + deleted: false, + href: 'http://test-argo.getcollate.io/api/v1/users/e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + }, + ], + tags: [ + { + tagFQN: '"Testing%Version@1.56"."156-Term.3"', + name: '156-Term.3', + displayName: '156-Term.3', + description: '156-Term.3', + source: TagSource.Glossary, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + { + tagFQN: '"classify-1.6.2".tagtest-1', + name: 'tagtest-1', + displayName: 'tagtest-1', + description: '

tagtest-1 for.1.6.2

', + style: {}, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + { + tagFQN: 'Data Center.center-1', + name: 'center-1', + displayName: '', + description: 'this is center-1', + style: {}, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + { + tagFQN: 'Tier.Tier5', + name: 'Tier5', + displayName: 'Priority 5', + description: + // eslint-disable-next-line max-len + '**Private/Unused data assets - No impact beyond individual users**\n\n- Data assets without any ownership with no usage in the last 60 days\n\n- Data assets owned by individuals without team ownership\n\n', + style: {}, + source: TagSource.Classification, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ], + href: 'http://test-argo.getcollate.io/api/v1/searchIndexes/2de6c0f1-c85a-4c90-b74b-b40869cc446c', + + deleted: false, + domain: { + id: 'a440b3a9-fbbf-464d-91d4-c9cfeeccc8e4', + type: 'domain', + name: 'Domain 1.6.0', + fullyQualifiedName: '"Domain 1.6.0"', + description: 'csacasc', + displayName: 'Domain 1.6.0', + href: 'http://test-argo.getcollate.io/api/v1/domains/a440b3a9-fbbf-464d-91d4-c9cfeeccc8e4', + }, + dataProducts: [ + { + id: '8c046cc5-ab23-40c0-a0bf-b58d206290f7', + type: 'dataProduct', + name: 'TestDataProduct', + fullyQualifiedName: 'TestDataProduct', + description: 'asd', + displayName: 'TestDataProduct', + href: 'http://test-argo.getcollate.io/api/v1/dataProducts/8c046cc5-ab23-40c0-a0bf-b58d206290f7', + }, + ], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.schema'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + name: i18n.t('label.frequently-joined-table-plural'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + name: i18n.t('label.table-constraints'), + data: { + gridSizes: ['small'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } +} + +const searchIndexClassBase = new SearchIndexClassBase(); + +export default searchIndexClassBase; +export { SearchIndexClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 233ec93200a8..187760e2b0f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -107,12 +107,9 @@ export const getStoredProcedureDetailsPageTabs = ({ decodedStoredProcedureFQN, entityName, code, - isEdit, deleted, owners, editDescriptionPermission, - onCancel, - onDescriptionEdit, onDescriptionUpdate, onThreadLinkSelect, storedProcedure, @@ -153,11 +150,8 @@ export const getStoredProcedureDetailsPageTabs = ({ entityType={EntityType.STORED_PROCEDURE} hasEditAccess={editDescriptionPermission} isDescriptionExpanded={isEmpty(code)} - isEdit={isEdit} owner={owners} showActions={!deleted} - onCancel={onCancel} - onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} onThreadLinkSelect={onThreadLinkSelect} /> From 196b78d92d992c9dcb90fb8b7537266f48cc1984 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 4 Feb 2025 21:47:25 +0530 Subject: [PATCH 12/63] support pipeline with customize page --- .../PipelineDetails.component.tsx | 291 +++++---------- .../ui/src/enums/CustomizeDetailPage.enum.ts | 1 + .../ui/src/utils/PipelineClassBase.ts | 49 ++- .../ui/src/utils/PipelineDetailsUtils.ts | 132 ------- .../ui/src/utils/PipelineDetailsUtils.tsx | 343 ++++++++++++++++++ 5 files changed, 475 insertions(+), 341 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index f3241357bfd8..0953fd8b0ad3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -13,8 +13,8 @@ import { FilterOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; -import { Card, Col, Radio, Row, Tabs, Typography } from 'antd'; -import Table, { ColumnsType } from 'antd/lib/table'; +import { Card, Col, Row, Tabs, Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; @@ -23,6 +23,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useHistory, useParams } from 'react-router-dom'; import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { DATA_ASSET_ICON_DIMENSION, getEntityDetailsPath, @@ -30,8 +31,6 @@ import { } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { PIPELINE_TASK_TABS } from '../../../constants/pipeline.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; @@ -44,15 +43,23 @@ import { Task, } from '../../../generated/entity/data/pipeline'; import { ThreadType } from '../../../generated/entity/feed/thread'; +import { Page } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/uiCustomization'; import { TagSource } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { FeedCounts } from '../../../interface/feed.interface'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { postThread } from '../../../rest/feedsAPI'; import { restorePipeline } from '../../../rest/pipelineAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import pipelineClassBase from '../../../utils/PipelineClassBase'; import { getAllTags, searchTagInData, @@ -61,25 +68,16 @@ import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../SearchedData/SearchedData.interface'; -import ExecutionsTab from '../Execution/Execution.component'; import TasksDAGView from '../TasksDAGView/TasksDAGView'; import './pipeline-details.style.less'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; @@ -102,7 +100,7 @@ const PipelineDetails = ({ const history = useHistory(); const { tab } = useParams<{ tab: EntityTabs }>(); const { t } = useTranslation(); - const { currentUser, theme } = useApplicationStore(); + const { currentUser, theme, selectedPersona } = useApplicationStore(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const userID = currentUser?.id ?? ''; const { @@ -130,7 +128,7 @@ const PipelineDetails = ({ }, [pipelineDetails]); // local state variables - + const [customizedPage, setCustomizedPage] = useState(null); const [editTask, setEditTask] = useState<{ task: Task; index: number; @@ -580,200 +578,89 @@ const PipelineDetails = ({ [pipelineDetails, selectedExecution] ); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.TASKS, - children: ( - -
- - - - - - - setActiveTab(e.target.value)} - /> - - - {activeTab === PIPELINE_TASK_TABS.LIST_VIEW ? ( -
- ) : ( - tasksDAGView - )} - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={pipelineDetails} - dataProducts={pipelineDetails?.dataProducts ?? []} - domain={pipelineDetails?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityFQN={pipelineFQN} - entityId={pipelineDetails.id} - entityType={EntityType.PIPELINE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - { - label: ( - - ), - key: EntityTabs.EXECUTIONS, - children: ( - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: pipelineDetails && ( -
- - entityDetails={pipelineDetails} - entityType={EntityType.PIPELINE} - handleExtensionUpdate={onExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ - description, + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = pipelineClassBase.getPipelineDetailPageTabs({ + description: description ?? '', activeTab, - feedCount.totalCount, - deleted, - owners, - entityName, - pipelineFQN, - pipelineDetails, + feedCount: { + totalCount: feedCount.totalCount, + }, + deleted: deleted ?? false, + owners: owners ?? [], + entityName: entityName ?? '', + pipelineFQN: pipelineFQN ?? '', + pipelineDetails: pipelineDetails ?? {}, tasksDAGView, - taskColumns, - tasksInternal, + taskColumns: taskColumns ?? [], + tasksInternal: tasksInternal ?? [], handleFeedCount, handleTagSelection, onExtensionUpdate, onDescriptionUpdate, onThreadLinkSelect, - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, - editLineagePermission, - editCustomAttributePermission, - viewAllPermission, - ] - ); + editDescriptionPermission: editDescriptionPermission ?? false, + editTagsPermission: editTagsPermission ?? false, + editGlossaryTermsPermission: editGlossaryTermsPermission ?? false, + editLineagePermission: editLineagePermission ?? false, + editCustomAttributePermission: editCustomAttributePermission ?? false, + viewAllPermission: viewAllPermission ?? false, + tab: tab ?? EntityTabs.TASKS, + getEntityFeedCount, + tags, + setActiveTab, + fetchPipeline, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.TASKS + ); + }, [ + description, + activeTab, + feedCount.totalCount, + deleted, + owners, + entityName, + pipelineFQN, + pipelineDetails, + tasksDAGView, + taskColumns, + tasksInternal, + handleFeedCount, + handleTagSelection, + onExtensionUpdate, + onDescriptionUpdate, + onThreadLinkSelect, + editDescriptionPermission, + editTagsPermission, + editGlossaryTermsPermission, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + ]); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Pipeline) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); useEffect(() => { getEntityFeedCount(); diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 2d8c9aa8ee93..1e4970f6498c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -37,6 +37,7 @@ export enum DetailPageWidgetKeys { STORED_PROCEDURE_CODE = 'KnowledgePanel.StoredProcedureCode', DATA_MODEL = 'KnowledgePanel.DataModel', CONTAINER_CHILDREN = 'KnowledgePanel.ContainerChildren', + PIPELINE_TASKS = 'KnowledgePanel.PipelineTasks', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index 1839ce992624..65aa6edf67b2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { ColumnType } from 'antd/lib/table'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -20,30 +21,64 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; +import { Tag } from '../generated/entity/classification/tag'; import { Pipeline, PipelineServiceType, State, StatusType, + Task, } from '../generated/entity/data/pipeline'; +import { ThreadType } from '../generated/entity/feed/thread'; +import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { LabelType, TagSource } from '../generated/type/tagLabel'; - +import { LabelType, TagLabel, TagSource } from '../generated/type/tagLabel'; +import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; +import { getPipelineDetailPageTabs } from './PipelineDetailsUtils'; export interface PipelineDetailPageTabProps { + description: string; + entityName: string; + feedCount: { + totalCount: number; + }; + editDescriptionPermission: boolean; + editGlossaryTermsPermission: boolean; + editTagsPermission: boolean; + getEntityFeedCount: () => void; + handleFeedCount: (data: FeedCounts) => void; + handleTagSelection: (selectedTags: TagLabel[]) => Promise; + onDescriptionUpdate: (value: string) => Promise; + onExtensionUpdate: (updatedPipeline: Pipeline) => Promise; + onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; + pipelineDetails: Pipeline; + pipelineFQN: string; + tasksInternal: Task[]; + tasksDAGView: JSX.Element; + tags: Tag[]; + viewAllPermission: boolean; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + deleted: boolean; + activeTab: PIPELINE_TASK_TABS; + tab: EntityTabs; + setActiveTab: (tab: PIPELINE_TASK_TABS) => void; + taskColumns: ColumnType[]; + owners: EntityReference[]; + fetchPipeline: () => void; labelMap?: Record; } class PipelineClassBase { public getPipelineDetailPageTabs( - _tabProps: PipelineDetailPageTabProps + tabProps: PipelineDetailPageTabProps ): TabProps[] { - return []; - // getDatabaseDetailPageBaseTabs(tableDetailsPageProps); + return getPipelineDetailPageTabs(tabProps); } public getPipelineDetailPageTabsIds(): Tab[] { @@ -72,7 +107,7 @@ class PipelineClassBase { public getDefaultLayout(tab: EntityTabs) { switch (tab) { - case EntityTabs.SCHEMA: + case EntityTabs.TASKS: return [ { h: 2, @@ -84,7 +119,7 @@ class PipelineClassBase { }, { h: 11, - i: DetailPageWidgetKeys.DATABASE_SCHEMA, + i: DetailPageWidgetKeys.PIPELINE_TASKS, w: 6, x: 0, y: 0, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts deleted file mode 100644 index 93006ab3c062..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2022 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 { isUndefined } from 'lodash'; -import { ReactComponent as IconFailBadge } from '../assets/svg/fail-badge.svg'; -import { ReactComponent as IconSkippedBadge } from '../assets/svg/skipped-badge.svg'; -import { ReactComponent as IconSuccessBadge } from '../assets/svg/success-badge.svg'; -import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; -import { - Pipeline, - StatusType, - TaskStatus, -} from '../generated/entity/data/pipeline'; -import { sortTagsCaseInsensitive } from './CommonUtils'; - -// eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.TASKS}, ${TabSpecificField.PIPELINE_STATUS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; - -export const getTaskExecStatus = (taskName: string, tasks: TaskStatus[]) => { - return tasks.find((task) => task.name === taskName)?.executionStatus || ''; -}; - -export const getStatusBadgeIcon = (status?: StatusType) => { - switch (status) { - case StatusType.Successful: - return IconSuccessBadge; - - case StatusType.Failed: - return IconFailBadge; - - default: - return IconSkippedBadge; - } -}; - -export const getFormattedPipelineDetails = ( - pipelineDetails: Pipeline -): Pipeline => { - if (pipelineDetails.tasks) { - const updatedTasks = pipelineDetails.tasks.map((task) => ({ - ...task, - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - tags: isUndefined(task.tags) - ? undefined - : sortTagsCaseInsensitive(task.tags), - })); - - return { ...pipelineDetails, tasks: updatedTasks }; - } else { - return pipelineDetails; - } -}; - -export const getPipelineDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx new file mode 100644 index 000000000000..d943ecb4cd15 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -0,0 +1,343 @@ +/* + * Copyright 2022 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 { Col, Radio, Row, Table } from 'antd'; +import { t } from 'i18next'; +import { isEmpty, isUndefined } from 'lodash'; +import React from 'react'; +import { ReactComponent as IconFailBadge } from '../assets/svg/fail-badge.svg'; +import { ReactComponent as IconSkippedBadge } from '../assets/svg/skipped-badge.svg'; +import { ReactComponent as IconSuccessBadge } from '../assets/svg/success-badge.svg'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; +import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; +import Lineage from '../components/Lineage/Lineage.component'; +import ExecutionsTab from '../components/Pipeline/Execution/Execution.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; +import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { + Pipeline, + StatusType, + TaskStatus, +} from '../generated/entity/data/pipeline'; +import { sortTagsCaseInsensitive } from './CommonUtils'; +import { PipelineDetailPageTabProps } from './PipelineClassBase'; + +// eslint-disable-next-line max-len +export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.TASKS}, ${TabSpecificField.PIPELINE_STATUS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; + +export const getTaskExecStatus = (taskName: string, tasks: TaskStatus[]) => { + return tasks.find((task) => task.name === taskName)?.executionStatus || ''; +}; + +export const getStatusBadgeIcon = (status?: StatusType) => { + switch (status) { + case StatusType.Successful: + return IconSuccessBadge; + + case StatusType.Failed: + return IconFailBadge; + + default: + return IconSkippedBadge; + } +}; + +export const getFormattedPipelineDetails = ( + pipelineDetails: Pipeline +): Pipeline => { + if (pipelineDetails.tasks) { + const updatedTasks = pipelineDetails.tasks.map((task) => ({ + ...task, + // Sorting tags as the response of PATCH request does not return the sorted order + // of tags, but is stored in sorted manner in the database + // which leads to wrong PATCH payload sent after further tags removal + tags: isUndefined(task.tags) + ? undefined + : sortTagsCaseInsensitive(task.tags), + })); + + return { ...pipelineDetails, tasks: updatedTasks }; + } else { + return pipelineDetails; + } +}; + +export const getPipelineDetailsPageDefaultLayout = (tab: EntityTabs) => { + switch (tab) { + case EntityTabs.SCHEMA: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.TABLE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + w: 2, + x: 6, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 3, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 4, + static: false, + }, + ]; + + default: + return []; + } +}; + +export const getPipelineDetailPageTabs = ({ + description, + editDescriptionPermission, + editGlossaryTermsPermission, + editTagsPermission, + entityName, + feedCount, + getEntityFeedCount, + handleFeedCount, + handleTagSelection, + onDescriptionUpdate, + onExtensionUpdate, + onThreadLinkSelect, + pipelineDetails, + pipelineFQN, + tasksInternal, + tasksDAGView, + tags, + viewAllPermission, + editLineagePermission, + editCustomAttributePermission, + deleted, + activeTab, + setActiveTab, + taskColumns, + owners, + fetchPipeline, + tab, +}: PipelineDetailPageTabProps) => { + return [ + { + label: , + key: EntityTabs.TASKS, + children: ( + +
+ + + + + + + setActiveTab(e.target.value)} + /> + + + {activeTab === PIPELINE_TASK_TABS.LIST_VIEW ? ( +
+ ) : ( + tasksDAGView + )} + + + + ), + ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, + }} + secondPanel={{ + children: ( +
+ + customProperties={pipelineDetails} + dataProducts={pipelineDetails?.dataProducts ?? []} + domain={pipelineDetails?.domain} + editCustomAttributePermission={ + editCustomAttributePermission + } + editGlossaryTermsPermission={editGlossaryTermsPermission} + editTagPermission={editTagsPermission} + entityFQN={pipelineFQN} + entityId={pipelineDetails.id} + entityType={EntityType.PIPELINE} + selectedTags={tags} + viewAllPermission={viewAllPermission} + onExtensionUpdate={onExtensionUpdate} + onTagSelectionChange={handleTagSelection} + onThreadLinkSelect={onThreadLinkSelect} + /> +
+ ), + ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, + className: + 'entity-resizable-right-panel-container entity-resizable-panel-container', + }} + /> + + + ), + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.EXECUTIONS, + children: ( + + ), + }, + { + label: , + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: pipelineDetails && ( +
+ + entityDetails={pipelineDetails} + entityType={EntityType.PIPELINE} + handleExtensionUpdate={onExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; +}; From a8684b4f47bba5578bc050f5222ba3cd3b378eea Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:11:48 +0530 Subject: [PATCH 13/63] fix console errors --- .../DashboardDetails.component.tsx | 18 ++++++++-------- .../DashboardDetails.interface.ts | 5 +---- .../DataModels/DataModelDetails.component.tsx | 13 +++++------- .../DataModels/DataModelDetails.interface.tsx | 5 +---- .../ModelTab/ModelTab.component.tsx | 10 ++++++--- .../DashboardDetailsPage.component.tsx | 21 +++++++++++++++---- .../DataModelPage/DataModelPage.component.tsx | 6 ++---- 7 files changed, 42 insertions(+), 36 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index f59f36d32797..34aeb48666e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -177,7 +177,7 @@ const DashboardDetails = ({ description: updatedHTML, }; try { - await onDashboardUpdate(updatedDashboard, 'description'); + await onDashboardUpdate(updatedDashboard); } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -194,7 +194,7 @@ const DashboardDetails = ({ ...dashboardDetails, owners: newOwners, }; - await onDashboardUpdate(updatedDashboard, 'owners'); + await onDashboardUpdate(updatedDashboard); }, [owners] ); @@ -205,7 +205,7 @@ const DashboardDetails = ({ ...dashboardDetails, tags: tierTag, }; - await onDashboardUpdate(updatedDashboard, 'tags'); + await onDashboardUpdate(updatedDashboard); }; const onUpdateDisplayName = async (data: EntityName) => { @@ -213,13 +213,13 @@ const DashboardDetails = ({ ...dashboardDetails, displayName: data.displayName, }; - await onDashboardUpdate(updatedData, 'displayName'); + await onDashboardUpdate(updatedData); }; const onExtensionUpdate = async (updatedData: Dashboard) => { - await onDashboardUpdate( - { ...dashboardDetails, extension: updatedData.extension }, - 'extension' - ); + await onDashboardUpdate({ + ...dashboardDetails, + extension: updatedData.extension, + }); }; const handleRestoreDashboard = async () => { @@ -267,7 +267,7 @@ const DashboardDetails = ({ if (updatedTags && dashboardDetails) { const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; const updatedDashboard = { ...dashboardDetails, tags: updatedTags }; - await onDashboardUpdate(updatedDashboard, 'tags'); + await onDashboardUpdate(updatedDashboard); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts index 721219b27935..647286bce7a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts @@ -36,10 +36,7 @@ export interface DashboardDetailsProps { followDashboardHandler: () => Promise; unFollowDashboardHandler: () => Promise; versionHandler: () => void; - onDashboardUpdate: ( - updatedDashboard: Dashboard, - key: keyof Dashboard - ) => Promise; + onDashboardUpdate: (updatedDashboard: Dashboard) => Promise; handleToggleDelete: (version?: number) => void; onUpdateVote?: (data: QueryVote, id: string) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 0854437b5651..4f5c1fe2a92e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -115,7 +115,7 @@ const DataModelDetails = ({ displayName: data.displayName, }; - await onUpdateDataModel(updatedData, 'displayName'); + await onUpdateDataModel(updatedData); }; const versionHandler = () => { @@ -207,13 +207,10 @@ const DataModelDetails = ({ const handelExtensionUpdate = useCallback( async (updatedDataModel: DashboardDataModel) => { - await onUpdateDataModel( - { - ...dataModelData, - extension: updatedDataModel.extension, - }, - 'extension' - ); + await onUpdateDataModel({ + ...dataModelData, + extension: updatedDataModel.extension, + }); }, [onUpdateDataModel, dataModelData] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx index 2a3783fd2c7b..77ca31dc4237 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx @@ -34,9 +34,6 @@ export interface DataModelDetailsProps { handleUpdateDescription: (value: string) => Promise; handleColumnUpdateDataModel: (updatedDataModel: Column[]) => Promise; onUpdateVote: (data: QueryVote, id: string) => Promise; - onUpdateDataModel: ( - updatedDataModel: DashboardDataModel, - key: keyof DashboardDataModel - ) => Promise; + onUpdateDataModel: (updatedDataModel: DashboardDataModel) => Promise; handleToggleDelete: (version?: number) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx index 22d75fd88dce..bc38bf5d3e73 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx @@ -44,10 +44,11 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { const { t } = useTranslation(); const [editColumnDescription, setEditColumnDescription] = useState(); const { - data: { columns: data, fullyQualifiedName: entityFqn }, + data: dataModel, permissions, onUpdate, } = useGenericContext(); + const { columns: data, fullyQualifiedName: entityFqn } = dataModel; const { hasEditDescriptionPermission, @@ -82,7 +83,7 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { dataModelData ); - await onUpdate(dataModelData); + await onUpdate({ ...dataModel, columns: dataModelData }); }, [data, updateFieldTags] ); @@ -96,7 +97,10 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { editColumnDescription?.fullyQualifiedName ?? '', updatedDescription ); - await onUpdate(dataModelColumns); + await onUpdate({ + ...dataModel, + columns: dataModelColumns, + }); } setEditColumnDescription(undefined); }, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx index 9df9a3c63ee6..39914c435ff3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx @@ -44,7 +44,6 @@ import { postThread } from '../../rest/feedsAPI'; import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { defaultFields } from '../../utils/DashboardDetailsUtils'; import { getEntityName } from '../../utils/EntityUtils'; @@ -151,7 +150,21 @@ const DashboardDetailsPage = () => { } }; - const onDashboardUpdate = async ( + const handleDashboardUpdate = async (updatedDashboard: Dashboard) => { + try { + const response = await saveUpdatedDashboardData(updatedDashboard); + setDashboardDetails((previous) => { + return { + ...previous, + ...response, + }; + }); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + + /* const onDashboardUpdate = async ( updatedDashboard: Dashboard, key: keyof Dashboard ) => { @@ -170,7 +183,7 @@ const DashboardDetailsPage = () => { } catch (error) { showErrorToast(error as AxiosError); } - }; + }; */ const followDashboard = async () => { try { @@ -302,7 +315,7 @@ const DashboardDetailsPage = () => { unFollowDashboardHandler={unFollowDashboard} updateDashboardDetailsState={updateDashboardDetailsState} versionHandler={versionHandler} - onDashboardUpdate={onDashboardUpdate} + onDashboardUpdate={handleDashboardUpdate} onUpdateVote={updateVote} /> ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index 9922778fd8d9..8f6d5401b4fc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -283,16 +283,14 @@ const DataModelsPage = () => { }; const handleUpdateDataModel = async ( - updatedDataModel: DashboardDataModel, - key: keyof DashboardDataModel + updatedDataModel: DashboardDataModel ) => { try { const response = await handleUpdateDataModelData(updatedDataModel); setDataModelData((prev) => ({ ...prev, - [key]: response[key], - version: response.version, + ...response, })); } catch (error) { showErrorToast(error as AxiosError); From 1cc5ab19ad32baa4d03f34beabb5b446ccef3996 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:44:20 +0530 Subject: [PATCH 14/63] fix tests --- .../PipelineDetails/PipelineDetails.test.tsx | 7 + .../Topic/TopicSchema/TopicSchema.test.tsx | 35 ++ .../ContainerPage/ContainerPage.test.tsx | 10 +- .../src/pages/ContainerPage/ContainerPage.tsx | 1 + .../DashboardDetailsPage.test.tsx | 424 ++++-------------- 5 files changed, 130 insertions(+), 347 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx index 9ad1dc33f2d4..6794a5953b21 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx @@ -89,6 +89,13 @@ jest.mock('../../../hooks/useApplicationStore', () => ({ currentUser: { id: 'testUser', }, + selectedPersona: { + id: 'personaid', + name: 'persona name', + description: 'persona description', + type: 'persona type', + owner: 'persona owner', + }, }), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx index a2da32452ab3..f961cd0c2d1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx @@ -22,6 +22,8 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { Topic } from '../../../generated/entity/data/topic'; +import { MESSAGE_SCHEMA } from '../TopicDetails/TopicDetails.mock'; import TopicSchema from './TopicSchema'; import { TopicSchemaFieldsProps } from './TopicSchema.interface'; @@ -81,6 +83,39 @@ jest.mock('../../Database/SchemaEditor/SchemaEditor', () => )) ); +const mockOnUpdate = jest.fn(); + +jest.mock('../../GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockReturnValue({ + data: { + columns: [], + displayName: 'test-display-name', + description: 'test-description', + tags: [], + owner: 'test-owner', + service: { + id: 'test-service-id', + name: 'test-service-name', + displayName: 'test-service-display-name', + description: 'test-service-description', + tags: [], + owner: 'test-owner', + }, + messageSchema: MESSAGE_SCHEMA as Topic['messageSchema'], + }, + isVersionView: false, + permissions: { + EditAll: true, + }, + onUpdate: mockOnUpdate, + currentVersionData: {}, + }), +})); + +jest.mock('../../../hooks/useFqn', () => ({ + useFqn: jest.fn().mockReturnValue('test-fqn'), +})); + describe('Topic Schema', () => { it('Should render the schema component', async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx index 6c598f2d054d..2f1e1c90c04e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx @@ -14,6 +14,7 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { ReactNode } from 'react'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; +import { EntityTabs } from '../../enums/entity.enum'; import { Include } from '../../generated/type/include'; import { addContainerFollower, @@ -62,6 +63,13 @@ jest.mock( jest.mock('../../hooks/useApplicationStore', () => ({ useApplicationStore: jest.fn().mockReturnValue({ id: 'userid', + selectedPersona: { + id: 'personaid', + name: 'persona name', + description: 'persona description', + type: 'persona type', + owner: 'persona owner', + }, }), })); @@ -370,7 +378,7 @@ describe('Container Page Component', () => { it('children should render on children tab', async () => { mockUseParams.mockReturnValue({ fqn: CONTAINER_DATA_1.fullyQualifiedName, - tab: 'children', + tab: EntityTabs.CHILDREN, }); await act(async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 0b3a745b04e9..0df4e57f329d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -85,6 +85,7 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; + const ContainerPage = () => { const history = useHistory(); const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx index 6627420ecfe4..689fc8d92615 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2022 Collate. + * Copyright 2025 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 @@ -10,389 +10,121 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { - act, - render, - screen, - waitForElementToBeRemoved, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { act, render, screen } from '@testing-library/react'; +import { AxiosError, InternalAxiosRequestConfig } from 'axios'; import React from 'react'; -import { MemoryRouter } from 'react-router'; -import { mockUserData } from '../../components/Settings/Users/mocks/User.mocks'; +import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; +import { getDashboardByFqn } from '../../rest/dashboardAPI'; import DashboardDetailsPage from './DashboardDetailsPage.component'; -import { - CREATE_THREAD, - DASHBOARD_DELETED, - ENTITY_MISSING_ERROR, - ERROR, - FETCH_DASHBOARD, - FOLLOW_DASHBOARD, - TOGGLE_DELETE, - UNFOLLOW_DASHBOARD, - UPDATE_CHART_DESCRIPTION, - UPDATE_CHART_TAGS, - UPDATE_DASHBOARD, - UPDATE_VOTE, - VERSION_HANDLER, -} from './mocks/DashboardDetailsPage.mock'; - -const mockAddFollower = jest.fn().mockResolvedValue({}); -const mockGetDashboardByFqn = jest.fn().mockResolvedValue({ version: 1 }); -const mockPatchDashboardDetails = jest.fn().mockResolvedValue({}); -const mockRemoveFollower = jest.fn().mockResolvedValue({}); -const mockUpdateDashboardVotes = jest.fn().mockResolvedValue({}); -const mockUpdateChart = jest.fn().mockResolvedValue({}); -const mockPostThread = jest.fn().mockResolvedValue({}); -const mockFetchCharts = jest.fn().mockResolvedValue([]); -const mockPush = jest.fn(); -const mockShowErrorToast = jest.fn(); +// Mock the required dependencies jest.mock('react-router-dom', () => ({ - useHistory: jest.fn(() => ({ - push: mockPush, - })), + useHistory: jest.fn(), + useParams: jest.fn().mockReturnValue({ fqn: 'test-dashboard' }), })); -jest.mock('../../hooks/useApplicationStore', () => ({ - useApplicationStore: jest.fn(() => ({ currentUser: mockUserData })), +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), })); -jest.mock('../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder', () => - jest.fn().mockImplementation(() =>
ErrorPlaceHolder
) -); - +jest.mock('../../context/PermissionProvider/PermissionProvider'); +jest.mock('../../rest/dashboardAPI'); jest.mock( '../../components/Dashboard/DashboardDetails/DashboardDetails.component', - () => - jest + () => { + return jest .fn() - .mockImplementation( - ({ - dashboardDetails, - createThread, - chartDescriptionUpdateHandler, - chartTagUpdateHandler, - fetchDashboard, - followDashboardHandler, - handleToggleDelete, - unFollowDashboardHandler, - versionHandler, - onDashboardUpdate, - onUpdateVote, - }) => ( -
- DashboardDetailsComponent - {dashboardDetails.deleted ? DASHBOARD_DELETED : ''} - - - - - - - - - - -
- ) - ) -); - -jest.mock('../../components/common/Loader/Loader', () => - jest.fn().mockImplementation(() =>
Loader
) + .mockImplementation(() =>
Dashboard Details Component
); + } ); -jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ - usePermissionProvider: jest.fn().mockReturnValue({ - getEntityPermissionByFqn: jest.fn().mockReturnValue({ - ViewAll: true, - ViewBasic: true, - }), - }), -})); - -jest.mock('../../hooks/useFqn', () => ({ - useFqn: jest.fn().mockReturnValue({ fqn: 'mockFQN' }), -})); - -jest.mock('../../rest/chartAPI', () => ({ - updateChart: jest.fn(() => mockUpdateChart()), -})); - -jest.mock('../../rest/dashboardAPI', () => ({ - addFollower: jest.fn(() => mockAddFollower()), - patchDashboardDetails: jest.fn(() => mockPatchDashboardDetails()), - removeFollower: jest.fn(() => mockRemoveFollower()), - updateDashboardVotes: jest.fn(() => mockUpdateDashboardVotes()), - getDashboardByFqn: jest.fn(() => mockGetDashboardByFqn()), -})); - -jest.mock('../../rest/feedsAPI', () => ({ - postThread: jest.fn(() => mockPostThread()), -})); - -jest.mock('../../utils/CommonUtils', () => ({ - addToRecentViewed: jest.fn(), - getEntityMissingError: jest.fn(() => ENTITY_MISSING_ERROR), - sortTagsCaseInsensitive: jest.fn((tags) => tags), -})); - -jest.mock('../../utils/DashboardDetailsUtils', () => ({ - defaultFields: 'defaultFields', - fetchCharts: jest.fn(() => mockFetchCharts()), - sortTagsForCharts: jest.fn((charts) => charts), -})); - -jest.mock('../../utils/EntityUtils', () => ({ - getEntityName: jest.fn().mockReturnValue('entityName'), -})); - -jest.mock('../../utils/ToastUtils', () => ({ - showErrorToast: jest.fn(() => mockShowErrorToast()), -})); - -jest.mock('fast-json-patch', () => ({ - ...jest.requireActual('fast-json-patch'), - compare: jest.fn(), -})); - -describe('Test DashboardDetails page', () => { - it('should render Dashboard Details Component after fetching data', async () => { - render(, { - wrapper: MemoryRouter, - }); - - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - expect(screen.getByText('DashboardDetailsComponent')).toBeInTheDocument(); - }); - - it('follow and unfollow dashboard action check', async () => { - render(, { - wrapper: MemoryRouter, - }); - - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // follow dashboard - act(() => { - userEvent.click( - screen.getByRole('button', { - name: FOLLOW_DASHBOARD, - }) - ); - }); - - expect(mockAddFollower).toHaveBeenCalled(); - - // unfollow dashboard - act(() => { - userEvent.click( - screen.getByRole('button', { - name: UNFOLLOW_DASHBOARD, - }) - ); - }); - - expect(mockRemoveFollower).toHaveBeenCalled(); - }); - - it('update chart actions check', async () => { - render(, { - wrapper: MemoryRouter, - }); - - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - act(() => { - // update chart description - userEvent.click( - screen.getByRole('button', { - name: UPDATE_CHART_DESCRIPTION, - }) - ); - - // update chart tags - userEvent.click( - screen.getByRole('button', { - name: UPDATE_CHART_TAGS, - }) - ); +const mockDashboard = { + id: '123', + name: 'test-dashboard', + fullyQualifiedName: 'test-dashboard', + displayName: 'Test Dashboard', + version: 1, +}; + +describe('DashboardDetailsPage', () => { + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); + + // Mock the permission provider + (usePermissionProvider as jest.Mock).mockReturnValue({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + ViewAll: true, + ViewBasic: true, + }), }); - - expect(mockUpdateChart).toHaveBeenCalledTimes(2); }); - it('create thread and version handler action check', async () => { - render(, { - wrapper: MemoryRouter, - }); - - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // create thread - userEvent.click( - screen.getByRole('button', { - name: CREATE_THREAD, - }) + it('should render loading state initially', async () => { + (getDashboardByFqn as jest.Mock).mockImplementation(() => + Promise.resolve(mockDashboard) ); - expect(mockPostThread).toHaveBeenCalled(); + render(); - // version handler - userEvent.click( - screen.getByRole('button', { - name: VERSION_HANDLER, - }) - ); - - expect(mockPush).toHaveBeenCalled(); + expect(screen.getByTestId('loader')).toBeInTheDocument(); }); - it('update vote and check toggle delete action', async () => { - render(, { - wrapper: MemoryRouter, - }); + it('should render dashboard details when data is loaded', async () => { + (getDashboardByFqn as jest.Mock).mockResolvedValue(mockDashboard); - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // toggle delete - act(() => { - userEvent.click( - screen.getByRole('button', { - name: TOGGLE_DELETE, - }) - ); - }); - - expect(screen.getByText(DASHBOARD_DELETED)).toBeInTheDocument(); - - // update vote - act(() => { - userEvent.click( - screen.getByRole('button', { - name: UPDATE_VOTE, - }) - ); + await act(async () => { + render(); }); - expect(mockUpdateDashboardVotes).toHaveBeenCalled(); + expect(screen.getByText('Dashboard Details Component')).toBeInTheDocument(); }); - it('fetch and update dashboard action check', async () => { - render(, { - wrapper: MemoryRouter, - }); - - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // fetchDashboardDetails - act(() => { - userEvent.click( - screen.getByRole('button', { - name: FETCH_DASHBOARD, + it('should show error placeholder when dashboard is not found', async () => { + (getDashboardByFqn as jest.Mock).mockImplementation(() => + Promise.reject( + new AxiosError('Not Found', '404', undefined, undefined, { + status: 404, + data: {}, + statusText: 'Not Found', + headers: {}, + config: {} as InternalAxiosRequestConfig, }) - ); + ) + ); + (usePermissionProvider as jest.Mock).mockReturnValue({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + ViewAll: true, + ViewBasic: true, + }), }); - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - expect(mockGetDashboardByFqn).toHaveBeenCalledTimes(2); - expect(mockFetchCharts).toHaveBeenCalledTimes(2); + await act(async () => { + render(); + }); - // update dashboard - act(() => { - userEvent.click( - screen.getByRole('button', { - name: UPDATE_DASHBOARD, - }) - ); + expect(getDashboardByFqn).toHaveBeenCalledWith('test-dashboard', { + fields: + 'domain,owners, followers, tags, charts,votes,dataProducts,extension,usageSummary', }); - expect(mockPatchDashboardDetails).toHaveBeenCalled(); + expect(screen.getByTestId('no-data-placeholder')).toBeInTheDocument(); }); - it('error checks', async () => { - mockUpdateChart.mockRejectedValue(ERROR); - mockPostThread.mockRejectedValueOnce(ERROR); - mockGetDashboardByFqn.mockRejectedValueOnce(ERROR); - mockFetchCharts.mockRejectedValueOnce(ERROR); - mockAddFollower.mockRejectedValueOnce(ERROR); - mockRemoveFollower.mockRejectedValueOnce(ERROR); - mockPatchDashboardDetails.mockRejectedValueOnce(ERROR); - mockUpdateDashboardVotes.mockRejectedValueOnce(ERROR); - - render(, { - wrapper: MemoryRouter, + it('should show permission error when user lacks view permissions', async () => { + (usePermissionProvider as jest.Mock).mockReturnValue({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + ViewAll: false, + ViewBasic: false, + }), }); - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // create thread - userEvent.click( - screen.getByRole('button', { - name: CREATE_THREAD, - }) - ); - await act(async () => { - // update chart description - userEvent.click( - screen.getByRole('button', { - name: UPDATE_CHART_DESCRIPTION, - }) - ); - - // update chart tags - userEvent.click( - screen.getByRole('button', { - name: UPDATE_CHART_TAGS, - }) - ); - - // fetchDashboardDetails - userEvent.click( - screen.getByRole('button', { - name: FETCH_DASHBOARD, - }) - ); - - // follow dashboard - userEvent.click( - screen.getByRole('button', { - name: FOLLOW_DASHBOARD, - }) - ); - - // unfollow dashboard - userEvent.click( - screen.getByRole('button', { - name: UNFOLLOW_DASHBOARD, - }) - ); - - // update dashboard - userEvent.click( - screen.getByRole('button', { - name: UPDATE_DASHBOARD, - }) - ); - - // update vote - userEvent.click( - screen.getByRole('button', { - name: UPDATE_VOTE, - }) - ); + render(); }); - expect(mockShowErrorToast).toHaveBeenCalledTimes(9); - - mockUpdateChart.mockResolvedValue({}); + expect( + screen.getByTestId('permission-error-placeholder') + ).toBeInTheDocument(); }); }); From 5a6147cb76eecbbdd769f58f5a7ff2ebb149cabb Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 5 Feb 2025 22:49:54 +0530 Subject: [PATCH 15/63] fix sonar cloud issues --- .../APIEndpointSchema/APIEndpointSchema.tsx | 20 +----- .../ContainerDataModel/ContainerDataModel.tsx | 22 +----- .../DashboardChartTable.tsx | 23 ++---- .../SchemaTable/SchemaTable.component.tsx | 22 +----- .../PipelineDetails.component.tsx | 31 ++------ .../IngestionPipelineList.component.tsx | 19 +---- .../components/Settings/Services/Services.tsx | 12 +--- .../Topic/TopicSchema/TopicSchema.tsx | 22 +----- .../ui/src/utils/ContainerDetailsClassBase.ts | 4 +- .../ui/src/utils/DashboardDataModelBase.ts | 4 -- .../ui/src/utils/DashboardDetailsClassBase.ts | 6 +- .../ui/src/utils/PipelineClassBase.ts | 2 +- .../ui/src/utils/PipelineDetailsUtils.tsx | 70 +------------------ .../ui/src/utils/TableColumn.util.tsx | 26 +++++++ 14 files changed, 57 insertions(+), 226 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/TableColumn.util.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx index f766cc1e34a1..4b03aa68f086 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Col, Radio, RadioChangeEvent, Row, Tooltip, Typography } from 'antd'; import Table, { ColumnsType } from 'antd/lib/table'; import classNames from 'classnames'; @@ -41,6 +40,7 @@ import { TagLabel } from '../../../generated/type/tagLabel'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { getEntityName } from '../../../utils/EntityUtils'; import { getVersionedSchema } from '../../../utils/SchemaVersionUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -323,14 +323,7 @@ const APIEndpointSchema: FC = ({ key: 'tags', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Field, index: number) => ( entityFqn={apiEndpointDetails.fullyQualifiedName ?? ''} @@ -355,14 +348,7 @@ const APIEndpointSchema: FC = ({ key: 'glossary', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Field, index: number) => ( entityFqn={apiEndpointDetails.fullyQualifiedName ?? ''} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx index f1ca32f94fea..17736eac3d90 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Tooltip, Typography } from 'antd'; import Table, { ColumnsType } from 'antd/lib/table'; import { @@ -28,12 +27,12 @@ import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants'; import { EntityType } from '../../../enums/entity.enum'; import { Column, TagLabel } from '../../../generated/entity/data/container'; import { TagSource } from '../../../generated/type/tagLabel'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { updateContainerColumnDescription, updateContainerColumnTags, } from '../../../utils/ContainerDetailUtils'; import { getEntityName } from '../../../utils/EntityUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -56,7 +55,6 @@ const ContainerDataModel: FC = ({ entityFqn, onThreadLinkSelect, }) => { - const { theme } = useApplicationStore(); const { t } = useTranslation(); const [editContainerColumnDescription, setEditContainerColumnDescription] = @@ -173,14 +171,7 @@ const ContainerDataModel: FC = ({ key: 'tags', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, filters: tagFilter.Classification, filterDropdown: ColumnFilter, onFilter: searchTagInData, @@ -205,14 +196,7 @@ const ContainerDataModel: FC = ({ key: 'glossary', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, filters: tagFilter.Glossary, filterDropdown: ColumnFilter, onFilter: searchTagInData, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx index 950b151c580e..94c60b03e5cf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Icon, { FilterOutlined } from '@ant-design/icons'; +import Icon from '@ant-design/icons'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; @@ -27,7 +27,6 @@ import { EntityType } from '../../../enums/entity.enum'; import { TagLabel, TagSource } from '../../../generated/entity/data/chart'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { ThreadType } from '../../../generated/entity/feed/thread'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { updateChart } from '../../../rest/chartAPI'; import { @@ -36,6 +35,7 @@ import { } from '../../../utils/DashboardDetailsUtils'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -60,7 +60,6 @@ export const DashboardChartTable = ({ }: DashboardChartTableProps) => { const { t } = useTranslation(); const { getEntityPermission } = usePermissionProvider(); - const { theme } = useApplicationStore(); const { data: dashboardDetails } = useGenericContext(); const { charts: listChartIds } = dashboardDetails ?? {}; @@ -344,14 +343,7 @@ export const DashboardChartTable = ({ key: 'tags', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: ChartType, index: number) => { return ( @@ -378,14 +370,7 @@ export const DashboardChartTable = ({ key: 'glossary', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: ChartType, index: number) => ( entityFqn={dashboardDetails?.fullyQualifiedName ?? ''} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index 9ba1bdaaf69b..3f231fea4358 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Button, Form, Select, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ExpandableConfig } from 'antd/lib/table/interface'; @@ -47,7 +46,6 @@ import { import { TestSummary } from '../../../generated/tests/testCase'; import { TagSource } from '../../../generated/type/schema'; import { TagLabel } from '../../../generated/type/tagLabel'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { getTestCaseExecutionSummary } from '../../../rest/testAPI'; import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils'; @@ -61,6 +59,7 @@ import { } from '../../../utils/EntityUtils'; import { getEntityColumnFQN } from '../../../utils/FeedUtils'; import { stringToHTML } from '../../../utils/StringsUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -100,7 +99,6 @@ const SchemaTable = ({ onUpdate, onThreadLinkSelect, }: SchemaTableProps) => { - const { theme } = useApplicationStore(); const { t } = useTranslation(); const [testCaseSummary, setTestCaseSummary] = useState(); const [searchedColumns, setSearchedColumns] = useState([]); @@ -427,14 +425,7 @@ const SchemaTable = ({ key: 'tags', accessor: 'tags', width: 230, - filterIcon: (filtered: boolean) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Column, index: number) => ( entityFqn={tableFqn} @@ -459,14 +450,7 @@ const SchemaTable = ({ key: 'glossary', accessor: 'tags', width: 230, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Column, index: number) => ( entityFqn={tableFqn} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index 0953fd8b0ad3..61b7bb02a4fc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; import { Card, Col, Row, Tabs, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; @@ -60,6 +59,7 @@ import { } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import pipelineClassBase from '../../../utils/PipelineClassBase'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -100,7 +100,7 @@ const PipelineDetails = ({ const history = useHistory(); const { tab } = useParams<{ tab: EntityTabs }>(); const { t } = useTranslation(); - const { currentUser, theme, selectedPersona } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const userID = currentUser?.id ?? ''; const { @@ -431,14 +431,7 @@ const PipelineDetails = ({ key: 'owners', width: 120, accessor: 'owner', - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (owner) => , }, { @@ -447,14 +440,7 @@ const PipelineDetails = ({ key: 'tags', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags, record, index) => ( entityFqn={pipelineFQN} @@ -479,14 +465,7 @@ const PipelineDetails = ({ key: 'glossary', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, filters: tagFilter.Glossary, filterDropdown: ColumnFilter, onFilter: searchTagInData, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionPipelineList/IngestionPipelineList.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionPipelineList/IngestionPipelineList.component.tsx index 1bc336c85c1e..030befc27028 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionPipelineList/IngestionPipelineList.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionPipelineList/IngestionPipelineList.component.tsx @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Button, Col, Row } from 'antd'; import { ColumnsType, TableProps } from 'antd/lib/table'; import { TableRowSelection } from 'antd/lib/table/interface'; @@ -27,12 +26,12 @@ import { import { Paging } from '../../../../../generated/type/paging'; import { usePaging } from '../../../../../hooks/paging/usePaging'; import { useAirflowStatus } from '../../../../../hooks/useAirflowStatus'; -import { useApplicationStore } from '../../../../../hooks/useApplicationStore'; import { deployIngestionPipelineById, getIngestionPipelines, } from '../../../../../rest/ingestionPipelineAPI'; import { getEntityTypeFromServiceCategory } from '../../../../../utils/ServiceUtils'; +import { columnFilterIcon } from '../../../../../utils/TableColumn.util'; import { showErrorToast, showSuccessToast, @@ -50,7 +49,6 @@ export const IngestionPipelineList = ({ serviceName: ServiceCategory | 'testSuites'; className?: string; }) => { - const { theme } = useApplicationStore(); const [pipelines, setPipelines] = useState>([]); const { isAirflowAvailable, isFetchingStatus } = useAirflowStatus(); @@ -69,17 +67,6 @@ export const IngestionPipelineList = ({ const { t } = useTranslation(); - const renderFilterIcon = useCallback( - (filtered: boolean) => ( - - ), - [theme] - ); - const typeColumnObj: ColumnsType = useMemo( () => [ { @@ -87,7 +74,7 @@ export const IngestionPipelineList = ({ dataIndex: 'pipelineType', key: 'pipelineType', filterDropdown: ColumnFilter, - filterIcon: renderFilterIcon, + filterIcon: columnFilterIcon, filters: map(PipelineType, (value) => ({ text: startCase(value), value, @@ -96,7 +83,7 @@ export const IngestionPipelineList = ({ filteredValue: pipelineTypeFilter, }, ], - [renderFilterIcon, pipelineTypeFilter] + [pipelineTypeFilter] ); const handleBulkRedeploy = useCallback(async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx index b199e98c6b31..38c3fa0987e3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Button, Col, Row, Space, Tooltip, Typography } from 'antd'; import Card from 'antd/lib/card/Card'; import { ColumnsType, TableProps } from 'antd/lib/table'; @@ -43,7 +42,6 @@ import { Include } from '../../../generated/type/include'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { usePaging } from '../../../hooks/paging/usePaging'; import { useAirflowStatus } from '../../../hooks/useAirflowStatus'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { DatabaseServiceSearchSource } from '../../../interface/search.interface'; import { ServicesType } from '../../../interface/service.interface'; import { getServices, searchService } from '../../../rest/serviceAPI'; @@ -57,6 +55,7 @@ import { getServiceTypesFromServiceCategory, } from '../../../utils/ServiceUtils'; import { stringToHTML } from '../../../utils/StringsUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import { ListView } from '../../common/ListView/ListView.component'; @@ -73,7 +72,6 @@ interface ServicesProps { } const Services = ({ serviceName }: ServicesProps) => { - const { theme } = useApplicationStore(); const { t } = useTranslation(); const { isFetchingStatus, platform } = useAirflowStatus(); @@ -344,13 +342,7 @@ const Services = ({ serviceName }: ServicesProps) => { key: 'serviceType', width: 200, filterDropdown: ColumnFilter, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, filtered: !isEmpty(serviceTypeFilter), filteredValue: serviceTypeFilter, filters: serviceTypeFilters, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index 0f477ceff060..e307e571169c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { FilterOutlined } from '@ant-design/icons'; import { Col, Radio, @@ -38,10 +37,10 @@ import { Topic, } from '../../../generated/entity/data/topic'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { getEntityName } from '../../../utils/EntityUtils'; import { getVersionedSchema } from '../../../utils/SchemaVersionUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -72,7 +71,6 @@ const TopicSchemaFields: FC = ({ onThreadLinkSelect, schemaTypePlaceholder, }) => { - const { theme } = useApplicationStore(); const { t } = useTranslation(); const [editFieldDescription, setEditFieldDescription] = useState(); const [expandedRowKeys, setExpandedRowKeys] = useState([]); @@ -250,14 +248,7 @@ const TopicSchemaFields: FC = ({ key: 'tags', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Field, index: number) => ( entityFqn={entityFqn} @@ -282,14 +273,7 @@ const TopicSchemaFields: FC = ({ key: 'glossary', accessor: 'tags', width: 300, - filterIcon: (filtered) => ( - - ), + filterIcon: columnFilterIcon, render: (tags: TagLabel[], record: Field, index: number) => ( entityFqn={entityFqn} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index a4276b10f745..f27a97fae279 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -50,7 +50,7 @@ export interface ContainerDetailPageTabProps { editCustomAttributePermission: boolean; viewAllPermission: boolean; containerChildrenData: EntityReference[]; - fetchContainerChildren: () => void; + fetchContainerChildren: () => Promise; isChildrenLoading: boolean; onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; handleUpdateDescription: (description: string) => Promise; @@ -58,7 +58,7 @@ export interface ContainerDetailPageTabProps { handleTagSelection: (tags: EntityTags[]) => Promise; handleExtensionUpdate: (updatedContainer: Container) => Promise; feedCount: { totalCount: number }; - getEntityFeedCount: () => void; + getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; tab: EntityTabs; owners: EntityReference[]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index aed8e629e991..dbb21fc805b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -46,10 +46,6 @@ export interface DashboardDataModelDetailPageTabProps { } class DashboardDataModelBase { - constructor() { - // Do nothing - } - public getDashboardDataModelDetailPageTabs( tabsProps: DashboardDataModelDetailPageTabProps ): TabProps[] { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 8f10a27ba7ee..55fddfd617f2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -51,16 +51,12 @@ export interface DashboardDetailsTabsProps { feedCount: FeedCounts; activeTab: EntityTabs; deleted: boolean; - getEntityFeedCount: () => void; + getEntityFeedCount: () => Promise; fetchDashboard: () => void; labelMap: Record; } class DashboardDetailsClassBase { - constructor() { - // Do nothing - } - public getDashboardDetailPageTabs( tabsProps: DashboardDetailsTabsProps ): TabProps[] { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index 65aa6edf67b2..7ba91d97af01 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -50,7 +50,7 @@ export interface PipelineDetailPageTabProps { editDescriptionPermission: boolean; editGlossaryTermsPermission: boolean; editTagsPermission: boolean; - getEntityFeedCount: () => void; + getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; handleTagSelection: (selectedTags: TagLabel[]) => Promise; onDescriptionUpdate: (value: string) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index d943ecb4cd15..ae03e6c0cc1d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -30,7 +30,6 @@ import { SourceType } from '../components/SearchedData/SearchedData.interface'; import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; -import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Pipeline, @@ -44,7 +43,7 @@ import { PipelineDetailPageTabProps } from './PipelineClassBase'; export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.TASKS}, ${TabSpecificField.PIPELINE_STATUS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; export const getTaskExecStatus = (taskName: string, tasks: TaskStatus[]) => { - return tasks.find((task) => task.name === taskName)?.executionStatus || ''; + return tasks.find((task) => task.name === taskName)?.executionStatus; }; export const getStatusBadgeIcon = (status?: StatusType) => { @@ -80,73 +79,6 @@ export const getFormattedPipelineDetails = ( } }; -export const getPipelineDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - export const getPipelineDetailPageTabs = ({ description, editDescriptionPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableColumn.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableColumn.util.tsx new file mode 100644 index 000000000000..e1406d1abd2a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableColumn.util.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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 { FilterOutlined } from '@ant-design/icons'; +import React from 'react'; +import { useApplicationStore } from '../hooks/useApplicationStore'; + +export const columnFilterIcon = (filtered: boolean) => { + const { theme } = useApplicationStore.getState(); + + return ( + + ); +}; From d5e79739918591acec4de2cee22d4cbfe39ea022 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:43:19 +0530 Subject: [PATCH 16/63] update code for data assets --- .../ContainerChildren/ContainerChildren.tsx | 5 ++-- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 3 ++- .../SynonymsWidget/GenericWidget.tsx | 6 +---- .../ui/src/enums/CustomizeDetailPage.enum.ts | 1 + .../ui/src/utils/ContainerDetailUtils.tsx | 2 -- .../ui/src/utils/ContainerDetailsClassBase.ts | 18 ++----------- .../utils/CustomizePage/CustomizePageUtils.ts | 12 +++++++++ .../ui/src/utils/DashboardDataModelBase.ts | 10 ++++++- .../ui/src/utils/DashboardDetailsClassBase.ts | 10 ++++++- .../src/utils/Database/DatabaseClassBase.ts | 14 ---------- .../ui/src/utils/DatabaseSchemaClassBase.ts | 26 +++++++++++++++++++ .../ui/src/utils/PipelineClassBase.ts | 16 +----------- .../src/utils/SearchIndexDetailsClassBase.ts | 18 ++----------- .../ui/src/utils/StoredProcedureBase.ts | 2 +- 14 files changed, 69 insertions(+), 74 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx index 30892f78a478..48fbb6a3b382 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx @@ -23,19 +23,20 @@ import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import Table from '../../common/Table/Table'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; interface ContainerChildrenProps { - childrenList: Container['children']; isLoading?: boolean; fetchChildren: () => void; } const ContainerChildren: FC = ({ - childrenList, isLoading, fetchChildren, }) => { const { t } = useTranslation(); + const { data } = useGenericContext(); + const { children: childrenList } = data ?? {}; const columns: ColumnsType = useMemo( () => [ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx index c0abcfaba3ec..0e2c51a3f458 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -58,8 +58,9 @@ export const CustomizeTabWidget = () => { const items = useMemo(() => { return currentPage?.tabs ?? getDefaultTabs(currentPageType as PageType); }, [currentPage, currentPageType, currentPage?.tabs]); + const [activeKey, setActiveKey] = useState( - (items[0]?.id as EntityTabs) ?? null + items.find((i) => i.editable)?.id ?? null ); const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 8de29cbba8d7..913d7a5466ed 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -612,11 +612,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.CONTAINER} onUpdate={async () => noop()}> - + ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 1e4970f6498c..9226682c18ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -38,6 +38,7 @@ export enum DetailPageWidgetKeys { DATA_MODEL = 'KnowledgePanel.DataModel', CONTAINER_CHILDREN = 'KnowledgePanel.ContainerChildren', PIPELINE_TASKS = 'KnowledgePanel.PipelineTasks', + SEARCH_INDEX_FIELDS = 'KnowledgePanel.SearchIndexFields', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index 4a55fd509c7f..571e1e6b54ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -178,7 +178,6 @@ export const getContainerDetailPageTabs = ({ {isDataModelEmpty ? ( @@ -244,7 +243,6 @@ export const getContainerDetailPageTabs = ({
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index f27a97fae279..dde70b218d51 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -215,8 +215,8 @@ class ContainerDetailsClassBase { return [ DESCRIPTION_WIDGET, { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, - name: i18n.t('label.schema'), + fullyQualifiedName: DetailPageWidgetKeys.CONTAINER_CHILDREN, + name: i18n.t('label.children'), data: { gridSizes: ['large'] as GridSizes[], }, @@ -224,20 +224,6 @@ class ContainerDetailsClassBase { DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, - { - fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - name: i18n.t('label.frequently-joined-table-plural'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, - { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, - name: i18n.t('label.table-constraints'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, CUSTOM_PROPERTIES_WIDGET, ]; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 756bf9a70bbd..bf5c96d67c19 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -293,6 +293,18 @@ export const getCustomizableWidgetByPage = ( return tableClassBase.getCommonWidgetList(); case PageType.Topic: return topicClassBase.getCommonWidgetList(); + case PageType.Dashboard: + return dashboardDetailsClassBase.getCommonWidgetList(); + case PageType.Container: + return containerDetailsClassBase.getCommonWidgetList(); + case PageType.Database: + return databaseClassBase.getCommonWidgetList(); + case PageType.DatabaseSchema: + return databaseSchemaClassBase.getCommonWidgetList(); + case PageType.Pipeline: + return pipelineClassBase.getCommonWidgetList(); + case PageType.SearchIndex: + return searchIndexClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index dbb21fc805b6..40068289d0f7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -16,6 +16,7 @@ import { DATA_PRODUCTS_WIDGET, DESCRIPTION_WIDGET, GLOSSARY_TERMS_WIDGET, + GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; @@ -26,6 +27,7 @@ import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import { getDashboardDataModelDetailPageTabs } from './DashboardDataModelUtils'; +import i18n from './i18next/LocalUtil'; export interface DashboardDataModelDetailPageTabProps { modelComponent: JSX.Element; @@ -226,7 +228,13 @@ class DashboardDataModelBase { public getCommonWidgetList() { return [ DESCRIPTION_WIDGET, - + { + fullyQualifiedName: DetailPageWidgetKeys.DATA_MODEL, + name: i18n.t('label.data-model'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 55fddfd617f2..d4ed02a8b1f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -17,6 +17,7 @@ import { DATA_PRODUCTS_WIDGET, DESCRIPTION_WIDGET, GLOSSARY_TERMS_WIDGET, + GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; @@ -31,6 +32,7 @@ import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import { getDashboardDetailPageTabs } from './DashboardDetailsUtils'; +import i18n from './i18next/LocalUtil'; export interface DashboardDetailsTabsProps { dashboardDetails: Dashboard; @@ -213,7 +215,13 @@ class DashboardDetailsClassBase { public getCommonWidgetList() { return [ DESCRIPTION_WIDGET, - + { + fullyQualifiedName: DetailPageWidgetKeys.CHARTS_TABLE, + name: i18n.t('label.chart-plural'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index b49ba3ab0286..a24ff70d3309 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -270,20 +270,6 @@ class DatabaseClassBase { DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, - { - fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - name: i18n.t('label.frequently-joined-table-plural'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, - { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, - name: i18n.t('label.table-constraints'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, CUSTOM_PROPERTIES_WIDGET, ]; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index af92cc9d8887..1450719c30dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -13,6 +13,14 @@ import { EntityTags } from 'Models'; import { PagingHandlerParams } from '../components/common/NextPrevious/NextPrevious.interface'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../constants/CustomizeWidgets.constants'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; @@ -29,6 +37,7 @@ import { UsePagingInterface } from '../hooks/paging/usePaging'; import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import { getDataBaseSchemaPageBaseTabs } from './DatabaseSchemaDetailsUtils'; +import i18n from './i18next/LocalUtil'; export interface DatabaseSchemaPageTabProps { feedCount: FeedCounts; @@ -213,6 +222,23 @@ class DatabaseSchemaClassBase { } }; + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + name: i18n.t('label.table-plural'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } + public getDummyData(): DatabaseSchema { return { id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index 7ba91d97af01..8a8912127a83 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -304,7 +304,7 @@ class PipelineClassBase { return [ DESCRIPTION_WIDGET, { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, + fullyQualifiedName: DetailPageWidgetKeys.PIPELINE_TASKS, name: i18n.t('label.schema'), data: { gridSizes: ['large'] as GridSizes[], @@ -313,20 +313,6 @@ class PipelineClassBase { DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, - { - fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - name: i18n.t('label.frequently-joined-table-plural'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, - { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, - name: i18n.t('label.table-constraints'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, CUSTOM_PROPERTIES_WIDGET, ]; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts index ba293c730fd2..e72ad259e4ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -313,8 +313,8 @@ class SearchIndexClassBase { return [ DESCRIPTION_WIDGET, { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_SCHEMA, - name: i18n.t('label.schema'), + fullyQualifiedName: DetailPageWidgetKeys.SEARCH_INDEX_FIELDS, + name: i18n.t('label.field-plural'), data: { gridSizes: ['large'] as GridSizes[], }, @@ -322,20 +322,6 @@ class SearchIndexClassBase { DATA_PRODUCTS_WIDGET, TAGS_WIDGET, GLOSSARY_TERMS_WIDGET, - { - fullyQualifiedName: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - name: i18n.t('label.frequently-joined-table-plural'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, - { - fullyQualifiedName: DetailPageWidgetKeys.TABLE_CONSTRAINTS, - name: i18n.t('label.table-constraints'), - data: { - gridSizes: ['small'] as GridSizes[], - }, - }, CUSTOM_PROPERTIES_WIDGET, ]; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index f58dfbe003f3..32466d6537c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -105,7 +105,7 @@ class StoredProcedureClassBase { static: false, }, { - h: 11, + h: 7, i: DetailPageWidgetKeys.TOPIC_SCHEMA, w: 6, x: 0, From 4580e6f1ffa5427827d46c5d4f725b653d2ceba2 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:41:57 +0530 Subject: [PATCH 17/63] remove fqn prop as --- .../APIEndpointDetails/APIEndpointDetails.tsx | 3 --- .../ActivityFeedTab.component.tsx | 3 ++- .../ActivityFeedTab.interface.ts | 1 - .../DataModels/DataModelDetails.component.tsx | 2 -- .../EntityRightPanel.test.tsx | 8 ------ .../EntityRightPanel/EntityRightPanel.tsx | 4 +-- .../GlossaryDetails.component.tsx | 2 -- .../GlossaryTermsV1.component.tsx | 1 - .../tabs/GlossaryOverviewTab.component.tsx | 1 - .../Metric/MetricDetails/MetricDetails.tsx | 3 --- .../MlModelDetail/MlModelDetail.component.tsx | 3 --- .../Settings/Users/Users.component.tsx | 1 - .../TopicDetails/TopicDetails.component.tsx | 3 --- .../Description.interface.ts | 1 - .../EntityDescription/DescriptionV1.tsx | 3 ++- .../APICollectionPage/APICollectionPage.tsx | 2 -- .../APICollectionPage/APIEndpointsTab.tsx | 2 -- .../DatabaseDetailsPage.tsx | 1 - .../DatabaseSchemaPage.component.tsx | 2 -- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 2 -- .../PoliciesDetailPage/PoliciesDetailPage.tsx | 1 - .../RolesDetailPage/RolesDetailPage.tsx | 1 - .../SearchIndexDetailsPage.tsx | 3 --- .../ServiceMainTabContent.tsx | 4 --- .../ServiceVersionMainTabContent.tsx | 1 - .../TableDetailsPageV1/TableDetailsPageV1.tsx | 18 ++++++------- .../ui/src/pages/TagPage/TagPage.tsx | 1 - .../ui/src/utils/ContainerDetailUtils.tsx | 3 --- .../ui/src/utils/DashboardDataModelUtils.tsx | 1 - .../ui/src/utils/DashboardDetailsUtils.tsx | 3 --- .../ui/src/utils/Database/Database.util.tsx | 4 --- .../src/utils/Database/DatabaseClassBase.ts | 1 - .../ui/src/utils/DatabaseSchemaClassBase.ts | 3 +-- .../src/utils/DatabaseSchemaDetailsUtils.tsx | 3 --- .../ui/src/utils/PipelineDetailsUtils.tsx | 3 --- .../ui/src/utils/StoredProceduresUtils.tsx | 4 --- .../resources/ui/src/utils/TableUtils.tsx | 25 +++++++++---------- 37 files changed, 28 insertions(+), 99 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index a93a1f759210..adc53cc5bc3b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -302,7 +302,6 @@ const APIEndpointDetails: React.FC = ({
= ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityFQN={decodedApiEndpointFqn} entityId={apiEndpointDetails.id} entityType={EntityType.API_ENDPOINT} selectedTags={apiEndpointTags} @@ -370,7 +368,6 @@ const APIEndpointDetails: React.FC = ({ refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.API_ENDPOINT} - fqn={apiEndpointDetails?.fullyQualifiedName ?? ''} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchAPIEndpointDetails} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index 3bd9325efaef..b65678083ed4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -48,6 +48,7 @@ import { import { useAuth } from '../../../hooks/authHooks'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useElementInView } from '../../../hooks/useElementInView'; +import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { getFeedCount } from '../../../rest/feedsAPI'; import { @@ -81,7 +82,6 @@ const componentsVisibility = { }; export const ActivityFeedTab = ({ - fqn, owners = [], columns, entityType, @@ -102,6 +102,7 @@ export const ActivityFeedTab = ({ root: document.querySelector('#center-container'), rootMargin: '0px 0px 2px 0px', }); + const { fqn } = useFqn(); const { tab = EntityTabs.ACTIVITY_FEED, subTab: activeTab = ActivityFeedTabs.ALL, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface.ts index 5f7fc59aa4fb..ee4816988f41 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface.ts @@ -24,7 +24,6 @@ export enum ActivityFeedTabs { } export interface ActivityFeedTabBasicProps { - fqn: string; isForFeedTab?: boolean; refetchFeed?: boolean; entityFeedTotalCount?: number; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 4f5c1fe2a92e..40bed050f3bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -226,7 +226,6 @@ const DataModelDetails = ({
{ customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -89,7 +88,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -113,7 +111,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -136,7 +133,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -165,7 +161,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -192,7 +187,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -217,7 +211,6 @@ describe('EntityRightPanel component test', () => { customProperties={mockCustomProperties} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} @@ -243,7 +236,6 @@ describe('EntityRightPanel component test', () => { customProperties={{} as Table} dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityFQN="testEntityFQN" entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index 496e8e4db7e4..cb3d0e7d4318 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -18,6 +18,7 @@ import { TablePartition } from '../../../generated/entity/data/table'; import { ThreadType } from '../../../generated/entity/feed/thread'; import { EntityReference } from '../../../generated/entity/type'; import { TagSource } from '../../../generated/type/tagLabel'; +import { useFqn } from '../../../hooks/useFqn'; import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; import entityRightPanelClassBase from '../../../utils/EntityRightPanelClassBase'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; @@ -34,7 +35,6 @@ interface EntityRightPanelProps { editTagPermission: boolean; editGlossaryTermsPermission: boolean; entityType: EntityType; - entityFQN: string; entityId: string; selectedTags: EntityTags[]; beforeSlot?: React.ReactNode; @@ -54,7 +54,6 @@ interface EntityRightPanelProps { const EntityRightPanel = ({ domain, dataProducts, - entityFQN, entityType, selectedTags, editTagPermission, @@ -74,6 +73,7 @@ const EntityRightPanel = ({ }: EntityRightPanelProps) => { const KnowledgeArticles = entityRightPanelClassBase.getKnowLedgeArticlesWidget(); + const { fqn: entityFQN } = useFqn(); return ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 337cb6f0b662..de5d1c176883 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -208,7 +208,6 @@ const GlossaryDetails = ({ return ( = ({ = ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityFQN={decodedMetricFqn} entityId={metricDetails.id} entityType={EntityType.METRIC} selectedTags={metricTags} @@ -376,7 +374,6 @@ const MetricDetails: React.FC = ({ refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.METRIC} - fqn={metricDetails?.fullyQualifiedName ?? ''} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchMetricDetails} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index e90f87ae3fca..89439242bd21 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -398,7 +398,6 @@ const MlModelDetail: FC = ({
= ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityFQN={decodedMlModelFqn} entityId={mlModelDetail.id} entityType={EntityType.MLMODEL} selectedTags={mlModelTags} @@ -471,7 +469,6 @@ const MlModelDetail: FC = ({ refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.MLMODEL} - fqn={mlModelDetail?.fullyQualifiedName ?? ''} onFeedUpdate={fetchEntityFeedCount} onUpdateEntityDetails={fetchMlModel} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx index 39c261445939..e3b26c4130dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx @@ -178,7 +178,6 @@ const Users = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index 2f7e26f7996f..ef2b59456083 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -316,7 +316,6 @@ const TopicDetails: React.FC = ({
= ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityFQN={decodedTopicFQN} entityId={topicDetails.id} entityType={EntityType.TOPIC} selectedTags={topicTags} @@ -385,7 +383,6 @@ const TopicDetails: React.FC = ({ refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.TOPIC} - fqn={topicDetails?.fullyQualifiedName ?? ''} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchTopic} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts index 6f438c8bf2e9..0bcb06dad070 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts @@ -22,7 +22,6 @@ export interface DescriptionProps { description?: string; isReadOnly?: boolean; entityType: EntityType; - entityFqn?: string; onThreadLinkSelect?: (value: string) => void; onDescriptionUpdate?: (value: string) => Promise; onSuggest?: (value: string) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index 8efa87efa42c..d6daf2ac4fcb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -23,6 +23,7 @@ import { ReactComponent as RequestIcon } from '../../../assets/svg/request-icon. import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { EntityField } from '../../../constants/Feeds.constants'; import { EntityType } from '../../../enums/entity.enum'; +import { useFqn } from '../../../hooks/useFqn'; import { isDescriptionContentEmpty } from '../../../utils/BlockEditorUtils'; import { getEntityFeedLink } from '../../../utils/EntityUtils'; import { @@ -49,7 +50,6 @@ const DescriptionV1 = ({ entityName, onThreadLinkSelect, entityType, - entityFqn, wrapInCard = false, showActions = true, showCommentsIcon = true, @@ -61,6 +61,7 @@ const DescriptionV1 = ({ const { suggestions = [], selectedUserSuggestions = [] } = useSuggestionsContext(); const [isEditDescription, setIsEditDescription] = useState(false); + const { fqn: entityFqn } = useFqn(); const handleRequestDescription = useCallback(() => { history.push( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index b30a574fdb7a..605676a63f9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -602,7 +602,6 @@ const APICollectionPage: FunctionComponent = () => { } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityFQN={decodedAPICollectionFQN} entityId={apiCollection?.id ?? ''} entityType={EntityType.API_COLLECTION} selectedTags={tags} @@ -638,7 +637,6 @@ const APICollectionPage: FunctionComponent = () => { refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.API_COLLECTION} - fqn={apiCollection?.fullyQualifiedName ?? ''} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchAPICollectionDetails} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index 2e8b5319433d..abbe90360b3f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -118,7 +118,6 @@ function APIEndpointsTab({ {isVersionView ? ( { activeTab, database, description, - decodedDatabaseFQN, editDescriptionPermission, editGlossaryTermsPermission, editTagsPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index 8e82df971e52..ec869cb2a632 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -627,7 +627,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { editCustomAttributePermission, editTagsPermission, editGlossaryTermsPermission, - decodedDatabaseSchemaFQN, tags, viewAllPermission, databaseSchemaPermission, @@ -659,7 +658,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { editCustomAttributePermission, editTagsPermission, editGlossaryTermsPermission, - decodedDatabaseSchemaFQN, tags, viewAllPermission, storedProcedureCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index 99f96e5210e8..6d6e4448fa1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -153,7 +153,6 @@ function SchemaTablesTab({ {isVersionView ? ( { hasEditAccess className="m-y-md" description={policy.description || ''} - entityFqn={policy.fullyQualifiedName} entityName={policyName} entityType={EntityType.POLICY} showCommentsIcon={false} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx index 231cef0a2632..895d0a10eb5e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx @@ -365,7 +365,6 @@ const RolesDetailPage = () => { hasEditAccess className="m-y-md" description={role.description || ''} - entityFqn={role.fullyQualifiedName} entityName={roleName} entityType={EntityType.ROLE} showCommentsIcon={false} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index ae5f5ce0724a..39685aa36cbd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -383,7 +383,6 @@ function SearchIndexDetailsPage() {
(); - const { fqn: serviceFQN } = useFqn(); const { permissions } = usePermissionProvider(); const [pageData, setPageData] = useState([]); @@ -232,7 +230,6 @@ function ServiceMainTabContent({
{ const { currentUser, selectedPersona } = useApplicationStore(); const { setDqLineageData } = useTestCaseStore(); const [tableDetails, setTableDetails] = useState
(); - const { tab: activeTab = EntityTabs.SCHEMA } = - useParams<{ tab: EntityTabs }>(); + const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); const { fqn: datasetFQN } = useFqn(); const { t } = useTranslation(); const history = useHistory(); @@ -375,7 +374,7 @@ const TableDetailsPageV1: React.FC = () => { [tableDetails, tableId] ); - const onTableUpdate = async (updatedTable: Table, key: keyof Table) => { + const onTableUpdate = async (updatedTable: Table, key?: keyof Table) => { try { const res = await saveUpdatedTableData(updatedTable); @@ -393,12 +392,13 @@ const TableDetailsPageV1: React.FC = () => { const updatedObj = { ...previous, + ...res, version: res.version, - [key]: res[key], + ...(key && { [key]: res[key] }), }; // If operation was to remove let's remove the key itself - if (res[key] === undefined) { + if (key && res[key] === undefined) { delete updatedObj[key]; } @@ -524,11 +524,13 @@ const TableDetailsPageV1: React.FC = () => { labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + const updatedTabs = getGlossaryTermDetailTabs( tabs, customizedPage?.tabs, EntityTabs.SCHEMA ); + + return updatedTabs; }, [ schemaTab, queryCount, @@ -802,9 +804,7 @@ const TableDetailsPageV1: React.FC = () => { isVersionView={false} permissions={tablePermissions} type={EntityType.TABLE} - onUpdate={async (data) => { - await saveUpdatedTableData(data); - }}> + onUpdate={onTableUpdate}> {/* Entity Heading */} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx index ea34585a3a47..495f7b754dc3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx @@ -437,7 +437,6 @@ const TagPage = () => { fetchContainerDetail(decodedContainerName) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index 563455ad0b5a..b108ee5cee85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -66,7 +66,6 @@ export const getDashboardDataModelDetailPageTabs = ({ refetchFeed entityFeedTotalCount={feedCount.totalCount} entityType={EntityType.DASHBOARD_DATA_MODEL} - fqn={dataModelData?.fullyQualifiedName ?? ''} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchDataModel} onUpdateFeedCount={handleFeedCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 71b0509c11ab..11f345229d16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -166,7 +166,6 @@ export const getDashboardDetailPageTabs = ({
( return searchedValue; }; +export const getUpdatedTags = (newFieldTags: Array): TagLabel[] => { + const mappedNewTags: TagLabel[] = newFieldTags.map((tag) => ({ + ...omit(tag, 'isRemovable'), + labelType: LabelType.Manual, + state: State.Confirmed, + source: tag.source || 'Classification', + tagFQN: tag.tagFQN, + })); + + return mappedNewTags; +}; + export const updateFieldTags = ( changedFieldFQN: string, newFieldTags: EntityTags[], @@ -700,18 +712,6 @@ export const updateFieldTags = ( }); }; -export const getUpdatedTags = (newFieldTags: Array): TagLabel[] => { - const mappedNewTags: TagLabel[] = newFieldTags.map((tag) => ({ - ...omit(tag, 'isRemovable'), - labelType: LabelType.Manual, - state: State.Confirmed, - source: tag.source || 'Classification', - tagFQN: tag.tagFQN, - })); - - return mappedNewTags; -}; - export const updateFieldDescription = ( changedFieldFQN: string, description: string, @@ -786,7 +786,6 @@ export const getTableDetailPageBaseTabs = ({ columns={tableDetails?.columns} entityFeedTotalCount={totalFeedCount} entityType={EntityType.TABLE} - fqn={tableDetails?.fullyQualifiedName ?? ''} owners={tableDetails?.owners} onFeedUpdate={getEntityFeedCount} onUpdateEntityDetails={fetchTableDetails} From 93c660ffb980eb2bc7de0cedd8c0b2ac65b8fa43 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:41:24 +0530 Subject: [PATCH 18/63] create generic tabs --- .../CommonWidgets/CommonWidgets.tsx | 231 +++++++++++ .../SchemaTab/SchemaTab.component.tsx | 62 --- .../SchemaTab/SchemaTab.interfaces.ts | 24 -- .../Database/SchemaTab/SchemaTab.test.tsx | 61 --- .../SchemaTable/SchemaTable.component.tsx | 202 +++++---- .../SchemaTable/SchemaTable.interface.ts | 11 - .../Database/SchemaTable/SchemaTable.test.tsx | 28 +- .../TableGenericTab/TableGenericTab.tsx | 102 +++++ .../TableSchemaTab/TableSchemaTab.tsx | 389 ------------------ .../EntityRightPanel/EntityRightPanel.tsx | 4 +- .../GenericProvider/GenericProvider.tsx | 80 +++- .../SynonymsWidget/GenericWidget.tsx | 35 +- .../TopicDetails/TopicDetails.component.tsx | 69 +--- .../TopicSchema/TopicSchema.interface.tsx | 3 - .../Topic/TopicSchema/TopicSchema.test.tsx | 7 +- .../Topic/TopicSchema/TopicSchema.tsx | 8 +- .../Topic/TopicSchemaTab/TopicSchemaTab.tsx | 84 ++++ .../TopicVersion/TopicVersion.component.tsx | 8 +- .../FrequentlyJoinedTables.component.tsx | 25 +- .../FrequentlyJoinedTables.test.tsx | 14 +- .../PartitionedKeys.component.tsx | 20 +- .../TableConstraints/TableConstraints.tsx | 35 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 5 - .../resources/ui/src/utils/TableClassBase.ts | 1 - .../resources/ui/src/utils/TableUtils.tsx | 4 +- 25 files changed, 704 insertions(+), 808 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx new file mode 100644 index 000000000000..dd9e037b0d3c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -0,0 +1,231 @@ +/* + * Copyright 2025 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 { isEmpty, noop } from 'lodash'; +import { EntityTags } from 'Models'; +import React, { useMemo } from 'react'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityType } from '../../../enums/entity.enum'; +import { Table } from '../../../generated/entity/data/table'; +import { EntityReference } from '../../../generated/entity/type'; +import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { getEntityName } from '../../../utils/EntityUtils'; +import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; +import { createTagObject } from '../../../utils/TagsUtils'; +import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; +import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; +import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; + +interface GenericEntity + extends Exclude, + Pick< + Table, + | 'deleted' + | 'description' + | 'owners' + | 'domain' + | 'dataProducts' + | 'extension' + | 'tags' + > {} + +interface CommonWidgetsProps { + widgetConfig: WidgetConfig; +} + +export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { + const { data, type, onUpdate, permissions, onThreadLinkSelect } = + useGenericContext(); + + const { + tier, + tags, + deleted, + owners, + domain, + extension, + dataProducts, + description, + entityName, + } = useMemo(() => { + const { tags } = data; + + return { + ...data, + tier: getTierTags(tags ?? []), + tags: getTagsWithoutTier(tags ?? []), + entityName: getEntityName(data), + }; + }, [data, data?.tags]); + + const { columns } = data as unknown as Table; + + const { + editTagsPermission, + editGlossaryTermsPermission, + editDescriptionPermission, + editCustomAttributePermission, + viewAllPermission, + } = useMemo( + () => ({ + editTagsPermission: + (permissions.EditTags || permissions.EditAll) && !deleted, + editDescriptionPermission: + (permissions.EditDescription || permissions.EditAll) && !deleted, + editGlossaryTermsPermission: + (permissions.EditGlossaryTerms || permissions.EditAll) && !deleted, + editCustomAttributePermission: + (permissions.EditAll || permissions.EditCustomFields) && !deleted, + editAllPermission: permissions.EditAll && !deleted, + editLineagePermission: + (permissions.EditAll || permissions.EditLineage) && !deleted, + viewSampleDataPermission: + permissions.ViewAll || permissions.ViewSampleData, + viewQueriesPermission: permissions.ViewAll || permissions.ViewQueries, + viewProfilerPermission: + permissions.ViewAll || + permissions.ViewDataProfile || + permissions.ViewTests, + viewAllPermission: permissions.ViewAll, + viewBasicPermission: permissions.ViewAll || permissions.ViewBasic, + }), + [permissions, deleted] + ); + + /** + * Formulates updated tags and updates table entity data for API call + * @param selectedTags + */ + const handleTagsUpdate = async (selectedTags?: Array) => { + if (selectedTags && data) { + const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; + const updatedTable = { ...data, tags: updatedTags }; + await onUpdate(updatedTable); + } + }; + + const handleTagSelection = async (selectedTags: EntityTags[]) => { + const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); + await handleTagsUpdate(updatedTags); + }; + + const tagsWidget = useMemo(() => { + return ( + + ); + }, [type]); + + const glossaryWidget = useMemo(() => { + return ( + + ); + }, []); + + const descriptionWidget = useMemo(() => { + return ( + 1} + showActions={!deleted} + onDescriptionUpdate={async (value: string) => { + if (value !== description) { + await onUpdate({ ...data, description: value }); + } + }} + onThreadLinkSelect={onThreadLinkSelect} + /> + ); + }, []); + + const handleExtensionUpdate = async (updatedTable: Table) => { + await onUpdate(updatedTable as unknown as GenericEntity); + }; + + const widget = useMemo(() => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DESCRIPTION)) { + return descriptionWidget; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DATA_PRODUCTS)) { + return ( + + ); + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TAGS)) { + return tagsWidget; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.GLOSSARY_TERMS)) { + return glossaryWidget; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.CUSTOM_PROPERTIES) + ) { + return ( + + isRenderedInRightPanel + entityDetails={extension} + entityType={EntityType.TABLE} + handleExtensionUpdate={handleExtensionUpdate} + hasEditAccess={Boolean(editCustomAttributePermission)} + hasPermission={Boolean(viewAllPermission)} + maxDataCap={5} + /> + ); + } + + return getWidgetFromKey({ + widgetConfig: widgetConfig, + handleOpenAddWidgetModal: noop, + handlePlaceholderWidgetKey: noop, + handleRemoveWidget: noop, + isEditView: false, + }); + }, [widgetConfig]); + + return ( +
+ {widget} +
+ ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx deleted file mode 100644 index 59351bf18944..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.component.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2022 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 { Col, Row } from 'antd'; -import { t } from 'i18next'; -import { lowerCase } from 'lodash'; -import React, { FunctionComponent, useState } from 'react'; -import Searchbar from '../../common/SearchBarComponent/SearchBar.component'; -import SchemaTable from '../SchemaTable/SchemaTable.component'; -import { Props } from './SchemaTab.interfaces'; - -const SchemaTab: FunctionComponent = ({ - onUpdate, - hasDescriptionEditAccess, - hasTagEditAccess, - hasGlossaryTermEditAccess, - onThreadLinkSelect, - isReadOnly = false, -}: Props) => { - const [searchText, setSearchText] = useState(''); - - const handleSearchAction = (searchValue: string) => { - setSearchText(searchValue); - }; - - return ( - -
- - - - - - - ); -}; - -export default SchemaTab; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts deleted file mode 100644 index 0f56946152fc..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.interfaces.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2022 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 { ThreadType } from '../../../generated/api/feed/createThread'; -import { Table } from '../../../generated/entity/data/table'; - -export type Props = { - hasDescriptionEditAccess: boolean; - hasTagEditAccess: boolean; - hasGlossaryTermEditAccess: boolean; - isReadOnly?: boolean; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; - onUpdate: (columns: Table['columns']) => Promise; -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx deleted file mode 100644 index 6debb7573140..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTab/SchemaTab.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 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 { getByTestId, getByText, render } from '@testing-library/react'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { MOCK_TABLE } from '../../../mocks/TableData.mock'; -import SchemaTab from './SchemaTab.component'; - -const mockUpdate = jest.fn(); - -jest.mock('../SampleDataTable/SampleDataTable.component', () => { - return jest.fn().mockReturnValue(

SampleDataTable

); -}); - -jest.mock('../SchemaTable/SchemaTable.component', () => { - return jest.fn().mockReturnValue(

SchemaTable

); -}); - -jest.mock('../../GenericProvider/GenericProvider', () => ({ - useGenericContext: jest.fn().mockReturnValue({ - data: MOCK_TABLE, - permissions: {}, - }), -})); - -describe('Test SchemaTab Component', () => { - it('Renders all the parts of the schema tab', () => { - const { queryByTestId, container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const searchBar = getByTestId(container, 'search-bar-container'); - - expect(searchBar).toBeInTheDocument(); - - const schemaTable = getByText(container, /SchemaTable/i); - - expect(schemaTable).toBeInTheDocument(); - expect(queryByTestId('sample-data-table')).toBeNull(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index 3f231fea4358..ad1b8908ded9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ -import { Button, Form, Select, Tooltip, Typography } from 'antd'; +import { Button, Col, Form, Row, Select, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ExpandableConfig } from 'antd/lib/table/interface'; import { @@ -72,6 +72,7 @@ import { updateFieldTags, } from '../../../utils/TableUtils'; import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTablePlaceHolder'; +import Searchbar from '../../common/SearchBarComponent/SearchBar.component'; import Table from '../../common/Table/Table'; import TestCaseStatusSummaryIndicator from '../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component'; import { useGenericContext } from '../../GenericProvider/GenericProvider'; @@ -85,42 +86,40 @@ import { ColumnFilter } from '../ColumnFilter/ColumnFilter.component'; import TableDescription from '../TableDescription/TableDescription.component'; import TableTags from '../TableTags/TableTags.component'; import { - SchemaTableProps, TableCellRendered, UpdatedColumnFieldData, } from './SchemaTable.interface'; -const SchemaTable = ({ - searchText, - hasDescriptionEditAccess, - hasTagEditAccess, - hasGlossaryTermEditAccess, - isReadOnly = false, - onUpdate, - onThreadLinkSelect, -}: SchemaTableProps) => { +const SchemaTable = () => { const { t } = useTranslation(); const [testCaseSummary, setTestCaseSummary] = useState(); const [searchedColumns, setSearchedColumns] = useState([]); const [expandedRowKeys, setExpandedRowKeys] = useState([]); + const [searchText, setSearchText] = useState(''); const [editColumn, setEditColumn] = useState(); const { fqn: decodedEntityFqn } = useFqn(); const [editColumnDisplayName, setEditColumnDisplayName] = useState(); - const { permissions: tablePermissions, data: table } = - useGenericContext(); - - const { testCaseCounts, tableColumns, joins, tableConstraints } = useMemo( - () => ({ - testCaseCounts: testCaseSummary?.columnTestSummary ?? [], - tableColumns: table?.columns ?? [], - joins: table?.joins?.columnJoins ?? [], - tableConstraints: table?.tableConstraints, - }), - [table, testCaseSummary] - ); + const { + permissions: tablePermissions, + data: table, + onUpdate, + onThreadLinkSelect, + } = useGenericContext(); + + const { testCaseCounts, tableColumns, joins, tableConstraints, deleted } = + useMemo( + () => ({ + testCaseCounts: testCaseSummary?.columnTestSummary ?? [], + tableColumns: table?.columns ?? [], + joins: table?.joins?.columnJoins ?? [], + tableConstraints: table?.tableConstraints, + deleted: table?.deleted, + }), + [table, testCaseSummary] + ); const tableFqn = useMemo( () => @@ -132,6 +131,42 @@ const SchemaTable = ({ [decodedEntityFqn] ); + const { + editTagsPermission, + editGlossaryTermsPermission, + editDescriptionPermission, + editDisplayNamePermission, + } = useMemo( + () => ({ + editTagsPermission: + (tablePermissions.EditTags || tablePermissions.EditAll) && !deleted, + editDescriptionPermission: + (tablePermissions.EditDescription || tablePermissions.EditAll) && + !deleted, + editGlossaryTermsPermission: + (tablePermissions.EditGlossaryTerms || tablePermissions.EditAll) && + !deleted, + editAllPermission: tablePermissions.EditAll && !deleted, + editLineagePermission: + (tablePermissions.EditAll || tablePermissions.EditLineage) && !deleted, + viewSampleDataPermission: + tablePermissions.ViewAll || tablePermissions.ViewSampleData, + viewQueriesPermission: + tablePermissions.ViewAll || tablePermissions.ViewQueries, + viewProfilerPermission: + tablePermissions.ViewAll || + tablePermissions.ViewDataProfile || + tablePermissions.ViewTests, + viewAllPermission: tablePermissions.ViewAll, + viewBasicPermission: + tablePermissions.ViewAll || tablePermissions.ViewBasic, + editDisplayNamePermission: + (tablePermissions.EditDisplayName || tablePermissions.EditAll) && + !deleted, + }), + [tablePermissions, deleted] + ); + const sortByOrdinalPosition = useMemo( () => sortBy(tableColumns, 'ordinalPosition'), [tableColumns] @@ -168,6 +203,16 @@ const SchemaTable = ({ setEditColumn(undefined); }; + const handleColumnUpdate = async (updatedColumns: Column[]) => { + if (table && !isEqual(tableColumns, updatedColumns)) { + const updatedTableDetails = { + ...table, + columns: updatedColumns, + }; + await onUpdate(updatedTableDetails); + } + }; + const updateColumnFields = ({ fqn, field, @@ -197,7 +242,7 @@ const SchemaTable = ({ field: 'description', columns: tableCols, }); - await onUpdate(tableCols); + await handleColumnUpdate(tableCols); setEditColumn(undefined); } else { setEditColumn(undefined); @@ -216,7 +261,7 @@ const SchemaTable = ({ selectedTags, tableCols ); - await onUpdate(tableCols); + await handleColumnUpdate(tableCols); } }; @@ -260,9 +305,9 @@ const SchemaTable = ({ }} entityFqn={tableFqn} entityType={EntityType.TABLE} - hasEditPermission={hasDescriptionEditAccess} + hasEditPermission={editDescriptionPermission} index={index} - isReadOnly={isReadOnly} + isReadOnly={deleted} onClick={() => handleUpdate(record)} onThreadLinkSelect={onThreadLinkSelect} /> @@ -325,7 +370,7 @@ const SchemaTable = ({ field: 'constraint', columns: tableCols, }); - await onUpdate(tableCols); + await handleColumnUpdate(tableCols); setEditColumnDisplayName(undefined); } else { setEditColumnDisplayName(undefined); @@ -341,6 +386,10 @@ const SchemaTable = ({ >; }, [data]); + const handleSearchAction = (searchValue: string) => { + setSearchText(searchValue); + }; + const columns: ColumnsType = useMemo( () => [ { @@ -379,26 +428,24 @@ const SchemaTable = ({ ) : null} - {(tablePermissions?.EditAll || - tablePermissions?.EditDisplayName) && - !isReadOnly && ( - - - - )} + {editDisplayNamePermission && ( + + + + )} ); }, @@ -431,9 +478,9 @@ const SchemaTable = ({ entityFqn={tableFqn} entityType={EntityType.TABLE} handleTagSelection={handleTagSelection} - hasTagEditAccess={hasTagEditAccess} + hasTagEditAccess={editTagsPermission} index={index} - isReadOnly={isReadOnly} + isReadOnly={deleted} record={record} tags={tags} type={TagSource.Classification} @@ -456,9 +503,9 @@ const SchemaTable = ({ entityFqn={tableFqn} entityType={EntityType.TABLE} handleTagSelection={handleTagSelection} - hasTagEditAccess={hasGlossaryTermEditAccess} + hasTagEditAccess={editGlossaryTermsPermission} index={index} - isReadOnly={isReadOnly} + isReadOnly={deleted} record={record} tags={tags} type={TagSource.Glossary} @@ -490,10 +537,10 @@ const SchemaTable = ({ ], [ tableFqn, - isReadOnly, + deleted, tableConstraints, - hasTagEditAccess, - hasGlossaryTermEditAccess, + editTagsPermission, + editGlossaryTermsPermission, handleUpdate, handleTagSelection, renderDataTypeDisplay, @@ -545,22 +592,33 @@ const SchemaTable = ({ }, [data, decodedEntityFqn]); return ( - <> -
, - }} - pagination={false} - rowKey="fullyQualifiedName" - scroll={TABLE_SCROLL_VALUE} - size="middle" - /> + + + + + +
, + }} + pagination={false} + rowKey="fullyQualifiedName" + scroll={TABLE_SCROLL_VALUE} + size="middle" + /> + {editColumn && ( )} - + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts index db1e9794eb54..25f5efd97ce2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.interface.ts @@ -12,19 +12,8 @@ */ import { ReactNode } from 'react'; -import { ThreadType } from '../../../generated/api/feed/createThread'; import { Column } from '../../../generated/entity/data/table'; -export interface SchemaTableProps { - hasDescriptionEditAccess: boolean; - hasTagEditAccess: boolean; - hasGlossaryTermEditAccess: boolean; - searchText?: string; - isReadOnly?: boolean; - onUpdate: (columns: Column[]) => Promise; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; -} - export type TableCellRendered = ( value: T[K], record: T, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx index 669871fc934f..68c74d60d1d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx @@ -18,11 +18,7 @@ import { Column } from '../../../generated/entity/data/container'; import { Table } from '../../../generated/entity/data/table'; import { MOCK_TABLE } from '../../../mocks/TableData.mock'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; -import EntityTableV1 from './SchemaTable.component'; -import { SchemaTableProps } from './SchemaTable.interface'; - -const onThreadLinkSelect = jest.fn(); -const onUpdate = jest.fn(); +import SchemaTable from './SchemaTable.component'; const mockTableConstraints = [ { @@ -68,16 +64,6 @@ const mockColumns = [ }, ] as Column[]; -const mockEntityTableProp: SchemaTableProps = { - searchText: '', - hasDescriptionEditAccess: true, - isReadOnly: false, - hasTagEditAccess: true, - hasGlossaryTermEditAccess: true, - onThreadLinkSelect, - onUpdate, -}; - const mockGenericContextProps = { data: { ...MOCK_TABLE, @@ -198,7 +184,7 @@ jest.mock('../../../utils/EntityUtils', () => ({ describe('Test EntityTable Component', () => { it('Initially, Table should load', async () => { - render(, { + render(, { wrapper: MemoryRouter, }); @@ -210,7 +196,7 @@ describe('Test EntityTable Component', () => { }); it('Should render tags and description components', async () => { - render(, { + render(, { wrapper: MemoryRouter, }); @@ -225,7 +211,7 @@ describe('Test EntityTable Component', () => { it('Table should load empty when no data present', async () => { mockGenericContextProps.data = { ...MOCK_TABLE, columns: [] } as Table; - render(, { + render(, { wrapper: MemoryRouter, }); @@ -243,7 +229,7 @@ describe('Test EntityTable Component', () => { ...MOCK_TABLE, columns: mockColumns, } as Table; - render(, { + render(, { wrapper: MemoryRouter, }); @@ -261,7 +247,7 @@ describe('Test EntityTable Component', () => { ...MOCK_TABLE, columns: columnsWithDisplayName, } as Table; - render(, { + render(, { wrapper: MemoryRouter, }); @@ -282,7 +268,7 @@ describe('Test EntityTable Component', () => { ...MOCK_TABLE, columns: columnsWithDisplayName, } as Table; - render(, { + render(, { wrapper: MemoryRouter, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx new file mode 100644 index 000000000000..ce725f5f3c2c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2024 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 React, { useMemo } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; +import { Page, PageType, Tab } from '../../../generated/system/ui/page'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; +import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; +import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; +import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; +import tableClassBase from '../../../utils/TableClassBase'; +import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; +import SchemaTable from '../SchemaTable/SchemaTable.component'; + +const ReactGridLayout = WidthProvider(RGL); + +export const TableGenericTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return tableClassBase.getDefaultLayout(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.Table + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return getDefaultWidgetForTab(PageType.Table, tab); + } + }, [currentPersonaDocStore, tab]); + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) + ) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) + ) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS) + ) { + return ; + } else { + return ; + } + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx deleted file mode 100644 index 1bd3514bfab6..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableSchemaTab/TableSchemaTab.tsx +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright 2024 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 { AxiosError } from 'axios'; -import { isEmpty, isEqual, noop } from 'lodash'; -import { EntityTags } from 'Models'; -import React, { useMemo, useState } from 'react'; -import RGL, { WidthProvider } from 'react-grid-layout'; -import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; -import { EntityTabs, EntityType } from '../../../enums/entity.enum'; -import { CreateThread } from '../../../generated/api/feed/createThread'; -import { - Table, - TagLabel, - TagSource, -} from '../../../generated/entity/data/table'; -import { ThreadType } from '../../../generated/entity/feed/thread'; -import { Page, PageType, Tab } from '../../../generated/system/ui/page'; -import { useFqn } from '../../../hooks/useFqn'; -import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; -import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; -import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; -import { postThread } from '../../../rest/feedsAPI'; -import { getEntityName } from '../../../utils/EntityUtils'; -import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; -import tableClassBase from '../../../utils/TableClassBase'; -import { - getJoinsFromTableJoins, - getTagsWithoutTier, - getTierTags, -} from '../../../utils/TableUtils'; -import { createTagObject } from '../../../utils/TagsUtils'; -import { showErrorToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; -import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; -import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; -import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; -import SchemaTab from '../SchemaTab/SchemaTab.component'; - -const ReactGridLayout = WidthProvider(RGL); - -export const TableSchemaTab = () => { - const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); - const { fqn: tableFqn } = useFqn(); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const { t } = useTranslation(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const { - data: tableDetails, - permissions: tablePermissions, - onUpdate, - type: entityType, - } = useGenericContext
(); - - const layout = useMemo(() => { - if (!currentPersonaDocStore) { - return tableClassBase.getDefaultLayout(tab); - } - - const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.Table - ); - - if (page) { - return page.tabs.find((t: Tab) => t.id === tab)?.layout; - } else { - return tableClassBase.getDefaultLayout(tab); - } - }, [currentPersonaDocStore, tab]); - const { - tier, - tableTags, - deleted, - columns, - owners, - domain, - extension, - tablePartition, - dataProducts, - description, - entityName, - joinedTables = [], - } = useMemo(() => { - const { tags } = tableDetails; - - const { joins } = tableDetails ?? {}; - - return { - ...tableDetails, - tier: getTierTags(tags ?? []), - tableTags: getTagsWithoutTier(tags ?? []), - entityName: getEntityName(tableDetails), - joinedTables: getJoinsFromTableJoins(joins), - }; - }, [tableDetails, tableDetails?.tags]); - - const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, - editAllPermission, - viewAllPermission, - } = useMemo( - () => ({ - editTagsPermission: - (tablePermissions.EditTags || tablePermissions.EditAll) && !deleted, - editDescriptionPermission: - (tablePermissions.EditDescription || tablePermissions.EditAll) && - !deleted, - editGlossaryTermsPermission: - (tablePermissions.EditGlossaryTerms || tablePermissions.EditAll) && - !deleted, - editCustomAttributePermission: - (tablePermissions.EditAll || tablePermissions.EditCustomFields) && - !deleted, - editAllPermission: tablePermissions.EditAll && !deleted, - editLineagePermission: - (tablePermissions.EditAll || tablePermissions.EditLineage) && !deleted, - viewSampleDataPermission: - tablePermissions.ViewAll || tablePermissions.ViewSampleData, - viewQueriesPermission: - tablePermissions.ViewAll || tablePermissions.ViewQueries, - viewProfilerPermission: - tablePermissions.ViewAll || - tablePermissions.ViewDataProfile || - tablePermissions.ViewTests, - viewAllPermission: tablePermissions.ViewAll, - viewBasicPermission: - tablePermissions.ViewAll || tablePermissions.ViewBasic, - }), - [tablePermissions, deleted] - ); - - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - - const descriptionWidget = useMemo(() => { - return ( - { - if (value !== description) { - await onUpdate({ ...tableDetails, description: value }); - } - }} - onThreadLinkSelect={onThreadLinkSelect} - /> - ); - }, [tablePermissions, tableDetails, deleted, entityName, deleted, owners]); - - /** - * Formulates updated tags and updates table entity data for API call - * @param selectedTags - */ - const handleTagsUpdate = async (selectedTags?: Array) => { - if (selectedTags && tableDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedTable = { ...tableDetails, tags: updatedTags }; - await onUpdate(updatedTable); - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleTagsUpdate(updatedTags); - }; - - const tagsWidget = useMemo(() => { - return ( - - ); - }, []); - - const glossaryWidget = useMemo(() => { - return ( - - ); - }, []); - - const onColumnsUpdate = async (updateColumns: Table['columns']) => { - if (tableDetails && !isEqual(columns, updateColumns)) { - const updatedTableDetails = { - ...tableDetails, - columns: updateColumns, - }; - await onUpdate(updatedTableDetails); - } - }; - - const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DESCRIPTION)) { - return descriptionWidget; - } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { - return ( - - ); - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) - ) { - return ( - - await onUpdate({ ...tableDetails, tableConstraints: constraint }) - } - /> - ); - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) - ) { - return isEmpty(joinedTables) ? null : ( - - ); - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.DATA_PRODUCTS) - ) { - return ( - - ); - } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TAGS)) { - return tagsWidget; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.GLOSSARY_TERMS) - ) { - return glossaryWidget; - } else if ( - // widgetConfig.i.startsWith(DetailPageWidgetKeys.KNOWLEDGE_ARTICLES) - // ) { - // return ( - // - // ); - // } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.CUSTOM_PROPERTIES) - ) { - return ( - - isRenderedInRightPanel - entityDetails={extension} - entityType={EntityType.TABLE} - handleExtensionUpdate={onUpdate} - hasEditAccess={Boolean(editCustomAttributePermission)} - hasPermission={Boolean(viewAllPermission)} - maxDataCap={5} - /> - ); - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS) - ) { - return tablePartition ? ( - - ) : null; - } - - return getWidgetFromKey({ - widgetConfig: widgetConfig, - handleOpenAddWidgetModal: noop, - handlePlaceholderWidgetKey: noop, - handleRemoveWidget: noop, - isEditView: false, - }); - }; - - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout, descriptionWidget, tagsWidget, glossaryWidget]); - - // call the hook to set the direction of the grid layout - useGridLayoutDirection(); - - return ( - <> - - {widgets} - - {threadLink ? ( - - ) : null} - - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index cb3d0e7d4318..825889f8e7c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -124,9 +124,7 @@ const EntityRightPanel = ({ maxDataCap={5} /> )} - {tablePartition ? ( - - ) : null} + {tablePartition ? : null} {afterSlot} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx index 89f32dfeedfc..590795a819f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx @@ -10,12 +10,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { AxiosError } from 'axios'; import { once } from 'lodash'; -import React, { useContext, useMemo } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../../enums/entity.enum'; +import { CreateThread } from '../../generated/api/feed/createThread'; +import { ThreadType } from '../../generated/entity/feed/thread'; +import { EntityReference } from '../../generated/entity/type'; +import { postThread } from '../../rest/feedsAPI'; +import { showErrorToast } from '../../utils/ToastUtils'; +import { useActivityFeedProvider } from '../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; -interface GenericProviderProps { +interface GenericProviderProps> { children?: React.ReactNode; data: T; type: EntityType; @@ -25,20 +34,21 @@ interface GenericProviderProps { currentVersionData?: T; } -interface GenericContextType { +interface GenericContextType> { data: T; type: EntityType; onUpdate: (updatedData: T) => Promise; isVersionView?: boolean; permissions: OperationPermission; currentVersionData?: T; + onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; } -const createGenericContext = once(() => +const createGenericContext = once(>() => React.createContext({} as GenericContextType) ); -export const GenericProvider = ({ +export const GenericProvider = >({ children, data, type, @@ -49,6 +59,37 @@ export const GenericProvider = ({ }: GenericProviderProps) => { const GenericContext = createGenericContext(); + const [threadLink, setThreadLink] = useState(''); + const [threadType, setThreadType] = useState( + ThreadType.Conversation + ); + const { t } = useTranslation(); + const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); + + const onThreadPanelClose = () => { + setThreadLink(''); + }; + + const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { + setThreadLink(link); + if (threadType) { + setThreadType(threadType); + } + }; + + const createThread = async (data: CreateThread) => { + try { + await postThread(data); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.create-entity-error', { + entity: t('label.conversation'), + }) + ); + } + }; + const values = useMemo( () => ({ data, @@ -57,14 +98,37 @@ export const GenericProvider = ({ isVersionView, permissions, currentVersionData, + onThreadLinkSelect, }), - [data, type, onUpdate, isVersionView, permissions, currentVersionData] + [ + data, + type, + onUpdate, + isVersionView, + permissions, + currentVersionData, + onThreadLinkSelect, + ] ); return ( - {children} + + {children} + {threadLink ? ( + + ) : null} + ); }; -export const useGenericContext = () => +export const useGenericContext = >() => useContext(createGenericContext()); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 913d7a5466ed..f565b69b7827 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -33,7 +33,6 @@ import dashboardDataModelClassBase from '../../../../utils/DashboardDataModelBas import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; -import { getJoinsFromTableJoins } from '../../../../utils/TableUtils'; import topicClassBase from '../../../../utils/TopicClassBase'; import { ExtensionTable } from '../../../common/CustomPropertyTable/ExtensionTable'; import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; @@ -533,24 +532,20 @@ export const GenericWidget = (props: WidgetCommonProps) => { permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.TABLE} onUpdate={async () => noop()}> - noop()} - /> + ); } else if ( props.widgetKey.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) ) { return ( - + + data={tableClassBase.getDummyData()} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.TABLE} + onUpdate={async () => noop()}> + + ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.DATA_PRODUCTS)) { return ( @@ -577,11 +572,13 @@ export const GenericWidget = (props: WidgetCommonProps) => { props.widgetKey.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) ) { return ( - noop()} - /> + + data={tableClassBase.getDummyData()} + permissions={DEFAULT_ENTITY_PERMISSION} + type={EntityType.TABLE} + onUpdate={async () => noop()}> + + ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { return ( @@ -590,7 +587,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.TOPIC} onUpdate={async () => noop()}> - + ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.DATA_MODEL)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index ef2b59456083..ef5039505a71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -13,14 +13,12 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty } from 'lodash'; import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; @@ -47,20 +45,17 @@ import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeed import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import QueryViewer from '../../common/QueryViewer/QueryViewer.component'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import SampleDataWithMessages from '../../Database/SampleDataWithMessages/SampleDataWithMessages'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; import { SourceType } from '../../SearchedData/SearchedData.interface'; -import TopicSchemaFields from '../TopicSchema/TopicSchema'; +import { TopicSchemaTab } from '../TopicSchemaTab/TopicSchemaTab'; import { TopicDetailsProps } from './TopicDetails.interface'; const TopicDetails: React.FC = ({ @@ -306,67 +301,7 @@ const TopicDetails: React.FC = ({ /> ), key: EntityTabs.SCHEMA, - children: ( - -
- - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - customProperties={topicDetails} - dataProducts={topicDetails?.dataProducts ?? []} - domain={topicDetails?.domain} - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityId={topicDetails.id} - entityType={EntityType.TOPIC} - selectedTags={topicTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx index ae665073085f..5fd3d1a58de5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.interface.tsx @@ -13,16 +13,13 @@ import { TableProps } from 'antd'; import { HTMLAttributes, ReactNode } from 'react'; -import { ThreadType } from '../../../generated/api/feed/createThread'; import { Field } from '../../../generated/entity/data/topic'; export interface TopicSchemaFieldsProps extends HTMLAttributes> { - isReadOnly: boolean; schemaTypePlaceholder?: ReactNode; defaultExpandAllRows?: boolean; showSchemaDisplayTypeSwitch?: boolean; - onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; } export enum SchemaViewType { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx index f961cd0c2d1c..cbf32ea742c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx @@ -27,10 +27,7 @@ import { MESSAGE_SCHEMA } from '../TopicDetails/TopicDetails.mock'; import TopicSchema from './TopicSchema'; import { TopicSchemaFieldsProps } from './TopicSchema.interface'; -const mockProps: TopicSchemaFieldsProps = { - isReadOnly: false, - onThreadLinkSelect: jest.fn(), -}; +const mockProps: TopicSchemaFieldsProps = {}; jest.mock('../../../utils/TagsUtils', () => ({ getAllTagsList: jest.fn().mockImplementation(() => Promise.resolve([])), @@ -187,7 +184,7 @@ describe('Topic Schema', () => { }); it('Should not render the edit action if isReadOnly', async () => { - render(); + render(); const rows = await screen.findAllByRole('row'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index e307e571169c..3ce13f63e699 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -67,8 +67,6 @@ import { const TopicSchemaFields: FC = ({ className, - isReadOnly, - onThreadLinkSelect, schemaTypePlaceholder, }) => { const { t } = useTranslation(); @@ -84,8 +82,14 @@ const TopicSchemaFields: FC = ({ permissions, onUpdate, currentVersionData, + onThreadLinkSelect, } = useGenericContext(); + const isReadOnly = useMemo(() => { + // If there is a current version, it should be read only + return currentVersionData ? true : topicDetails.deleted; + }, [currentVersionData, topicDetails.deleted]); + const messageSchema = useMemo( () => isVersionView && currentVersionData?.changeDescription diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx new file mode 100644 index 000000000000..6db2df5d210f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2025 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 React, { useMemo } from 'react'; +import ReactGridLayout from 'react-grid-layout'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; +import { Page, PageType, Tab } from '../../../generated/system/ui/page'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; +import topicClassBase from '../../../utils/TopicClassBase'; +import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; +import TopicSchemaFields from '../TopicSchema/TopicSchema'; + +export const TopicSchemaTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return topicClassBase.getDefaultLayout(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.Table + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return topicClassBase.getDefaultLayout(tab); + } + }, [currentPersonaDocStore, tab]); + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { + return ; + } else { + return ; + } + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx index 2dcd1b198e49..f08e8acbb846 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx @@ -13,7 +13,7 @@ import { Col, Row, Space, Tabs, TabsProps, Tag } from 'antd'; import classNames from 'classnames'; -import { isEmpty, noop } from 'lodash'; +import { isEmpty } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; @@ -141,11 +141,7 @@ const TopicVersion: FC = ({ />
- + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx index e08782675d3f..fef044b402e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx @@ -11,27 +11,34 @@ * limitations under the License. */ import { Col, Row, Space, Typography } from 'antd'; -import React from 'react'; +import { isEmpty } from 'lodash'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; import { getEntityDetailsPath } from '../../../constants/constants'; import { EntityType } from '../../../enums/entity.enum'; -import { JoinedWith } from '../../../generated/entity/data/table'; +import { JoinedWith, Table } from '../../../generated/entity/data/table'; import { getCountBadge } from '../../../utils/CommonUtils'; +import { getJoinsFromTableJoins } from '../../../utils/TableUtils'; import './frequently-joined-tables.style.less'; export type Joined = JoinedWith & { name: string; }; -interface FrequentlyJoinedTablesProps { - joinedTables: Joined[]; -} - -export const FrequentlyJoinedTables = ({ - joinedTables, -}: FrequentlyJoinedTablesProps) => { +export const FrequentlyJoinedTables = () => { const { t } = useTranslation(); + const { data } = useGenericContext
(); + + const joinedTables = useMemo( + () => getJoinsFromTableJoins(data?.joins), + [data?.joins] + ); + + if (isEmpty(joinedTables)) { + return null; + } return ( { it('should render the component', async () => { - render(, { + render(, { wrapper: MemoryRouter, }); @@ -43,7 +33,7 @@ describe('FrequentlyJoinedTables component', () => { }); it("should show the table's name and join count", async () => { - render(, { + render(, { wrapper: MemoryRouter, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx index c3cece12b32a..5064063be2b0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx @@ -10,27 +10,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Table, Typography } from 'antd'; +import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { t } from 'i18next'; import React, { useMemo } from 'react'; +import Table from '../../../components/common/Table/Table'; +import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; import { PartitionColumnDetails, - TablePartition, + Table as TableType, } from '../../../generated/entity/data/table'; -interface PartitionedKeysProps { - tablePartition: TablePartition; -} +export const PartitionedKeys = () => { + const { data } = useGenericContext(); -export const PartitionedKeys = ({ tablePartition }: PartitionedKeysProps) => { const partitionColumnDetails = useMemo( () => - tablePartition?.columns?.map((column) => ({ + data?.tablePartition?.columns?.map((column) => ({ ...column, key: column.columnName, })), - [tablePartition.columns] + [data?.tablePartition?.columns] ); const columns = useMemo(() => { @@ -57,6 +57,10 @@ export const PartitionedKeys = ({ tablePartition }: PartitionedKeysProps) => { return data; }, []); + if (!data.tablePartition) { + return null; + } + return ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx index 46bbbcd00e7e..73ac4cd6f2bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx @@ -12,12 +12,13 @@ */ import { Button, Space, Tooltip, Typography } from 'antd'; import { isEmpty, map } from 'lodash'; -import React, { FC, useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg'; import TagButton from '../../../components/common/TagButton/TagButton.component'; +import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants'; import { EntityType, FqnPart } from '../../../enums/entity.enum'; @@ -29,19 +30,17 @@ import ForeignKeyConstraint from './ForeignKeyConstraint'; import './table-constraints.less'; import TableConstraintsModal from './TableConstraintsModal/TableConstraintsModal.component'; -interface TableConstraintsProps { - hasPermission: boolean; - tableDetails?: Table; - onUpdate: (updateData: Table['tableConstraints']) => Promise; -} - -const TableConstraints: FC = ({ - tableDetails, - hasPermission, - onUpdate, -}) => { +const TableConstraints = () => { const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); + const { data, permissions, onUpdate } = useGenericContext
(); + + const { deleted } = data ?? {}; + + const hasPermission = useMemo( + () => permissions.EditAll && !deleted, + [permissions, deleted] + ); const handleOpenEditConstraintModal = useCallback( () => setIsModalOpen(true), @@ -53,7 +52,7 @@ const TableConstraints: FC = ({ ); const handleSubmit = async (values: Table['tableConstraints']) => { - await onUpdate(values); + await onUpdate({ ...data, tableConstraints: values }); setIsModalOpen(false); }; @@ -65,7 +64,7 @@ const TableConstraints: FC = ({ {t('label.table-constraints')} - {hasPermission && !isEmpty(tableDetails?.tableConstraints) && ( + {hasPermission && !isEmpty(data?.tableConstraints) && ( = ({ )} - {hasPermission && isEmpty(tableDetails?.tableConstraints) && ( + {hasPermission && isEmpty(data?.tableConstraints) && ( = ({ /> )} - {tableDetails?.tableConstraints?.map( + {data?.tableConstraints?.map( ({ constraintType, columns, referredColumns }) => { if (constraintType === ConstraintType.PrimaryKey) { return tableConstraintRendererBasedOnType( @@ -180,8 +179,8 @@ const TableConstraints: FC = ({ {isModalOpen && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 6a70f1469c8a..a27838bdc0e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -27,7 +27,6 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error import Loader from '../../components/common/Loader/Loader'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { TableSchemaTab } from '../../components/Database/TableSchemaTab/TableSchemaTab'; import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; @@ -495,13 +494,10 @@ const TableDetailsPageV1: React.FC = () => { [tablePermissions, deleted] ); - const schemaTab = useMemo(() => , []); - const tabs = useMemo(() => { const tabLabelMap = getTabLabelMap(customizedPage?.tabs); const tabs = tableClassBase.getTableDetailPageTabs({ - schemaTab, queryCount, isTourOpen, tablePermissions, @@ -532,7 +528,6 @@ const TableDetailsPageV1: React.FC = () => { return updatedTabs; }, [ - schemaTab, queryCount, isTourOpen, tablePermissions, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index f8752e7fece6..01b03f239c3e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -43,7 +43,6 @@ export interface TableDetailPageTabProps { isTourOpen: boolean; activeTab: EntityTabs; totalFeedCount: number; - schemaTab: JSX.Element; isViewTableType: boolean; viewAllPermission: boolean; viewQueriesPermission: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 67b31d4ed4c3..795d06d9a412 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -158,6 +158,7 @@ import { ReactComponent as IconUnknown } from '../assets/svg/data-type-icon/unkn import { ReactComponent as IconVarchar } from '../assets/svg/data-type-icon/varchar.svg'; import { ReactComponent as IconVariant } from '../assets/svg/data-type-icon/variant.svg'; import { ReactComponent as IconXML } from '../assets/svg/data-type-icon/xml.svg'; +import { TableGenericTab } from '../components/Database/TableGenericTab/TableGenericTab'; import { Joined } from '../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import ConstraintIcon from '../pages/TableDetailsPageV1/TableConstraints/ConstraintIcon'; @@ -731,7 +732,6 @@ export const updateFieldDescription = ( }; export const getTableDetailPageBaseTabs = ({ - schemaTab, queryCount, isTourOpen, tablePermissions, @@ -764,7 +764,7 @@ export const getTableDetailPageBaseTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: schemaTab, + children: , }, { label: ( From 581d70652d5ae3158252b2905d5996524aa2bd5d Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:19:01 +0530 Subject: [PATCH 19/63] update --- .../ContainerChildren/ContainerChildren.tsx | 4 +- .../DataModels/DataModelDetails.component.tsx | 13 ++++--- .../DataModels/DataModelDetails.interface.tsx | 5 ++- .../ModelTab/ModelTab.component.tsx | 3 ++ .../EntityRightPanel/EntityRightPanel.tsx | 5 +-- .../src/pages/ContainerPage/ContainerPage.tsx | 39 ++++++++++++------- .../DataModelPage/DataModelPage.component.tsx | 4 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 1 - 8 files changed, 47 insertions(+), 27 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx index 48fbb6a3b382..f780250ee39d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx @@ -36,7 +36,9 @@ const ContainerChildren: FC = ({ }) => { const { t } = useTranslation(); const { data } = useGenericContext(); - const { children: childrenList } = data ?? {}; + const { children: childrenList } = useMemo(() => { + return data ?? {}; + }, [data?.children]); const columns: ColumnsType = useMemo( () => [ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 40bed050f3bb..cfb198c9b63e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -115,7 +115,7 @@ const DataModelDetails = ({ displayName: data.displayName, }; - await onUpdateDataModel(updatedData); + await onUpdateDataModel(updatedData, 'displayName'); }; const versionHandler = () => { @@ -207,10 +207,13 @@ const DataModelDetails = ({ const handelExtensionUpdate = useCallback( async (updatedDataModel: DashboardDataModel) => { - await onUpdateDataModel({ - ...dataModelData, - extension: updatedDataModel.extension, - }); + await onUpdateDataModel( + { + ...dataModelData, + extension: updatedDataModel.extension, + }, + 'extension' + ); }, [onUpdateDataModel, dataModelData] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx index 77ca31dc4237..8b03e4027d1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx @@ -34,6 +34,9 @@ export interface DataModelDetailsProps { handleUpdateDescription: (value: string) => Promise; handleColumnUpdateDataModel: (updatedDataModel: Column[]) => Promise; onUpdateVote: (data: QueryVote, id: string) => Promise; - onUpdateDataModel: (updatedDataModel: DashboardDataModel) => Promise; + onUpdateDataModel: ( + updatedDataModel: DashboardDataModel, + key?: keyof DashboardDataModel + ) => Promise; handleToggleDelete: (version?: number) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx index bc38bf5d3e73..9e7dbe516e8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx @@ -27,6 +27,7 @@ import { getColumnSorter, getEntityName, } from '../../../../../utils/EntityUtils'; +import { columnFilterIcon } from '../../../../../utils/TableColumn.util'; import { getAllTags, searchTagInData, @@ -160,6 +161,7 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { accessor: 'tags', width: 250, filters: tagFilter.Classification, + filterIcon: columnFilterIcon, filterDropdown: ColumnFilter, onFilter: searchTagInData, render: (tags: TagLabel[], record: Column, index: number) => ( @@ -183,6 +185,7 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { key: 'glossary', accessor: 'tags', width: 250, + filterIcon: columnFilterIcon, filters: tagFilter.Glossary, filterDropdown: ColumnFilter, onFilter: searchTagInData, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index 825889f8e7c9..d729bad96427 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -14,7 +14,6 @@ import { Space } from 'antd'; import { EntityTags } from 'Models'; import React from 'react'; import { EntityType } from '../../../enums/entity.enum'; -import { TablePartition } from '../../../generated/entity/data/table'; import { ThreadType } from '../../../generated/entity/feed/thread'; import { EntityReference } from '../../../generated/entity/type'; import { TagSource } from '../../../generated/type/tagLabel'; @@ -46,7 +45,6 @@ interface EntityRightPanelProps { onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; viewAllPermission?: boolean; customProperties?: ExtentionEntities[T]; - tablePartition?: TablePartition; editCustomAttributePermission?: boolean; onExtensionUpdate?: (updatedTable: ExtentionEntities[T]) => Promise; } @@ -67,7 +65,6 @@ const EntityRightPanel = ({ showDataProductContainer = true, viewAllPermission, customProperties, - tablePartition, editCustomAttributePermission, onExtensionUpdate, }: EntityRightPanelProps) => { @@ -124,7 +121,7 @@ const EntityRightPanel = ({ maxDataCap={5} /> )} - {tablePartition ? : null} + {afterSlot} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 0df4e57f329d..6852d17e895d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -21,11 +21,11 @@ import { useHistory, useParams } from 'react-router-dom'; import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; - import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; +import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -289,7 +289,7 @@ const ContainerPage = () => { }; const handleUpdateContainerData = useCallback( - (updatedData: Container) => { + async (updatedData: Container) => { const jsonPatch = compare( omitBy(containerData, isUndefined), updatedData @@ -569,6 +569,11 @@ const ContainerPage = () => { } }; + const handleContainerUpdate = async (updatedData: Container) => { + const updatedContainer = await handleUpdateContainerData(updatedData); + setContainerData(updatedContainer); + }; + const tabs = useMemo(() => { const tabLabelMap = getTabLabelMap(customizedPage?.tabs); @@ -733,18 +738,24 @@ const ContainerPage = () => { onVersionClick={versionHandler} /> - - - + + data={{ ...containerData, children: containerChildrenData }} + permissions={containerPermissions} + type={EntityType.CONTAINER} + onUpdate={handleContainerUpdate}> + + + + <> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index 8f6d5401b4fc..e9cad03a7340 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -283,7 +283,8 @@ const DataModelsPage = () => { }; const handleUpdateDataModel = async ( - updatedDataModel: DashboardDataModel + updatedDataModel: DashboardDataModel, + key?: keyof DashboardDataModel ) => { try { const response = await handleUpdateDataModelData(updatedDataModel); @@ -291,6 +292,7 @@ const DataModelsPage = () => { setDataModelData((prev) => ({ ...prev, ...response, + ...(key && { [key]: response[key] }), })); } catch (error) { showErrorToast(error as AxiosError); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index a27838bdc0e7..7e5dcdd4de5d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -392,7 +392,6 @@ const TableDetailsPageV1: React.FC = () => { const updatedObj = { ...previous, ...res, - version: res.version, ...(key && { [key]: res[key] }), }; From ea6df662b87ccaa90120a52aadf0e583b8d855fd Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:30:04 +0530 Subject: [PATCH 20/63] support customization for domain page --- .../DomainDetailsPage.component.tsx | 184 ++++++--------- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 2 +- .../SynonymsWidget/GenericWidget.tsx | 10 + .../ui/src/enums/CustomizeDetailPage.enum.ts | 4 + .../resources/ui/src/enums/entity.enum.ts | 3 + .../CustomizableDomainPage.tsx | 116 ++++++++++ .../CustomizablePage/CustomizablePage.tsx | 9 + .../utils/CustomizePage/CustomizePageUtils.ts | 22 +- .../ui/src/utils/Domain/DomainClassBase.ts | 209 ++++++++++++++++++ .../resources/ui/src/utils/DomainUtils.tsx | 150 ++++++++++++- 10 files changed, 582 insertions(+), 127 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx index c15e6a3ba265..8b13e5806c33 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx @@ -46,10 +46,7 @@ import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg'; import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg'; import { ManageButtonItemLabel } from '../../../components/common/ManageButtonContentItem/ManageButtonContentItem.component'; import { EntityHeader } from '../../../components/Entity/EntityHeader/EntityHeader.component'; -import EntitySummaryPanel from '../../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; -import AssetsTabs, { - AssetsTabRef, -} from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; +import { AssetsTabRef } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; import { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface'; import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; @@ -65,21 +62,24 @@ import { OperationPermission, ResourceEntity, } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { EntityType } from '../../../enums/entity.enum'; +import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { SearchIndex } from '../../../enums/search.enum'; import { CreateDataProduct } from '../../../generated/api/domains/createDataProduct'; import { CreateDomain } from '../../../generated/api/domains/createDomain'; -import { DataProduct } from '../../../generated/entity/domains/dataProduct'; import { Domain } from '../../../generated/entity/domains/domain'; import { ChangeDescription } from '../../../generated/entity/type'; +import { Page, PageType } from '../../../generated/system/ui/page'; import { Style } from '../../../generated/type/tagLabel'; +import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { addDataProducts } from '../../../rest/dataProductAPI'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { addDomains } from '../../../rest/domainAPI'; import { searchData } from '../../../rest/miscAPI'; import { searchQuery } from '../../../rest/searchAPI'; import { formatDomainsResponse } from '../../../utils/APIUtils'; import { getIsErrorMatch } from '../../../utils/CommonUtils'; +import domainClassBase from '../../../utils/Domain/DomainClassBase'; import { getQueryFilterForDomain, getQueryFilterToExcludeDomainTerms, @@ -87,6 +87,10 @@ import { import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import Fqn from '../../../utils/Fqn'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getDomainDetailsPath, @@ -99,8 +103,6 @@ import { } from '../../../utils/StringsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { AssetSelectionModal } from '../../DataAssets/AssetsSelectionModal/AssetSelectionModal'; import { EntityDetailsObjectInterface } from '../../Explore/ExplorePage.interface'; import StyleModal from '../../Modals/StyleModal/StyleModal.component'; @@ -108,10 +110,7 @@ import AddDomainForm from '../AddDomainForm/AddDomainForm.component'; import AddSubDomainModal from '../AddSubDomainModal/AddSubDomainModal.component'; import '../domain.less'; import { DomainFormType, DomainTabs } from '../DomainPage.interface'; -import DataProductsTab from '../DomainTabs/DataProductsTab/DataProductsTab.component'; import { DataProductsTabRef } from '../DomainTabs/DataProductsTab/DataProductsTab.interface'; -import DocumentationTab from '../DomainTabs/DocumentationTab/DocumentationTab.component'; -import SubDomainsTable from '../SubDomainsTable/SubDomainsTable.component'; import { DomainDetailsPageProps } from './DomainDetailsPage.interface'; const DomainDetailsPage = ({ @@ -149,6 +148,8 @@ const DomainDetailsPage = ({ const encodedFqn = getEncodedFqn( escapeESReservedCharacters(domain.fullyQualifiedName) ); + const { selectedPersona } = useApplicationStore(); + const [customizedPage, setCustomizedPage] = useState(null); const isSubDomain = useMemo(() => !isEmpty(domain.parent), [domain]); @@ -510,117 +511,38 @@ const DomainDetailsPage = ({ }, [domainFqn]); const tabs = useMemo(() => { - return [ - { - label: ( - - ), - key: DomainTabs.DOCUMENTATION, - children: ( - onUpdate(data as Domain)} - /> - ), - }, - ...(!isVersionsView - ? [ - { - label: ( - - ), - key: DomainTabs.SUBDOMAINS, - children: ( - setShowAddSubDomainModal(true)} - /> - ), - }, - { - label: ( - - ), - key: DomainTabs.DATA_PRODUCTS, - children: ( - - ), - }, - { - label: ( - - ), - key: DomainTabs.ASSETS, - children: ( - - setAssetModalVisible(true)} - onAssetClick={handleAssetClick} - onRemoveAsset={handleAssetSave} - /> - - ), - minWidth: 800, - flex: 0.87, - }} - hideSecondPanel={!previewAsset} - pageTitle={t('label.domain')} - secondPanel={{ - children: previewAsset && ( - setPreviewAsset(undefined)} - /> - ), - minWidth: 400, - flex: 0.13, - className: - 'entity-summary-resizable-right-panel-container domain-resizable-panel-container', - }} - /> - ), - }, - ] - : []), - ]; + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = domainClassBase.getDomainDetailPageTabs({ + domain, + isVersionsView, + domainPermission, + subDomains, + dataProductsCount, + assetCount, + activeTab: activeTab as EntityTabs, + onUpdate, + onAddDataProduct, + isSubDomainsLoading, + queryFilter, + assetTabRef, + dataProductsTabRef, + previewAsset, + setPreviewAsset, + setAssetModalVisible, + handleAssetClick, + handleAssetSave, + setShowAddSubDomainModal, + onAddSubDomain: addSubDomain, + showAddSubDomainModal, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.DOCUMENTATION + ); }, [ domain, domainPermission, @@ -641,6 +563,24 @@ const DomainDetailsPage = ({ fetchDataProducts(); }, [domain.fullyQualifiedName]); + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Domain) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + useEffect(() => { fetchSubDomains(); }, [domainFqn]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx index 0e2c51a3f458..850f26a5074a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -105,7 +105,7 @@ export const CustomizeTabWidget = () => { }; const add = () => { - const newActiveKey = uniqueId(`newTab`); + const newActiveKey = uniqueId(`custom`); updateCurrentPage({ ...currentPage, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index f565b69b7827..9fead26b338c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -30,6 +30,7 @@ import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/Fre import TableConstraints from '../../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import containerDetailsClassBase from '../../../../utils/ContainerDetailsClassBase'; import dashboardDataModelClassBase from '../../../../utils/DashboardDataModelBase'; +import domainClassBase from '../../../../utils/Domain/DomainClassBase'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; import tableClassBase from '../../../../utils/TableClassBase'; @@ -268,6 +269,8 @@ export const GenericWidget = (props: WidgetCommonProps) => { label="synonym" /> ); + } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.DOMAIN_TYPE)) { + return domainClassBase.getDummyData().domainType; } else if ( props.widgetKey.startsWith(DetailPageWidgetKeys.DOMAIN) || props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.DOMAIN) @@ -614,6 +617,13 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { return ; + } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.EXPERTS)) { + return ( + + ); } return widgetName; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 9226682c18ac..3f11a6ebc2a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -39,6 +39,10 @@ export enum DetailPageWidgetKeys { CONTAINER_CHILDREN = 'KnowledgePanel.ContainerChildren', PIPELINE_TASKS = 'KnowledgePanel.PipelineTasks', SEARCH_INDEX_FIELDS = 'KnowledgePanel.SearchIndexFields', + DOCUMENTATION = 'KnowledgePanel.Documentation', + OWNERS = 'KnowledgePanel.Owners', + EXPERTS = 'KnowledgePanel.Experts', + DOMAIN_TYPE = 'KnowledgePanel.DomainType', } export enum GlossaryTermDetailPageWidgetKeys { 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 872f28874a9c..465ed5deb043 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 @@ -211,6 +211,9 @@ export enum EntityTabs { ASSETS = 'assets', EXPRESSION = 'expression', DASHBOARD = 'dashboard', + DOCUMENTATION = 'documentation', + DATA_PRODUCTS = 'data_products', + SUBDOMAINS = 'subdomains', } export enum EntityAction { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx new file mode 100644 index 000000000000..b0a5e1f7c30b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2023 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 React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import gridBgImg from '../../assets/img/grid-bg-img.png'; +import { ReactComponent as DomainIcon } from '../../assets/svg/ic-domain.svg'; +import { EntityHeader } from '../../components/Entity/EntityHeader/EntityHeader.component'; +import { CustomizeTabWidget } from '../../components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; +import { CustomizablePageHeader } from '../../components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader'; +import { CustomizeMyDataProps } from '../../components/MyData/CustomizableComponents/CustomizeMyData/CustomizeMyData.interface'; +import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; +import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; +import { DE_ACTIVE_COLOR } from '../../constants/constants'; +import { EntityType } from '../../enums/entity.enum'; +import { Domain } from '../../generated/entity/domains/domain'; +import { Page } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/uiCustomization'; +import { getDummyDataByPage } from '../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../utils/EntityUtils'; +import Fqn from '../../utils/Fqn'; +import { getDomainPath } from '../../utils/RouterUtils'; +import { useCustomizeStore } from '../CustomizablePage/CustomizeStore'; + +const CustomizableDomainPage = ({ + personaDetails, + onSaveLayout, +}: CustomizeMyDataProps) => { + const { t } = useTranslation(); + const { currentPage, currentPageType } = useCustomizeStore(); + + const handleReset = useCallback(async () => { + await onSaveLayout(); + }, [onSaveLayout]); + + const handleSave = async () => { + await onSaveLayout(currentPage ?? ({ pageType: currentPageType } as Page)); + }; + + const entityDummyData = getDummyDataByPage( + currentPageType as PageType + ) as Domain; + + const breadcrumbs = useMemo(() => { + if (!entityDummyData.fullyQualifiedName) { + return []; + } + + const arr = Fqn.split(entityDummyData.fullyQualifiedName); + const dataFQN: Array = []; + + return [ + { + name: 'Domains', + url: getDomainPath(arr[0]), + activeTitle: false, + }, + ...arr.slice(0, -1).map((d) => { + dataFQN.push(d); + + return { + name: d, + url: getDomainPath(dataFQN.join(FQN_SEPARATOR_CHAR)), + activeTitle: false, + }; + }), + ]; + }, [entityDummyData.fullyQualifiedName]); + + return ( + + +
+ + } + serviceName="" + /> +
+ +
+ ); +}; + +export default CustomizableDomainPage; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx index 670f75e0c292..c5407cd49aa8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx @@ -43,6 +43,7 @@ import { getPersonaByName } from '../../rest/PersonaAPI'; import { Transi18next } from '../../utils/CommonUtils'; import { getSettingPath } from '../../utils/RouterUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; +import CustomizableDomainPage from '../CustomizableDomainPage/CustomizableDomainPage'; import { CustomizeTableDetailPage } from '../CustomizeTableDetailPage/CustomizeTableDetailPage'; import { SettingsNavigationPage } from '../SettingsNavigationPage/SettingsNavigationPage'; import { useCustomizeStore } from './CustomizeStore'; @@ -254,6 +255,14 @@ export const CustomizablePage = () => { onSaveLayout={handlePageCustomizeSave} /> ); + case PageType.Domain: + return ( + + ); case PageType.Glossary: case PageType.GlossaryTerm: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index bf5c96d67c19..e09f23ed2825 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -21,6 +21,7 @@ import dashboardDataModelClassBase from '../DashboardDataModelBase'; import dashboardDetailsClassBase from '../DashboardDetailsClassBase'; import databaseClassBase from '../Database/DatabaseClassBase'; import databaseSchemaClassBase from '../DatabaseSchemaClassBase'; +import domainClassBase from '../Domain/DomainClassBase'; import i18n from '../i18next/LocalUtil'; import pipelineClassBase from '../PipelineClassBase'; import searchIndexClassBase from '../SearchIndexDetailsClassBase'; @@ -108,7 +109,7 @@ export const getTabLabelFromId = (tab: EntityTabs) => { case EntityTabs.GLOSSARY_TERMS: return i18n.t('label.glossary-terms'); case EntityTabs.ASSETS: - return i18n.t('label.assets'); + return i18n.t('label.asset-plural'); case EntityTabs.ACTIVITY_FEED: return i18n.t('label.activity-feed-and-task-plural'); case EntityTabs.CUSTOM_PROPERTIES: @@ -135,6 +136,13 @@ export const getTabLabelFromId = (tab: EntityTabs) => { return i18n.t('label.children'); case EntityTabs.DETAILS: return i18n.t('label.detail-plural'); + case EntityTabs.SUBDOMAINS: + return i18n.t('label.sub-domain-plural'); + case EntityTabs.DATA_PRODUCTS: + return i18n.t('label.data-product-plural'); + case EntityTabs.DOCUMENTATION: + return i18n.t('label.documentation'); + default: return ''; } @@ -189,6 +197,10 @@ export const getDashboardDefaultTabs = () => { return dashboardDetailsClassBase.getDashboardDetailPageTabsIds(); }; +export const getDomainDefaultTabs = () => { + return domainClassBase.getDomainDetailPageTabsIds(); +}; + export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: @@ -215,6 +227,8 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { return getPipelineDefaultTabs(); case PageType.Dashboard: return getDashboardDefaultTabs(); + case PageType.Domain: + return getDomainDefaultTabs(); default: return [ { @@ -252,6 +266,8 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return searchIndexClassBase.getDefaultLayout(tab); case PageType.Container: return containerDetailsClassBase.getDefaultLayout(tab); + case PageType.Domain: + return domainClassBase.getDefaultLayout(tab); default: return []; } @@ -305,6 +321,8 @@ export const getCustomizableWidgetByPage = ( return pipelineClassBase.getCommonWidgetList(); case PageType.SearchIndex: return searchIndexClassBase.getCommonWidgetList(); + case PageType.Domain: + return domainClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; @@ -333,6 +351,8 @@ export const getDummyDataByPage = (pageType: PageType) => { return searchIndexClassBase.getDummyData(); case PageType.Dashboard: return dashboardDetailsClassBase.getDummyData(); + case PageType.Domain: + return domainClassBase.getDummyData(); case PageType.LandingPage: default: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts new file mode 100644 index 000000000000..39f10a84614d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2024 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 { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { DataProductsTabRef } from '../../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.interface'; +import { EntityDetailsObjectInterface } from '../../components/Explore/ExplorePage.interface'; +import { AssetsTabRef } from '../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; +import { + DESCRIPTION_WIDGET, + GridSizes, +} from '../../constants/CustomizeWidgets.constants'; +import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { CreateDomain } from '../../generated/api/domains/createDomain'; +import { Domain } from '../../generated/entity/domains/domain'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import { getDomainDetailTabs } from '../DomainUtils'; +import i18n from '../i18next/LocalUtil'; + +export interface DomainDetailPageTabProps { + domain: Domain; + isVersionsView: boolean; + domainPermission: OperationPermission; + subDomains: Domain[]; + dataProductsCount: number; + assetCount: number; + activeTab: EntityTabs; + onUpdate: (domain: Domain) => Promise; + onAddDataProduct: () => void; + onAddSubDomain: (subDomain: CreateDomain) => Promise; + isSubDomainsLoading: boolean; + queryFilter?: string | Record; + assetTabRef: React.RefObject; + dataProductsTabRef: React.RefObject; + showAddSubDomainModal: boolean; + previewAsset?: EntityDetailsObjectInterface; + setPreviewAsset: (asset?: EntityDetailsObjectInterface) => void; + setAssetModalVisible: (visible: boolean) => void; + handleAssetClick: (asset?: EntityDetailsObjectInterface) => void; + handleAssetSave: () => void; + setShowAddSubDomainModal: (visible: boolean) => void; + labelMap?: Record; +} + +class DomainClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getDomainDetailPageTabs( + domainDetailsPageProps: DomainDetailPageTabProps + ): TabProps[] { + return getDomainDetailTabs(domainDetailsPageProps); + } + + public getDomainDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.DOCUMENTATION, + EntityTabs.SUBDOMAINS, + EntityTabs.DATA_PRODUCTS, + EntityTabs.ASSETS, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.DOCUMENTATION, + })); + } + + public getDefaultLayout(tab: EntityTabs) { + switch (tab) { + case EntityTabs.DOCUMENTATION: + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + + { + h: 1, + i: DetailPageWidgetKeys.OWNERS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.EXPERTS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.DOMAIN_TYPE, + w: 2, + x: 6, + y: 3, + static: false, + }, + ]; + + default: + return []; + } + } + + public getDummyData(): Domain { + return { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + domainType: 'Consumer-aligned', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + displayName: 'Engineering', + description: 'Domain related engineering development.', + style: {}, + version: 0.8, + updatedAt: 1698061758989, + updatedBy: 'rupesh', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + children: [], + owners: [ + { + id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', + type: 'team', + name: 'Data', + fullyQualifiedName: 'Data', + description: '', + displayName: 'Data', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/ebac156e-6779-499c-8bbf-ab98a6562bc5', + }, + ], + experts: [ + { + id: '34ee72dc-7dad-4710-9f1d-e934ad0554a9', + type: 'user', + name: 'brian_smith7', + fullyQualifiedName: 'brian_smith7', + displayName: 'Brian Smith', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/34ee72dc-7dad-4710-9f1d-e934ad0554a9', + }, + { + id: '9a6687fa-8bd5-446c-aa8f-81416c88fe67', + type: 'user', + name: 'brittney_thomas3', + fullyQualifiedName: 'brittney_thomas3', + displayName: 'Brittney Thomas', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/9a6687fa-8bd5-446c-aa8f-81416c88fe67', + }, + ], + } as Domain; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.OWNERS, + name: i18n.t('label.owner-plural'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.EXPERTS, + name: i18n.t('label.expert-plural'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + { + fullyQualifiedName: DetailPageWidgetKeys.DOMAIN_TYPE, + name: i18n.t('label.domain-type'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + ]; + } +} + +const domainClassBase = new DomainClassBase(); + +export default domainClassBase; +export { DomainClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index 548dd9acc237..a4cb2f11ff97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -13,7 +13,6 @@ import { Divider, Space, Typography } from 'antd'; import { ItemType } from 'antd/lib/menu/hooks/useItems'; import classNames from 'classnames'; -import { t } from 'i18next'; import { get, isEmpty, isUndefined } from 'lodash'; import React, { Fragment, ReactNode } from 'react'; import { Link } from 'react-router-dom'; @@ -21,20 +20,31 @@ import { ReactComponent as DomainIcon } from '../assets/svg/ic-domain.svg'; import { ReactComponent as SubDomainIcon } from '../assets/svg/ic-subdomain.svg'; import { TreeListItem } from '../components/common/DomainSelectableTree/DomainSelectableTree.interface'; import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; +import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import DataProductsTab from '../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.component'; +import DocumentationTab from '../components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component'; +import SubDomainsTable from '../components/Domain/SubDomainsTable/SubDomainsTable.component'; +import EntitySummaryPanel from '../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; +import AssetsTabs from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; +import { AssetsOfEntity } from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface'; import { DEFAULT_DOMAIN_VALUE, DE_ACTIVE_COLOR, NO_DATA_PLACEHOLDER, } from '../constants/constants'; import { DOMAIN_TYPE_DATA } from '../constants/Domain.constants'; -import { EntityType } from '../enums/entity.enum'; +import { EntityTabs, EntityType } from '../enums/entity.enum'; +import { DataProduct } from '../generated/entity/domains/dataProduct'; import { Domain } from '../generated/entity/domains/domain'; import { EntityReference } from '../generated/entity/type'; import { QueryFieldInterface, QueryFilterInterface, } from '../pages/ExplorePage/ExplorePage.interface'; +import { DomainDetailPageTabProps } from './Domain/DomainClassBase'; import { getEntityName, getEntityReferenceFromEntity } from './EntityUtils'; +import i18n from './i18next/LocalUtil'; import { getDomainPath } from './RouterUtils'; export const getOwner = ( @@ -162,7 +172,7 @@ export const domainTypeTooltipDataRender = () => ( export const getDomainOptions = (domains: Domain[] | EntityReference[]) => { const domainOptions: ItemType[] = [ { - label: t('label.all-domain-plural'), + label: i18n.t('label.all-domain-plural'), key: DEFAULT_DOMAIN_VALUE, }, ]; @@ -307,3 +317,137 @@ export const isDomainExist = ( return false; }; + +export const getDomainDetailTabs = ({ + domain, + isVersionsView, + domainPermission, + subDomains, + dataProductsCount, + assetCount, + activeTab, + onUpdate, + onAddDataProduct, + isSubDomainsLoading, + queryFilter, + assetTabRef, + dataProductsTabRef, + previewAsset, + setPreviewAsset, + setAssetModalVisible, + handleAssetClick, + handleAssetSave, + setShowAddSubDomainModal, +}: DomainDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.DOCUMENTATION, + children: ( + onUpdate(data as Domain)} + /> + ), + }, + ...(!isVersionsView + ? [ + { + label: ( + + ), + key: EntityTabs.SUBDOMAINS, + children: ( + setShowAddSubDomainModal(true)} + /> + ), + }, + { + label: ( + + ), + key: EntityTabs.DATA_PRODUCTS, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.ASSETS, + children: ( + + setAssetModalVisible(true)} + onAssetClick={handleAssetClick} + onRemoveAsset={handleAssetSave} + /> + + ), + minWidth: 800, + flex: 0.87, + }} + hideSecondPanel={!previewAsset} + pageTitle={i18n.t('label.domain')} + secondPanel={{ + children: previewAsset && ( + setPreviewAsset(undefined)} + /> + ), + minWidth: 400, + flex: 0.13, + className: + 'entity-summary-resizable-right-panel-container domain-resizable-panel-container', + }} + /> + ), + }, + ] + : []), + ]; +}; From dbc4866966812c3f4c3bac6e24e32545f2b22c22 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:58:27 +0530 Subject: [PATCH 21/63] update --- .../schema/entity/data/databaseSchema.json | 2 +- .../APIEndpointDetails.interface.ts | 4 +- .../APIEndpointDetails/APIEndpointDetails.tsx | 61 ++-- .../APIEndpointSchema/APIEndpointSchema.tsx | 6 - .../APIEndpointVersion/APIEndpointVersion.tsx | 2 - .../ContainerDataModel.interface.ts | 2 - .../ContainerDataModel/ContainerDataModel.tsx | 5 - .../DashboardChartTable.tsx | 25 +- .../DashboardDetails.component.tsx | 34 --- .../DashboardDetails.interface.ts | 2 - .../DashboardDetails.test.tsx | 252 ---------------- .../DataModels/DataModelDetails.component.tsx | 34 +-- .../DataModels/DataModelDetails.interface.tsx | 2 - .../ModelTab/ModelTab.component.tsx | 13 +- .../ModelTab/ModelTab.interface.tsx | 18 -- .../CommonWidgets/CommonWidgets.tsx | 5 +- .../SchemaTable/SchemaTable.component.tsx | 3 - .../TableDescription.component.tsx | 3 +- .../TableDescription.interface.ts | 2 - .../TableTags/TableTags.component.tsx | 4 +- .../Database/TableTags/TableTags.interface.ts | 2 - .../EntityRightPanel.test.tsx | 9 - .../EntityRightPanel/EntityRightPanel.tsx | 5 - .../SynonymsWidget/GenericWidget.tsx | 4 +- .../GlossaryDetails.component.tsx | 5 +- .../GlossaryDetails.interface.ts | 2 - .../GlossaryTermsV1.component.tsx | 27 +- .../GlossaryTermsV1.interface.ts | 1 - .../tabs/GlossaryOverviewTab.component.tsx | 19 +- .../tabs/GlossaryOverviewTab.test.tsx | 63 ---- .../Glossary/GlossaryV1.component.tsx | 51 ---- .../MetricDetails/MetricDetails.interface.ts | 4 +- .../Metric/MetricDetails/MetricDetails.tsx | 59 +--- .../MlModelDetail/MlModel.interface.ts | 2 - .../MlModelDetail/MlModelDetail.component.tsx | 63 +--- .../MlModelDetail/MlModelDetail.interface.ts | 2 - .../MlModelDetail/MlModelFeaturesList.tsx | 4 - .../PipelineDetails.component.tsx | 80 +---- .../TagsContainerV2.interface.ts | 2 - .../Tag/TagsContainerV2/TagsContainerV2.tsx | 5 +- .../TopicDetails/TopicDetails.component.tsx | 32 -- .../TopicDetails/TopicDetails.interface.ts | 2 - .../Topic/TopicDetails/TopicDetails.test.tsx | 225 -------------- .../Topic/TopicSchema/TopicSchema.tsx | 6 +- .../Description.interface.ts | 1 - .../EntityDescription/DescriptionV1.tsx | 3 +- .../generated/entity/data/databaseSchema.ts | 2 +- .../APICollectionPage/APICollectionPage.tsx | 71 +---- .../APICollectionPage/APIEndpointsTab.tsx | 4 - .../pages/APIEndpointPage/APIEndpointPage.tsx | 31 +- .../src/pages/ContainerPage/ContainerPage.tsx | 66 +--- .../DashboardDetailsPage.component.tsx | 16 - .../DataModelPage/DataModelPage.component.tsx | 21 +- .../DatabaseDetailsPage.tsx | 89 ++---- .../DatabaseSchemaPage.component.tsx | 106 ++----- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 3 - .../MetricDetailsPage/MetricDetailsPage.tsx | 32 +- .../MetricListPage/MetricListPage.tsx | 2 - .../MlModelPage/MlModelPage.component.tsx | 21 +- .../PipelineDetailsPage.component.tsx | 14 +- .../SearchIndexDetailsPage.tsx | 99 ++---- .../SearchIndexFieldsTab.interface.ts | 2 - .../SearchIndexFieldsTab.test.tsx | 2 - .../SearchIndexFieldsTab.tsx | 2 - .../SearchIndexFieldsTable.interface.ts | 2 - .../SearchIndexFieldsTable.tsx | 14 +- .../ServiceDetailsPage/ServiceDetailsPage.tsx | 10 +- .../StoredProcedure/StoredProcedurePage.tsx | 70 +---- .../FrequentlyJoinedTables.test.tsx | 10 + .../TableDetailsPageV1.test.tsx | 10 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 10 +- .../TopicDetailsPage.component.tsx | 27 +- .../ui/src/utils/CommonUtils.test.ts | 284 +----------------- .../resources/ui/src/utils/CommonUtils.tsx | 4 - .../ui/src/utils/ContainerDetailUtils.tsx | 4 - .../ui/src/utils/ContainerDetailsClassBase.ts | 2 - .../ui/src/utils/DashboardDetailsClassBase.ts | 1 - .../src/utils/DashboardDetailsUtils.test.ts | 24 -- .../ui/src/utils/DashboardDetailsUtils.tsx | 15 +- .../resources/ui/src/utils/DataModelsUtils.ts | 7 - .../ui/src/utils/Database/Database.util.tsx | 3 - .../src/utils/Database/DatabaseClassBase.ts | 1 - .../ui/src/utils/DatabaseSchemaClassBase.ts | 2 - .../src/utils/DatabaseSchemaDetailsUtils.tsx | 3 - .../ui/src/utils/PipelineClassBase.ts | 2 - .../ui/src/utils/PipelineDetailsUtils.test.ts | 57 ---- .../ui/src/utils/PipelineDetailsUtils.tsx | 32 +- .../ui/src/utils/SearchIndexUtils.tsx | 15 +- .../ui/src/utils/StoredProcedureBase.ts | 2 - .../ui/src/utils/StoredProceduresUtils.tsx | 3 - .../resources/ui/src/utils/TableUtils.tsx | 15 +- 91 files changed, 240 insertions(+), 2129 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.test.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.test.ts diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/databaseSchema.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/databaseSchema.json index 089957f161bc..1437e74d12d3 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/data/databaseSchema.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/databaseSchema.json @@ -155,6 +155,6 @@ } } }, - "required": ["name", "database", "service"], + "required": ["id", "name", "database", "service"], "additionalProperties": false } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.interface.ts index 3d343e4ad34d..48b8993fa4aa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.interface.ts @@ -11,7 +11,6 @@ * limitations under the License. */ import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { APIEndpoint } from '../../../generated/entity/data/apiEndpoint'; import { DataAssetWithDomains } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; @@ -19,12 +18,11 @@ import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; export interface APIEndpointDetailsProps { apiEndpointDetails: APIEndpoint; apiEndpointPermissions: OperationPermission; - onCreateThread: (data: CreateThread) => Promise; fetchAPIEndpointDetails: () => void; onFollowApiEndPoint: () => Promise; onApiEndpointUpdate: ( updatedData: APIEndpoint, - key: keyof APIEndpoint + key?: keyof APIEndpoint ) => Promise; onToggleDelete: (version?: number) => void; onUnFollowApiEndPoint: () => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index adc53cc5bc3b..feb2ff13eb3d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -25,7 +25,6 @@ import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { APIEndpoint } from '../../../generated/entity/data/apiEndpoint'; import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { TagLabel } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -40,9 +39,7 @@ import { import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; @@ -50,6 +47,7 @@ import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -60,7 +58,7 @@ import { APIEndpointDetailsProps } from './APIEndpointDetails.interface'; const APIEndpointDetails: React.FC = ({ apiEndpointDetails, apiEndpointPermissions, - onCreateThread, + fetchAPIEndpointDetails, onFollowApiEndPoint, onApiEndpointUpdate, @@ -72,20 +70,14 @@ const APIEndpointDetails: React.FC = ({ }: APIEndpointDetailsProps) => { const { t } = useTranslation(); const { currentUser } = useApplicationStore(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedApiEndpointFqn } = useFqn(); const history = useHistory(); - const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const { owners, deleted, @@ -129,14 +121,6 @@ const APIEndpointDetails: React.FC = ({ ); }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const onThreadPanelClose = () => setThreadLink(''); - const handleRestoreApiEndpoint = async () => { try { const { version: newVersion } = await restoreApiEndPoint( @@ -308,13 +292,11 @@ const APIEndpointDetails: React.FC = ({ owner={apiEndpointDetails.owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -340,7 +322,6 @@ const APIEndpointDetails: React.FC = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -422,7 +403,6 @@ const APIEndpointDetails: React.FC = ({ deleted, handleFeedCount, onExtensionUpdate, - onThreadLinkSelect, handleTagSelection, onDescriptionUpdate, onDataProductsUpdate, @@ -463,32 +443,25 @@ const APIEndpointDetails: React.FC = ({ onVersionClick={onVersionChange} /> -
- - + + data={apiEndpointDetails} + permissions={apiEndpointPermissions} + type={EntityType.API_ENDPOINT} + onUpdate={onApiEndpointUpdate}> + + + + <> - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx index 4b03aa68f086..482006f78643 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointSchema/APIEndpointSchema.tsx @@ -34,7 +34,6 @@ import { Field, TagSource, } from '../../../generated/entity/data/apiEndpoint'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { APISchema } from '../../../generated/type/apiSchema'; import { TagLabel } from '../../../generated/type/tagLabel'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -62,7 +61,6 @@ import { APIEndpointDetailsProps } from '../APIEndpointDetails/APIEndpointDetail interface APIEndpointSchemaProps { apiEndpointDetails: APIEndpoint; permissions: OperationPermission; - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; onApiEndpointUpdate?: APIEndpointDetailsProps['onApiEndpointUpdate']; isVersionView?: boolean; } @@ -75,7 +73,6 @@ export enum SchemaViewType { const APIEndpointSchema: FC = ({ apiEndpointDetails, permissions, - onThreadLinkSelect, onApiEndpointUpdate, isVersionView = false, }) => { @@ -313,7 +310,6 @@ const APIEndpointSchema: FC = ({ index={index} isReadOnly={Boolean(apiEndpointDetails.deleted) || isVersionView} onClick={() => setEditFieldDescription(record)} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -335,7 +331,6 @@ const APIEndpointSchema: FC = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Classification, @@ -362,7 +357,6 @@ const APIEndpointSchema: FC = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Glossary, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx index c3ebb60b9e54..017e5c1d451d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx @@ -13,7 +13,6 @@ import { Col, Row, Space, Tabs, TabsProps } from 'antd'; import classNames from 'classnames'; -import { noop } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; @@ -128,7 +127,6 @@ const APIEndpointVersion: FC = ({ isVersionView apiEndpointDetails={currentVersionData} permissions={entityPermissions} - onThreadLinkSelect={noop} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.interface.ts index 3f713c25c555..4dec26ceb839 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.interface.ts @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ThreadType } from '../../../generated/api/feed/createThread'; import { Container } from '../../../generated/entity/data/container'; export interface ContainerDataModelProps { @@ -20,6 +19,5 @@ export interface ContainerDataModelProps { hasGlossaryTermEditAccess: boolean; isReadOnly: boolean; entityFqn: string; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; onUpdate: (updatedDataModel: Container['dataModel']) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx index 17736eac3d90..58db7cb3f6a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerDataModel/ContainerDataModel.tsx @@ -53,7 +53,6 @@ const ContainerDataModel: FC = ({ isReadOnly, onUpdate, entityFqn, - onThreadLinkSelect, }) => { const { t } = useTranslation(); @@ -161,7 +160,6 @@ const ContainerDataModel: FC = ({ index={index} isReadOnly={isReadOnly} onClick={() => setEditContainerColumnDescription(record)} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -186,7 +184,6 @@ const ContainerDataModel: FC = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -211,7 +208,6 @@ const ContainerDataModel: FC = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -224,7 +220,6 @@ const ContainerDataModel: FC = ({ hasDescriptionEditAccess, editContainerColumnDescription, getEntityName, - onThreadLinkSelect, handleFieldTagsChange, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx index 94c60b03e5cf..073e7be4d163 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx @@ -26,13 +26,9 @@ import { ResourceEntity } from '../../../context/PermissionProvider/PermissionPr import { EntityType } from '../../../enums/entity.enum'; import { TagLabel, TagSource } from '../../../generated/entity/data/chart'; import { Dashboard } from '../../../generated/entity/data/dashboard'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { updateChart } from '../../../rest/chartAPI'; -import { - fetchCharts, - sortTagsForCharts, -} from '../../../utils/DashboardDetailsUtils'; +import { fetchCharts } from '../../../utils/DashboardDetailsUtils'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { columnFilterIcon } from '../../../utils/TableColumn.util'; @@ -51,15 +47,10 @@ import { useGenericContext } from '../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import { ChartsPermissions } from '../DashboardDetails/DashboardDetails.interface'; -interface DashboardChartTableProps { - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; -} - -export const DashboardChartTable = ({ - onThreadLinkSelect, -}: DashboardChartTableProps) => { +export const DashboardChartTable = () => { const { t } = useTranslation(); const { getEntityPermission } = usePermissionProvider(); + const { onThreadLinkSelect } = useGenericContext(); const { data: dashboardDetails } = useGenericContext(); const { charts: listChartIds } = dashboardDetails ?? {}; @@ -222,14 +213,9 @@ export const DashboardChartTable = ({ const res = await updateChart(chartId, patch); setCharts((prevCharts) => { - const charts = [...prevCharts].map((chart) => + return [...prevCharts].map((chart) => chart.id === chartId ? res : chart ); - - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - return sortTagsForCharts(charts); }); } catch (error) { showErrorToast( @@ -332,7 +318,6 @@ export const DashboardChartTable = ({ index={index} isReadOnly={dashboardDetails?.deleted} onClick={() => handleUpdateChart(record, index)} - onThreadLinkSelect={onThreadLinkSelect} /> ); }, @@ -356,7 +341,6 @@ export const DashboardChartTable = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ); }, @@ -382,7 +366,6 @@ export const DashboardChartTable = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Glossary, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index 34aeb48666e1..7060e45b9fa3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -25,7 +25,6 @@ import { ResourceEntity } from '../../../context/PermissionProvider/PermissionPr import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Dashboard } from '../../../generated/entity/data/dashboard'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { Page } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/uiCustomization'; import { TagLabel } from '../../../generated/type/tagLabel'; @@ -46,8 +45,6 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { GenericProvider } from '../../GenericProvider/GenericProvider'; @@ -63,7 +60,6 @@ const DashboardDetails = ({ followDashboardHandler, unFollowDashboardHandler, versionHandler, - createThread, onUpdateVote, onDashboardUpdate, handleToggleDelete, @@ -76,17 +72,12 @@ const DashboardDetails = ({ const [customizedPage, setCustomizedPage] = useState(null); const { fqn: decodedDashboardFQN } = useFqn(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const [isEdit, setIsEdit] = useState(false); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); const [dashboardPermissions, setDashboardPermissions] = useState( DEFAULT_ENTITY_PERMISSION ); @@ -250,17 +241,6 @@ const DashboardDetails = ({ : await followDashboardHandler(); }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); @@ -327,7 +307,6 @@ const DashboardDetails = ({ deleted: deleted ?? false, dashboardTags, handleFeedCount, - onThreadLinkSelect, handleTagSelection, onDescriptionUpdate, onExtensionUpdate, @@ -356,7 +335,6 @@ const DashboardDetails = ({ handleFeedCount, onDescriptionEdit, onDescriptionUpdate, - onThreadLinkSelect, handleTagSelection, editTagsPermission, editGlossaryTermsPermission, @@ -433,18 +411,6 @@ const DashboardDetails = ({ <> - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts index 647286bce7a7..baa6b799fa53 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.interface.ts @@ -12,7 +12,6 @@ */ import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Chart } from '../../../generated/entity/data/chart'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { EntityReference } from '../../../generated/type/entityReference'; @@ -32,7 +31,6 @@ export interface DashboardDetailsProps { charts: Array; dashboardDetails: Dashboard; fetchDashboard: () => void; - createThread: (data: CreateThread) => Promise; followDashboardHandler: () => Promise; unFollowDashboardHandler: () => Promise; versionHandler: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx deleted file mode 100644 index e0c2f542021f..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.test.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2022 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 { - findAllByTestId, - findByTestId, - findByText, - render, -} from '@testing-library/react'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { EntityTabs, EntityType } from '../../../enums/entity.enum'; -import { Dashboard } from '../../../generated/entity/data/dashboard'; -import { mockGlossaryList } from '../../../mocks/Glossary.mock'; -import { mockTagList } from '../../../mocks/Tags.mock'; -import DashboardDetails from './DashboardDetails.component'; -import { DashboardDetailsProps } from './DashboardDetails.interface'; - -const mockUserTeam = [ - { - description: 'description', - displayName: 'displayName', - href: 'href', - id: 'id', - name: 'name', - type: 'type', - }, - { - description: 'description', - displayName: 'displayName', - href: 'href', - id: 'id', - name: 'name', - type: 'type', - }, -]; - -const dashboardDetailsProps: DashboardDetailsProps = { - charts: [ - { - displayName: 'Test chart', - id: '1', - deleted: false, - name: '', - type: EntityType.CHART, - }, - ], - dashboardDetails: {} as Dashboard, - followDashboardHandler: jest.fn(), - unFollowDashboardHandler: jest.fn(), - onDashboardUpdate: jest.fn(), - versionHandler: jest.fn(), - createThread: jest.fn(), - fetchDashboard: jest.fn(), - handleToggleDelete: jest.fn(), -}; - -const mockEntityPermissions = { - Create: true, - Delete: true, - ViewAll: true, - ViewBasic: true, - EditAll: true, - EditTags: true, - EditDescription: true, - EditDisplayName: true, - EditCustomFields: true, -}; - -const mockParams = { - dashboardFQN: 'test', - tab: EntityTabs.DETAILS, -}; - -jest.mock('react-router-dom', () => ({ - useHistory: jest.fn(), - useParams: jest.fn().mockImplementation(() => mockParams), -})); - -jest.mock('../../common/TabsLabel/TabsLabel.component', () => { - return jest.fn().mockImplementation(({ name }) =>

{name}

); -}); - -jest.mock('../../common/EntityDescription/DescriptionV1', () => { - return jest.fn().mockReturnValue(

Description Component

); -}); -jest.mock('../../common/RichTextEditor/RichTextEditorPreviewerV1', () => { - return jest.fn().mockReturnValue(

RichTextEditorPreviwer

); -}); - -jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({ - usePermissionProvider: jest.fn().mockImplementation(() => ({ - getEntityPermission: jest - .fn() - .mockImplementation(() => mockEntityPermissions), - })), -})); - -jest.mock('../../Database/TableTags/TableTags.component', () => - jest - .fn() - .mockImplementation(() => ( -
Table Tag Container
- )) -); - -jest.mock('../../Lineage/Lineage.component', () => { - return jest.fn().mockReturnValue(

Lineage

); -}); -jest.mock('../../common/CustomPropertyTable/CustomPropertyTable', () => ({ - CustomPropertyTable: jest - .fn() - .mockReturnValue(

CustomPropertyTable.component

), -})); - -jest.mock('../../PageLayoutV1/PageLayoutV1', () => { - return jest.fn().mockImplementation(({ children }) =>
{children}
); -}); - -jest.mock('../../ActivityFeed/FeedEditor/FeedEditor', () => { - return jest.fn().mockReturnValue(

FeedEditor.component

); -}); - -jest.mock('../../../utils/CommonUtils', () => ({ - addToRecentViewed: jest.fn(), - getCountBadge: jest.fn(), - getPartialNameFromFQN: jest.fn().mockReturnValue('PartialNameFromFQN'), - getUserTeams: () => mockUserTeam, - getHtmlForNonAdminAction: jest.fn(), - getEntityPlaceHolder: jest.fn().mockReturnValue('value'), - getEntityName: jest.fn().mockReturnValue('entityName'), - pluralize: jest.fn().mockReturnValue('2 charts'), - getEntityDeleteMessage: jest.fn(), - getOwnerValue: jest.fn().mockReturnValue('Owner'), -})); - -jest.mock('../../../utils/TagsUtils', () => ({ - getAllTagsList: jest.fn(() => Promise.resolve(mockTagList)), - getTagsHierarchy: jest.fn().mockReturnValue([]), -})); - -jest.mock('../../../utils/GlossaryUtils', () => ({ - getGlossaryTermsList: jest.fn(() => Promise.resolve(mockGlossaryList)), - getGlossaryTermHierarchy: jest.fn().mockReturnValue([]), -})); - -describe.skip('Test DashboardDetails component', () => { - it('Checks if the DashboardDetails component has all the proper components rendered', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const tabs = await findByTestId(container, 'tabs'); - const detailsTab = await findByText(tabs, 'label.detail-plural'); - const activityFeedTab = await findByText( - tabs, - 'label.activity-feed-and-task-plural' - ); - const lineageTab = await findByText(tabs, 'label.lineage'); - const tagsContainer = await findAllByTestId( - container, - 'table-tag-container' - ); - - expect(tabs).toBeInTheDocument(); - expect(detailsTab).toBeInTheDocument(); - expect(activityFeedTab).toBeInTheDocument(); - expect(lineageTab).toBeInTheDocument(); - expect(tagsContainer).toHaveLength(2); - }); - - it('Check if active tab is details', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const activityFeedList = await findByTestId(container, 'charts-table'); - - expect(activityFeedList).toBeInTheDocument(); - }); - - it('Check if active tab is activity feed', async () => { - mockParams.tab = EntityTabs.ACTIVITY_FEED; - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const activityFeedList = await findByText(container, /ActivityFeedList/i); - - expect(activityFeedList).toBeInTheDocument(); - }); - - it('Check if active tab is lineage', async () => { - mockParams.tab = EntityTabs.LINEAGE; - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const lineage = await findByTestId(container, 'lineage'); - - expect(lineage).toBeInTheDocument(); - }); - - it('Check if active tab is custom properties', async () => { - mockParams.tab = EntityTabs.CUSTOM_PROPERTIES; - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const customProperties = await findByText( - container, - 'CustomPropertyTable.component' - ); - - expect(customProperties).toBeInTheDocument(); - }); - - it('Should create an observer if IntersectionObserver is available', async () => { - mockParams.tab = EntityTabs.CUSTOM_PROPERTIES; - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const obServerElement = await findByTestId(container, 'observer-element'); - - expect(obServerElement).toBeInTheDocument(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index cfb198c9b63e..1c7bacde5904 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -36,8 +36,6 @@ import { getEntityName } from '../../../../utils/EntityUtils'; import { getTagsWithoutTier } from '../../../../utils/TableUtils'; import { createTagObject } from '../../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../../AppRouter/withActivityFeed'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; @@ -54,7 +52,6 @@ const DataModelDetails = ({ dataModelData, dataModelPermissions, fetchDataModel, - createThread, handleFollowDataModel, handleUpdateTags, handleUpdateOwner, @@ -67,12 +64,9 @@ const DataModelDetails = ({ }: DataModelDetailsProps) => { const { t } = useTranslation(); const history = useHistory(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); - const { fqn: decodedDataModelFQN } = useFqn(); - const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -128,14 +122,6 @@ const DataModelDetails = ({ ); }; - const onThreadLinkSelect = (link: string) => { - setThreadLink(link); - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const handleTabChange = (tabValue: EntityTabs) => { if (tabValue !== activeTab) { history.push({ @@ -236,12 +222,8 @@ const DataModelDetails = ({ owner={owners} showActions={!deleted} onDescriptionUpdate={handleUpdateDescription} - onThreadLinkSelect={onThreadLinkSelect} - /> - + ), ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, @@ -266,7 +248,6 @@ const DataModelDetails = ({ viewAllPermission={dataModelPermissions.ViewAll} onExtensionUpdate={handelExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -289,7 +270,6 @@ const DataModelDetails = ({ editDescriptionPermission, entityName, handleTagSelection, - onThreadLinkSelect, handleColumnUpdateDataModel, handleUpdateDescription, ]); @@ -364,18 +344,6 @@ const DataModelDetails = ({ /> - - {threadLink ? ( - - ) : null} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx index 8b03e4027d1c..1913dd4eb2ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.interface.tsx @@ -13,7 +13,6 @@ import { EntityTags } from 'Models'; import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; -import { CreateThread } from '../../../../generated/api/feed/createThread'; import { Tag } from '../../../../generated/entity/classification/tag'; import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { Column } from '../../../../generated/entity/data/table'; @@ -26,7 +25,6 @@ export interface DataModelDetailsProps { dataModelData: DashboardDataModel; dataModelPermissions: OperationPermission; fetchDataModel: () => void; - createThread: (data: CreateThread) => Promise; handleFollowDataModel: () => Promise; handleUpdateTags: (selectedTags?: EntityTags[]) => Promise; handleUpdateOwner: (owner?: EntityReference[]) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx index 9e7dbe516e8a..d45ac532869f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx @@ -39,9 +39,8 @@ import TableDescription from '../../../../Database/TableDescription/TableDescrip import TableTags from '../../../../Database/TableTags/TableTags.component'; import { useGenericContext } from '../../../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; -import { ModelTabProps } from './ModelTab.interface'; -const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { +const ModelTab = () => { const { t } = useTranslation(); const [editColumnDescription, setEditColumnDescription] = useState(); const { @@ -49,7 +48,11 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { permissions, onUpdate, } = useGenericContext(); - const { columns: data, fullyQualifiedName: entityFqn } = dataModel; + const { + columns: data, + fullyQualifiedName: entityFqn, + deleted: isReadOnly, + } = dataModel; const { hasEditDescriptionPermission, @@ -150,7 +153,6 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { index={index} isReadOnly={isReadOnly} onClick={() => setEditColumnDescription(record)} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -175,7 +177,6 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -200,7 +201,6 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -213,7 +213,6 @@ const ModelTab = ({ isReadOnly, onThreadLinkSelect }: ModelTabProps) => { hasEditGlossaryTermPermission, editColumnDescription, hasEditDescriptionPermission, - onThreadLinkSelect, handleFieldTagsChange, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx deleted file mode 100644 index 503ebd6529ad..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.interface.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2023 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 { ThreadType } from '../../../../../generated/api/feed/createThread'; - -export interface ModelTabProps { - isReadOnly: boolean; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; -} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index dd9e037b0d3c..d41db7f85d4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -48,7 +48,7 @@ interface CommonWidgetsProps { } export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { - const { data, type, onUpdate, permissions, onThreadLinkSelect } = + const { data, type, onUpdate, permissions } = useGenericContext(); const { @@ -133,7 +133,6 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { selectedTags={tags} tagType={TagSource.Classification} onSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ); }, [type]); @@ -148,7 +147,6 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { selectedTags={tags} tagType={TagSource.Glossary} onSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ); }, []); @@ -170,7 +168,6 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { await onUpdate({ ...data, description: value }); } }} - onThreadLinkSelect={onThreadLinkSelect} /> ); }, []); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index ad1b8908ded9..2c73b3301325 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -309,7 +309,6 @@ const SchemaTable = () => { index={index} isReadOnly={deleted} onClick={() => handleUpdate(record)} - onThreadLinkSelect={onThreadLinkSelect} /> {getFrequentlyJoinedColumns( record?.name, @@ -484,7 +483,6 @@ const SchemaTable = () => { record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Classification, @@ -509,7 +507,6 @@ const SchemaTable = () => { record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Glossary, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx index 9246b15ae0ff..fe21f9aa2be0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx @@ -22,6 +22,7 @@ import EntityTasks from '../../../pages/TasksPage/EntityTasks/EntityTasks.compon import EntityLink from '../../../utils/EntityLink'; import { getEntityFeedLink } from '../../../utils/EntityUtils'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert'; import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; import { TableDescriptionProps } from './TableDescription.interface'; @@ -34,10 +35,10 @@ const TableDescription = ({ onClick, entityType, hasEditPermission, - onThreadLinkSelect, }: TableDescriptionProps) => { const { t } = useTranslation(); const { selectedUserSuggestions = [] } = useSuggestionsContext(); + const { onThreadLinkSelect } = useGenericContext(); const entityLink = useMemo( () => diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.interface.ts index 44b73350d8b3..996235b237b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.interface.ts @@ -13,7 +13,6 @@ import { EntityType } from '../../../enums/entity.enum'; import { Column } from '../../../generated/entity/data/table'; -import { ThreadType } from '../../../generated/entity/feed/thread'; export interface TableDescriptionProps { index: number; @@ -27,5 +26,4 @@ export interface TableDescriptionProps { hasEditPermission: boolean; isReadOnly?: boolean; onClick: () => void; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx index 23e67b89f9fb..633afbfc0f41 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx @@ -16,6 +16,7 @@ import { lowerCase } from 'lodash'; import React from 'react'; import { EntityField } from '../../../constants/Feeds.constants'; import EntityTasks from '../../../pages/TasksPage/EntityTasks/EntityTasks.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { TableTagsComponentProps, TableUnion } from './TableTags.interface'; @@ -28,10 +29,11 @@ const TableTags = ({ isReadOnly, hasTagEditAccess, showInlineEditTagButton, - onThreadLinkSelect, handleTagSelection, entityType, }: TableTagsComponentProps) => { + const { onThreadLinkSelect } = useGenericContext(); + return (
{ selectedTags: EntityTags[], editColumnTag: T ) => Promise; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; } export interface TableTagsProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx index 9822d47a7fc1..4b0bb2e3bce9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx @@ -51,7 +51,6 @@ describe('EntityRightPanel component test', () => { const mockDataProducts: EntityReference[] = []; const mockSelectedTags: EntityTags[] = []; const mockOnTagSelectionChange = jest.fn(); - const mockOnThreadLinkSelect = jest.fn(); const mockCustomProperties = { extension: { test1: 'test', @@ -72,7 +71,6 @@ describe('EntityRightPanel component test', () => { selectedTags={mockSelectedTags} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -94,7 +92,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -117,7 +114,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -139,7 +135,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -167,7 +162,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -193,7 +187,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -217,7 +210,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); @@ -242,7 +234,6 @@ describe('EntityRightPanel component test', () => { showDataProductContainer={false} onExtensionUpdate={mockExtensionUpdate} onTagSelectionChange={mockOnTagSelectionChange} - onThreadLinkSelect={mockOnThreadLinkSelect} /> ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index d729bad96427..bc30eaaf29bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -14,7 +14,6 @@ import { Space } from 'antd'; import { EntityTags } from 'Models'; import React from 'react'; import { EntityType } from '../../../enums/entity.enum'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { EntityReference } from '../../../generated/entity/type'; import { TagSource } from '../../../generated/type/tagLabel'; import { useFqn } from '../../../hooks/useFqn'; @@ -42,7 +41,6 @@ interface EntityRightPanelProps { afterSlot?: React.ReactNode; domain?: EntityReference; onTagSelectionChange?: (selectedTags: EntityTags[]) => Promise; - onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; viewAllPermission?: boolean; customProperties?: ExtentionEntities[T]; editCustomAttributePermission?: boolean; @@ -57,7 +55,6 @@ const EntityRightPanel = ({ editTagPermission, editGlossaryTermsPermission, onTagSelectionChange, - onThreadLinkSelect, beforeSlot, afterSlot, entityId, @@ -93,7 +90,6 @@ const EntityRightPanel = ({ showTaskHandler={showTaskHandler} tagType={TagSource.Classification} onSelectionChange={onTagSelectionChange} - onThreadLinkSelect={onThreadLinkSelect} /> ({ showTaskHandler={showTaskHandler} tagType={TagSource.Glossary} onSelectionChange={onTagSelectionChange} - onThreadLinkSelect={onThreadLinkSelect} /> {KnowledgeArticles && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx index 9fead26b338c..b5f5e52dc0ed 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx @@ -600,7 +600,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.DASHBOARD_DATA_MODEL} onUpdate={async () => noop()}> - + ); } else if ( @@ -616,7 +616,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { - return ; + return ; } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.EXPERTS)) { return ( { const { t } = useTranslation(); const history = useHistory(); @@ -197,10 +196,9 @@ const GlossaryDetails = ({ onSelectionChange={async (updatedTags: TagLabel[]) => await handleGlossaryUpdate({ ...glossary, tags: updatedTags }) } - onThreadLinkSelect={onThreadLinkSelect} /> ); - }, [tags, glossary, handleGlossaryUpdate, permissions, onThreadLinkSelect]); + }, [tags, glossary, handleGlossaryUpdate, permissions]); const widgets = useMemo(() => { const getWidgetFromKeyInternal = (widget: WidgetConfig) => { @@ -215,7 +213,6 @@ const GlossaryDetails = ({ owner={glossary?.owners} showActions={!glossary.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ); } else if ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.interface.ts index 388f47023d2b..28cab96a4491 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.interface.ts @@ -19,7 +19,6 @@ import { VotingDataProps } from '../../Entity/Voting/voting.interface'; export type GlossaryDetailsProps = { isVersionView?: boolean; permissions: OperationPermission; - termsLoading: boolean; updateGlossary: (value: Glossary) => Promise; updateVote?: (data: VotingDataProps) => Promise; @@ -27,5 +26,4 @@ export type GlossaryDetailsProps = { refreshGlossaryTerms: () => void; onAddGlossaryTerm: (glossaryTerm: GlossaryTerm | undefined) => void; onEditGlossaryTerm: (glossaryTerm: GlossaryTerm) => void; - onThreadLinkSelect: (value: string) => void; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index 028c9a3836f6..e420b67e36ed 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -82,7 +82,6 @@ const GlossaryTermsV1 = ({ updateVote, refreshActiveGlossaryTerm, isVersionView, - onThreadLinkSelect, }: GlossaryTermsV1Props) => { const { tab, version } = useParams<{ tab: string; version: string }>(); const { fqn: glossaryFqn } = useFqn(); @@ -196,7 +195,6 @@ const GlossaryTermsV1 = ({ (permissions.EditAll || permissions.EditCustomFields) } onExtensionUpdate={onExtensionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -390,15 +388,22 @@ const GlossaryTermsV1 = ({ /> -
- - + + data={updatedGlossaryTerm} + isVersionView={isVersionView} + permissions={permissions} + type={EntityType.GLOSSARY_TERM} + onUpdate={onTermUpdate}> + + + + {glossaryTerm.fullyQualifiedName && assetModalVisible && ( void; updateVote?: (data: VotingDataProps) => Promise; refreshActiveGlossaryTerm?: () => void; - onThreadLinkSelect: (value: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index cac06e4c692f..8cddc42bc197 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -48,13 +48,11 @@ import RelatedTerms from './RelatedTerms'; const ReactGridLayout = WidthProvider(RGL); type Props = { - onThreadLinkSelect: (value: string) => void; editCustomAttributePermission: boolean; onExtensionUpdate: (updatedTable: GlossaryTerm) => Promise; }; const GlossaryOverviewTab = ({ - onThreadLinkSelect, editCustomAttributePermission, onExtensionUpdate, }: Props) => { @@ -152,16 +150,9 @@ const GlossaryOverviewTab = ({ owner={selectedData?.owners} showActions={!selectedData.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ); - }, [ - glossaryDescription, - selectedData, - onDescriptionUpdate, - onThreadLinkSelect, - permissions, - ]); + }, [glossaryDescription, selectedData, onDescriptionUpdate, permissions]); const tagsWidget = useMemo(() => { return ( @@ -173,15 +164,9 @@ const GlossaryOverviewTab = ({ selectedTags={tags ?? []} tagType={TagSource.Classification} onSelectionChange={handleTagsUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ); - }, [ - tags, - selectedData.fullyQualifiedName, - hasEditTagsPermissions, - onThreadLinkSelect, - ]); + }, [tags, selectedData.fullyQualifiedName, hasEditTagsPermissions]); const domainWidget = useMemo(() => { return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.test.tsx deleted file mode 100644 index b408ae8fafcd..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023 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 { act, render } from '@testing-library/react'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import GlossaryOverviewTab from './GlossaryOverviewTab.component'; - -jest.mock('./GlossaryTermSynonyms', () => { - return jest.fn().mockReturnValue(

GlossaryTermSynonyms

); -}); -jest.mock('./RelatedTerms', () => { - return jest.fn().mockReturnValue(

RelatedTerms

); -}); -jest.mock('./GlossaryTermReferences', () => { - return jest.fn().mockReturnValue(

GlossaryTermReferences

); -}); - -jest.mock('../../../common/EntityDescription/DescriptionV1', () => { - return jest.fn().mockReturnValue(

Description

); -}); - -jest.mock('../../../common/ResizablePanels/ResizablePanels', () => { - return jest.fn().mockImplementation(({ firstPanel, secondPanel }) => ( -
- {firstPanel.children}
{secondPanel.children}
-
- )); -}); - -describe.skip('GlossaryOverviewTab', () => { - it('renders the component', async () => { - const { findByText } = render( - , - { wrapper: MemoryRouter } - ); - - await act(async () => { - const description = await findByText(/Description/i); - const synonymsContainer = await findByText(/GlossaryTermSynonyms/i); - const relatedTermsContainer = await findByText(/RelatedTerms/i); - const referencesContainer = await findByText(/GlossaryTermReferences/i); - - expect(description).toBeInTheDocument(); - expect(synonymsContainer).toBeInTheDocument(); - expect(relatedTermsContainer).toBeInTheDocument(); - expect(referencesContainer).toBeInTheDocument(); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx index b72b2dac7a60..98bdb1590ae9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx @@ -26,14 +26,9 @@ import { ResourceEntity, } from '../../context/PermissionProvider/PermissionProvider.interface'; import { EntityAction, EntityTabs } from '../../enums/entity.enum'; -import { - CreateThread, - ThreadType, -} from '../../generated/api/feed/createThread'; import { Glossary } from '../../generated/entity/data/glossary'; import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm'; import { VERSION_VIEW_GLOSSARY_PERMISSION } from '../../mocks/Glossary.mock'; -import { postThread } from '../../rest/feedsAPI'; import { addGlossaryTerm, getFirstLevelGlossaryTerms, @@ -44,8 +39,6 @@ import { getEntityDeleteMessage } from '../../utils/CommonUtils'; import { updateGlossaryTermByFqn } from '../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import Loader from '../common/Loader/Loader'; import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal'; import { GlossaryTermForm } from './AddGlossaryTermForm/AddGlossaryTermForm.interface'; @@ -75,11 +68,6 @@ const GlossaryV1 = ({ useParams<{ action: EntityAction; glossaryName: string; tab: string }>(); const history = useHistory(); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const [activeGlossaryTerm, setActiveGlossaryTerm] = useState(null); const { getEntityPermission } = usePermissionProvider(); @@ -113,17 +101,6 @@ const GlossaryV1 = ({ [action] ); - const onThreadPanelClose = () => { - setThreadLink(''); - }; - - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const fetchGlossaryTerm = async ( params?: ListGlossaryTermsParams, refresh?: boolean @@ -167,19 +144,6 @@ const GlossaryV1 = ({ } }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleDelete = async () => { const { id } = selectedData; if (isGlossaryActive) { @@ -416,7 +380,6 @@ const GlossaryV1 = ({ onEditGlossaryTerm={(term) => handleGlossaryTermModalAction(true, term ?? null) } - onThreadLinkSelect={onThreadLinkSelect} /> ) : ( handleGlossaryTermModalAction(true, term) } - onThreadLinkSelect={onThreadLinkSelect} /> ))} @@ -461,19 +423,6 @@ const GlossaryV1 = ({ onSave={handleGlossaryTermSave} /> )} - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.interface.ts index 67388c23b448..09d47fb1cdd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.interface.ts @@ -11,7 +11,6 @@ * limitations under the License. */ import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Metric } from '../../../generated/entity/data/metric'; import { DataAssetWithDomains } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; @@ -19,10 +18,9 @@ import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; export interface MetricDetailsProps { metricDetails: Metric; metricPermissions: OperationPermission; - onCreateThread: (data: CreateThread) => Promise; fetchMetricDetails: () => void; onFollowMetric: () => Promise; - onMetricUpdate: (updatedData: Metric, key: keyof Metric) => Promise; + onMetricUpdate: (updatedData: Metric, key?: keyof Metric) => Promise; onToggleDelete: (version?: number) => void; onUnFollowMetric: () => Promise; onUpdateMetricDetails: (data: DataAssetWithDomains) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index 94042e62e477..e646e8496c71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -25,7 +25,6 @@ import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Metric } from '../../../generated/entity/data/metric'; import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { TagLabel } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -40,9 +39,7 @@ import { import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; @@ -50,6 +47,7 @@ import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -61,7 +59,6 @@ import { MetricDetailsProps } from './MetricDetails.interface'; const MetricDetails: React.FC = ({ metricDetails, metricPermissions, - onCreateThread, fetchMetricDetails, onFollowMetric, onMetricUpdate, @@ -73,20 +70,14 @@ const MetricDetails: React.FC = ({ }: MetricDetailsProps) => { const { t } = useTranslation(); const { currentUser } = useApplicationStore(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab: activeTab = EntityTabs.OVERVIEW } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedMetricFqn } = useFqn(); const history = useHistory(); - const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const { owners, deleted, @@ -130,14 +121,6 @@ const MetricDetails: React.FC = ({ ); }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const onThreadPanelClose = () => setThreadLink(''); - const handleRestoreMetric = async () => { try { const { version: newVersion } = await restoreMetric(metricDetails.id); @@ -297,7 +280,6 @@ const MetricDetails: React.FC = ({ owner={metricDetails.owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -332,7 +314,6 @@ const MetricDetails: React.FC = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -427,7 +408,6 @@ const MetricDetails: React.FC = ({ deleted, handleFeedCount, onExtensionUpdate, - onThreadLinkSelect, handleTagSelection, onDescriptionUpdate, onDataProductsUpdate, @@ -469,32 +449,25 @@ const MetricDetails: React.FC = ({ onVersionClick={onVersionChange} /> -
- - + + data={metricDetails} + permissions={metricPermissions} + type={EntityType.METRIC} + onUpdate={onMetricUpdate}> + + + + <> - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModel.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModel.interface.ts index efc6d2bbe1c8..913a232618d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModel.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModel.interface.ts @@ -12,7 +12,6 @@ */ import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { ThreadType } from '../../../generated/api/feed/createThread'; import { Mlmodel } from '../../../generated/entity/data/mlmodel'; export interface MlModelFeaturesListProp { @@ -21,5 +20,4 @@ export interface MlModelFeaturesListProp { handleFeaturesUpdate: (features: Mlmodel['mlFeatures']) => Promise; isDeleted?: boolean; entityFqn: string; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index 89439242bd21..d1125f547fb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -29,7 +29,6 @@ import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { MlHyperParameter } from '../../../generated/api/data/createMlModel'; import { Tag } from '../../../generated/entity/classification/tag'; import { Mlmodel, MlStore } from '../../../generated/entity/data/mlmodel'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { TagLabel } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -42,9 +41,7 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; @@ -52,6 +49,7 @@ import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -69,7 +67,6 @@ const MlModelDetail: FC = ({ settingsUpdateHandler, updateMlModelFeatures, onExtensionUpdate, - createThread, onUpdateVote, versionHandler, tagUpdateHandler, @@ -78,7 +75,6 @@ const MlModelDetail: FC = ({ const { t } = useTranslation(); const { currentUser } = useApplicationStore(); const history = useHistory(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedMlModelFqn } = useFqn(); @@ -91,11 +87,6 @@ const MlModelDetail: FC = ({ DEFAULT_ENTITY_PERMISSION ); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const [threadLink, setThreadLink] = useState(''); - const { getEntityPermission } = usePermissionProvider(); const mlModelName = useMemo( @@ -230,17 +221,6 @@ const MlModelDetail: FC = ({ await updateMlModelFeatures({ ...mlModelDetail, mlFeatures: features }); }; - const handleThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - - const handleThreadPanelClose = () => { - setThreadLink(''); - }; - const getMlHyperParametersColumn: ColumnsType = useMemo( () => [ { @@ -407,7 +387,6 @@ const MlModelDetail: FC = ({ owner={mlModelDetail.owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={handleThreadLinkSelect} /> = ({ isDeleted={mlModelDetail.deleted} mlFeatures={mlModelDetail.mlFeatures} permissions={mlModelPermissions} - onThreadLinkSelect={handleThreadLinkSelect} /> ), @@ -441,7 +419,6 @@ const MlModelDetail: FC = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={handleThreadLinkSelect} /> ), @@ -533,7 +510,6 @@ const MlModelDetail: FC = ({ handleFeedCount, onExtensionUpdate, onFeaturesUpdate, - handleThreadLinkSelect, onDescriptionUpdate, deleted, editTagsPermission, @@ -571,33 +547,26 @@ const MlModelDetail: FC = ({ onVersionClick={versionHandler} /> - - - + + data={mlModelDetail} + permissions={mlModelPermissions} + type={EntityType.MLMODEL} + onUpdate={settingsUpdateHandler}> + + + + <> - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.interface.ts index 922780e0c03a..0a94fb4a35ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.interface.ts @@ -12,7 +12,6 @@ */ import { HTMLAttributes } from 'react'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Mlmodel } from '../../../generated/entity/data/mlmodel'; import { DataAssetWithDomains } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; @@ -29,7 +28,6 @@ export interface MlModelDetailProp extends HTMLAttributes { settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise; versionHandler: () => void; onExtensionUpdate: (updatedMlModel: Mlmodel) => Promise; - createThread: (data: CreateThread) => Promise; handleToggleDelete: (version?: number) => void; onUpdateVote: (data: QueryVote, id: string) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx index 3ec435b0a881..f71ae8721792 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx @@ -34,7 +34,6 @@ const MlModelFeaturesList = ({ permissions, isDeleted, entityFqn, - onThreadLinkSelect, }: MlModelFeaturesListProp) => { const { t } = useTranslation(); const [selectedFeature, setSelectedFeature] = useState( @@ -162,7 +161,6 @@ const MlModelFeaturesList = ({ record={feature} tags={feature.tags ?? []} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> @@ -186,7 +184,6 @@ const MlModelFeaturesList = ({ record={feature} tags={feature.tags ?? []} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> @@ -216,7 +213,6 @@ const MlModelFeaturesList = ({ setSelectedFeature(feature); setEditDescription(true); }} - onThreadLinkSelect={onThreadLinkSelect} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index 61b7bb02a4fc..59a84f20b77a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -33,7 +33,6 @@ import { PIPELINE_TASK_TABS } from '../../../constants/pipeline.constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Tag } from '../../../generated/entity/classification/tag'; import { Pipeline, @@ -41,7 +40,6 @@ import { TagLabel, Task, } from '../../../generated/entity/data/pipeline'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { Page } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/uiCustomization'; import { TagSource } from '../../../generated/type/schema'; @@ -49,7 +47,6 @@ import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { FeedCounts } from '../../../interface/feed.interface'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; -import { postThread } from '../../../rest/feedsAPI'; import { restorePipeline } from '../../../rest/pipelineAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; @@ -67,14 +64,13 @@ import { import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; +import { GenericProvider } from '../../GenericProvider/GenericProvider'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -101,7 +97,6 @@ const PipelineDetails = ({ const { tab } = useParams<{ tab: EntityTabs }>(); const { t } = useTranslation(); const { currentUser, selectedPersona } = useApplicationStore(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const userID = currentUser?.id ?? ''; const { deleted, @@ -138,14 +133,9 @@ const PipelineDetails = ({ FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - const [selectedExecution] = useState( pipelineStatus ); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); const [pipelinePermissions, setPipelinePermissions] = useState( DEFAULT_ENTITY_PERMISSION @@ -286,17 +276,6 @@ const PipelineDetails = ({ } }, [isFollowing, followPipelineHandler, unFollowPipelineHandler]); - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const handleTableTagSelection = async ( selectedTags: EntityTags[], editColumnTag: Task @@ -421,7 +400,6 @@ const PipelineDetails = ({ index={index} isReadOnly={deleted} onClick={() => setEditTask({ task: record, index })} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -452,7 +430,6 @@ const PipelineDetails = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), filters: tagFilter.Classification, @@ -480,7 +457,6 @@ const PipelineDetails = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -491,7 +467,6 @@ const PipelineDetails = ({ editTagsPermission, editGlossaryTermsPermission, getEntityName, - onThreadLinkSelect, handleTableTagSelection, editDescriptionPermission, ] @@ -509,19 +484,6 @@ const PipelineDetails = ({ } }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); @@ -578,7 +540,6 @@ const PipelineDetails = ({ handleTagSelection, onExtensionUpdate, onDescriptionUpdate, - onThreadLinkSelect, editDescriptionPermission: editDescriptionPermission ?? false, editTagsPermission: editTagsPermission ?? false, editGlossaryTermsPermission: editGlossaryTermsPermission ?? false, @@ -614,7 +575,6 @@ const PipelineDetails = ({ handleTagSelection, onExtensionUpdate, onDescriptionUpdate, - onThreadLinkSelect, editDescriptionPermission, editTagsPermission, editGlossaryTermsPermission, @@ -671,16 +631,21 @@ const PipelineDetails = ({ onVersionClick={versionHandler} /> - - - - + + data={pipelineDetails} + permissions={pipelinePermissions} + type={EntityType.PIPELINE} + onUpdate={settingsUpdateHandler}> + + + + {editTask && ( @@ -701,19 +666,6 @@ const PipelineDetails = ({ <> - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts index 29334f2fd362..5a0eb8fdc783 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts @@ -13,7 +13,6 @@ import { EntityTags } from 'Models'; import { ReactElement } from 'react'; -import { ThreadType } from '../../../generated/api/feed/createThread'; import { LabelType, State, TagSource } from '../../../generated/type/tagLabel'; import { DisplayType, LayoutType } from '../TagsViewer/TagsViewer.interface'; @@ -31,7 +30,6 @@ export type TagsContainerV2Props = { displayType?: DisplayType; layoutType?: LayoutType; onSelectionChange?: (selectedTags: EntityTags[]) => Promise; - onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; defaultState?: State; defaultLabelType?: LabelType; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx index de06003cf9fa..48afec36aadd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx @@ -39,6 +39,7 @@ import { } from '../../../utils/TasksUtils'; import { SelectOption } from '../../common/AsyncSelectList/AsyncSelectList.interface'; import { TableTagsProps } from '../../Database/TableTags/TableTags.interface'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagSelectForm from '../TagsSelectForm/TagsSelectForm.component'; import TagsV1 from '../TagsV1/TagsV1.component'; import TagsViewer from '../TagsViewer/TagsViewer'; @@ -59,7 +60,6 @@ const TagsContainerV2 = ({ showBottomEditButton, showInlineEditButton, onSelectionChange, - onThreadLinkSelect, children, defaultLabelType, defaultState, @@ -67,6 +67,7 @@ const TagsContainerV2 = ({ const history = useHistory(); const [form] = Form.useForm(); const { t } = useTranslation(); + const { onThreadLinkSelect } = useGenericContext(); const [isEditTags, setIsEditTags] = useState(false); const [tags, setTags] = useState(); @@ -297,7 +298,7 @@ const TagsContainerV2 = ({ {showTaskHandler && ( <> {tagType === TagSource.Classification && requestTagElement} - {onThreadLinkSelect && conversationThreadElement} + {conversationThreadElement} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index ef5039505a71..c6b4930cc4f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -25,7 +25,6 @@ import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Topic } from '../../../generated/entity/data/topic'; import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { ThreadType } from '../../../generated/entity/feed/thread'; import { TagLabel } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -40,9 +39,7 @@ import { import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -65,7 +62,6 @@ const TopicDetails: React.FC = ({ followTopicHandler, unFollowTopicHandler, versionHandler, - createThread, onTopicUpdate, topicPermissions, handleToggleDelete, @@ -73,21 +69,15 @@ const TopicDetails: React.FC = ({ }: TopicDetailsProps) => { const { t } = useTranslation(); const { currentUser } = useApplicationStore(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedTopicFQN } = useFqn(); const history = useHistory(); - const [threadLink, setThreadLink] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const { owners, deleted, @@ -131,14 +121,6 @@ const TopicDetails: React.FC = ({ ); }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const onThreadPanelClose = () => setThreadLink(''); - const handleSchemaFieldsUpdate = async ( updatedMessageSchema: Topic['messageSchema'] ) => { @@ -399,7 +381,6 @@ const TopicDetails: React.FC = ({ deleted, handleFeedCount, onExtensionUpdate, - onThreadLinkSelect, handleTagSelection, onDescriptionUpdate, onDataProductsUpdate, @@ -460,19 +441,6 @@ const TopicDetails: React.FC = ({ <> - - {threadLink ? ( - - ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts index 7bf9640f4237..1853dd90d169 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.interface.ts @@ -12,7 +12,6 @@ */ import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Topic } from '../../../generated/entity/data/topic'; import { DataAssetWithDomains } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { QueryVote } from '../../Database/TableQueries/TableQueries.interface'; @@ -22,7 +21,6 @@ export interface TopicDetailsProps { topicDetails: Topic; topicPermissions: OperationPermission; fetchTopic: () => void; - createThread: (data: CreateThread) => Promise; followTopicHandler: () => Promise; unFollowTopicHandler: () => Promise; versionHandler: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.test.tsx deleted file mode 100644 index 706a22220b35..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.test.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2022 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 { - findByTestId, - findByText, - render, - screen, -} from '@testing-library/react'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { EntityTabs } from '../../../enums/entity.enum'; -import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; -import TopicDetails from './TopicDetails.component'; -import { TopicDetailsProps } from './TopicDetails.interface'; -import { TOPIC_DETAILS } from './TopicDetails.mock'; - -jest.mock('../../common/EntitySummaryDetails/EntitySummaryDetails', () => { - return jest - .fn() - .mockReturnValue( -

EntitySummaryDetails component

- ); -}); -const mockUserTeam = [ - { - description: 'description', - displayName: 'Cloud_Infra', - id: 'id1', - name: 'Cloud_infra', - type: 'team', - }, - { - description: 'description', - displayName: 'Finance', - id: 'id2', - name: 'Finance', - type: 'team', - }, -]; - -const topicDetailsProps: TopicDetailsProps = { - topicDetails: TOPIC_DETAILS, - fetchTopic: jest.fn(), - followTopicHandler: jest.fn(), - unFollowTopicHandler: jest.fn(), - onTopicUpdate: jest.fn(), - versionHandler: jest.fn(), - createThread: jest.fn(), - topicPermissions: DEFAULT_ENTITY_PERMISSION, - handleToggleDelete: jest.fn(), - onUpdateVote: jest.fn(), -}; - -const mockParams = { - topicFQN: 'test', - tab: EntityTabs.SCHEMA, -}; - -jest.mock('../../../hooks/useCustomLocation/useCustomLocation', () => { - return jest.fn().mockImplementation(() => ({ pathname: 'topic' })); -}); - -jest.mock('react-router-dom', () => ({ - useHistory: jest.fn(), - useParams: jest.fn().mockImplementation(() => mockParams), -})); - -jest.mock('../../Lineage/Lineage.component', () => { - return jest.fn().mockReturnValue(

EntityLineage.component

); -}); - -jest.mock('../../common/EntityDescription/DescriptionV1', () => { - return jest.fn().mockReturnValue(

Description Component

); -}); - -jest.mock('../../common/TitleBreadcrumb/TitleBreadcrumb.component', () => { - return jest.fn().mockReturnValue(

Breadcrumb

); -}); - -jest.mock('../../PageLayoutV1/PageLayoutV1', () => { - return jest.fn().mockImplementation(({ children }) =>
{children}
); -}); - -jest.mock('../../common/RichTextEditor/RichTextEditorPreviewerV1', () => { - return jest.fn().mockReturnValue(

RichTextEditorPreviwer

); -}); - -jest.mock('../../common/CustomPropertyTable/CustomPropertyTable', () => ({ - CustomPropertyTable: jest - .fn() - .mockReturnValue(

CustomPropertyTable.component

), -})); - -jest.mock('../../ActivityFeed/FeedEditor/FeedEditor', () => { - return jest.fn().mockReturnValue(

FeedEditor.component

); -}); - -jest.mock('../TopicSchema/TopicSchema', () => { - return jest - .fn() - .mockReturnValue(
TopicSchema
); -}); - -jest.mock( - '../../Database/SampleDataWithMessages/SampleDataWithMessages', - () => { - return jest.fn().mockReturnValue(
SampleDataWithMessages
); - } -); - -jest.mock('../../../utils/CommonUtils', () => ({ - addToRecentViewed: jest.fn(), - getCountBadge: jest.fn(), - getPartialNameFromFQN: jest.fn().mockReturnValue('PartialNameFromFQN'), - getUserTeams: () => mockUserTeam, - getHtmlForNonAdminAction: jest.fn(), - getEntityPlaceHolder: jest.fn().mockReturnValue('value'), - getEntityName: jest.fn().mockReturnValue('entityName'), - getOwnerValue: jest.fn().mockReturnValue('Owner'), -})); - -describe.skip('Test TopicDetails component', () => { - it('Checks if the TopicDetails component has all the proper components rendered', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const tabs = await findByTestId(container, 'tabs'); - const schemaTab = await findByTestId(tabs, 'schema'); - const activityFeedTab = await findByTestId(tabs, 'activity_feed'); - const configTab = await findByTestId(tabs, 'config'); - - expect(tabs).toBeInTheDocument(); - expect(schemaTab).toBeInTheDocument(); - expect(activityFeedTab).toBeInTheDocument(); - expect(configTab).toBeInTheDocument(); - }); - - it('Check if active tab is schema', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - const schema = await findByTestId(container, 'schema'); - const schemaFields = await screen.findByTestId('schema-fields'); - - expect(schema).toBeInTheDocument(); - expect(schemaFields).toBeInTheDocument(); - }); - - it('Check if active tab is activity feed', async () => { - mockParams.tab = EntityTabs.ACTIVITY_FEED; - const { container } = render(, { - wrapper: MemoryRouter, - }); - const activityFeedList = await findByText(container, /ActivityFeedList/i); - - expect(activityFeedList).toBeInTheDocument(); - }); - - it('Check if active tab is sample data', async () => { - mockParams.tab = EntityTabs.SAMPLE_DATA; - const { container } = render(, { - wrapper: MemoryRouter, - }); - const sampleData = await findByText(container, 'SampleDataWithMessages'); - - expect(sampleData).toBeInTheDocument(); - }); - - it('Check if active tab is config', async () => { - mockParams.tab = EntityTabs.CONFIG; - const { container } = render(, { - wrapper: MemoryRouter, - }); - const config = await findByTestId(container, 'config-details'); - - expect(config).toBeInTheDocument(); - }); - - it('Should render lineage tab', async () => { - mockParams.tab = EntityTabs.LINEAGE; - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const detailContainer = await findByTestId(container, 'lineage-details'); - - expect(detailContainer).toBeInTheDocument(); - }); - - it('Check if active tab is custom properties', async () => { - mockParams.tab = EntityTabs.CUSTOM_PROPERTIES; - const { container } = render(, { - wrapper: MemoryRouter, - }); - const customProperties = await findByText( - container, - 'CustomPropertyTable.component' - ); - - expect(customProperties).toBeInTheDocument(); - }); - - it('Should create an observer if IntersectionObserver is available', async () => { - mockParams.tab = EntityTabs.ACTIVITY_FEED; - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const obServerElement = await findByTestId(container, 'observer-element'); - - expect(obServerElement).toBeInTheDocument(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index 3ce13f63e699..56a90a4a7225 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -23,7 +23,7 @@ import { import Table, { ColumnsType } from 'antd/lib/table'; import { Key } from 'antd/lib/table/interface'; import classNames from 'classnames'; -import { cloneDeep, groupBy, isEmpty, isUndefined, noop, uniqBy } from 'lodash'; +import { cloneDeep, groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; import { EntityTags, TagFilterOptions } from 'Models'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -82,7 +82,6 @@ const TopicSchemaFields: FC = ({ permissions, onUpdate, currentVersionData, - onThreadLinkSelect, } = useGenericContext(); const isReadOnly = useMemo(() => { @@ -242,7 +241,6 @@ const TopicSchemaFields: FC = ({ index={index} isReadOnly={isReadOnly} onClick={() => setEditFieldDescription(record)} - onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), }, @@ -264,7 +262,6 @@ const TopicSchemaFields: FC = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), filters: tagFilter.Classification, @@ -289,7 +286,6 @@ const TopicSchemaFields: FC = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect ?? noop} /> ), filters: tagFilter.Glossary, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts index 0bcb06dad070..09bc72ba4ad4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/Description.interface.ts @@ -22,7 +22,6 @@ export interface DescriptionProps { description?: string; isReadOnly?: boolean; entityType: EntityType; - onThreadLinkSelect?: (value: string) => void; onDescriptionUpdate?: (value: string) => Promise; onSuggest?: (value: string) => void; onEntityFieldSelect?: (value: string) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index d6daf2ac4fcb..182f58653e80 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -31,6 +31,7 @@ import { getUpdateDescriptionPath, TASK_ENTITIES, } from '../../../utils/TasksUtils'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert'; import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; @@ -48,7 +49,6 @@ const DescriptionV1 = ({ isReadOnly = false, removeBlur = false, entityName, - onThreadLinkSelect, entityType, wrapInCard = false, showActions = true, @@ -62,6 +62,7 @@ const DescriptionV1 = ({ useSuggestionsContext(); const [isEditDescription, setIsEditDescription] = useState(false); const { fqn: entityFqn } = useFqn(); + const { onThreadLinkSelect } = useGenericContext(); const handleRequestDescription = useCallback(() => { history.push( diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/databaseSchema.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/databaseSchema.ts index 6cb6e00f0f81..68ac3551cf4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/databaseSchema.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/databaseSchema.ts @@ -65,7 +65,7 @@ export interface DatabaseSchema { /** * Unique identifier that identifies this schema instance. */ - id?: string; + id: string; /** * Life Cycle properties of the entity */ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index 605676a63f9d..bcae15bfb766 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -25,11 +25,8 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import ActivityFeedProvider, { - useActivityFeedProvider, -} from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -63,11 +60,9 @@ import { EntityType, TabSpecificField, } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { APICollection } from '../../generated/entity/data/apiCollection'; import { APIEndpoint } from '../../generated/entity/data/apiEndpoint'; -import { ThreadType } from '../../generated/entity/feed/thread'; import { Include } from '../../generated/type/include'; import { TagLabel } from '../../generated/type/tagLabel'; import { usePaging } from '../../hooks/paging/usePaging'; @@ -83,12 +78,7 @@ import { getApiEndPoints, GetApiEndPointsType, } from '../../rest/apiEndpointsAPI'; -import { postThread } from '../../rest/feedsAPI'; -import { - getEntityMissingError, - getFeedCounts, - sortTagsCaseInsensitive, -} from '../../utils/CommonUtils'; +import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; @@ -98,7 +88,6 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import APIEndpointsTab from './APIEndpointsTab'; const APICollectionPage: FunctionComponent = () => { - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); const pagingInfo = usePaging(PAGE_SIZE); @@ -116,9 +105,6 @@ const APICollectionPage: FunctionComponent = () => { const { fqn: decodedAPICollectionFQN } = useFqn(); const history = useHistory(); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); const [isPermissionsLoading, setIsPermissionsLoading] = useState(true); const [apiCollection, setAPICollection] = useState( {} as APICollection @@ -131,7 +117,7 @@ const APICollectionPage: FunctionComponent = () => { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); + const [apiCollectionPermission, setAPICollectionPermission] = useState(DEFAULT_ENTITY_PERMISSION); const [showDeletedEndpoints, setShowDeletedEndpoints] = @@ -182,20 +168,6 @@ const APICollectionPage: FunctionComponent = () => { [apiCollectionPermission?.ViewAll, apiCollectionPermission?.ViewBasic] ); - const onThreadLinkSelect = useCallback( - (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }, - [] - ); - - const onThreadPanelClose = useCallback(() => { - setThreadLink(''); - }, []); - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -340,10 +312,7 @@ const APICollectionPage: FunctionComponent = () => { const res = await saveUpdatedAPICollectionData( updatedData as APICollection ); - setAPICollection({ - ...res, - tags: sortTagsCaseInsensitive(res.tags ?? []), - }); + setAPICollection(res); } catch (error) { showErrorToast(error as AxiosError, t('server.api-error')); } @@ -388,22 +357,6 @@ const APICollectionPage: FunctionComponent = () => { [apiCollection, saveUpdatedAPICollectionData] ); - const createThread = useCallback( - async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }, - [getEntityFeedCount] - ); - const handleToggleDelete = (version?: number) => { setAPICollection((prev) => { if (!prev) { @@ -584,7 +537,6 @@ const APICollectionPage: FunctionComponent = () => { showDeletedEndpoints={showDeletedEndpoints} onDescriptionUpdate={onDescriptionUpdate} onShowDeletedEndpointsChange={handleShowDeletedEndPoints} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -608,7 +560,6 @@ const APICollectionPage: FunctionComponent = () => { viewAllPermission={viewAllPermission} onExtensionUpdate={handleExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -741,20 +692,6 @@ const APICollectionPage: FunctionComponent = () => { onChange={activeTabHandler} /> -
- {threadLink ? ( - - ) : null} - )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index abbe90360b3f..b40a5c5157e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -31,7 +31,6 @@ import { APIEndpoint } from '../../generated/entity/data/apiEndpoint'; import { UsePagingInterface } from '../../hooks/paging/usePaging'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; - interface APIEndpointsTabProps { apiCollectionDetails: APICollection; apiEndpointsLoading: boolean; @@ -42,7 +41,6 @@ interface APIEndpointsTabProps { currentEndpointsPage: number; endpointPaginationHandler: NextPreviousProps['pagingHandler']; onDescriptionUpdate?: (updatedHTML: string) => Promise; - onThreadLinkSelect?: (link: string) => void; onShowDeletedEndpointsChange?: (value: boolean) => void; isVersionView?: boolean; pagingInfo: UsePagingInterface; @@ -57,7 +55,6 @@ function APIEndpointsTab({ currentEndpointsPage, endpointPaginationHandler, onDescriptionUpdate, - onThreadLinkSelect, showDeletedEndpoints = false, onShowDeletedEndpointsChange, isVersionView = false, @@ -131,7 +128,6 @@ function APIEndpointsTab({ isDescriptionExpanded={isEmpty(apiEndpoints)} showActions={!apiCollectionDetails.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APIEndpointPage/APIEndpointPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APIEndpointPage/APIEndpointPage.tsx index e1dc074babf6..467737f53d27 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APIEndpointPage/APIEndpointPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APIEndpointPage/APIEndpointPage.tsx @@ -31,7 +31,6 @@ import { import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { APIEndpoint } from '../../generated/entity/data/apiEndpoint'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; @@ -42,11 +41,9 @@ import { removeApiEndpointFollower, updateApiEndPointVote, } from '../../rest/apiEndpointsAPI'; -import { postThread } from '../../rest/feedsAPI'; import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; @@ -82,24 +79,16 @@ const APIEndpointPage = () => { const handleApiEndpointUpdate = async ( updatedData: APIEndpoint, - key: keyof APIEndpoint + key?: keyof APIEndpoint ) => { try { const res = await saveUpdatedApiEndpointData(updatedData); setApiEndpointDetails((previous) => { - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } - return { ...previous, - version: res.version, - [key]: res[key], + ...res, + ...(key && { [key]: res[key] }), }; }); } catch (error) { @@ -222,19 +211,6 @@ const APIEndpointPage = () => { ); }; - const handleCreateThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleToggleDelete = (version?: number) => { setApiEndpointDetails((prev) => { if (!prev) { @@ -305,7 +281,6 @@ const APIEndpointPage = () => { apiEndpointPermissions={apiEndpointPermissions} fetchAPIEndpointDetails={() => fetchApiEndPointDetail(apiEndpointFqn)} onApiEndpointUpdate={handleApiEndpointUpdate} - onCreateThread={handleCreateThread} onFollowApiEndPoint={followApiEndPoint} onToggleDelete={handleToggleDelete} onUnFollowApiEndPoint={unFollowApiEndPoint} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 6852d17e895d..139b627ba19b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -18,8 +18,6 @@ import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; @@ -47,10 +45,8 @@ import { EntityType, TabSpecificField, } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { Container } from '../../generated/entity/data/container'; -import { ThreadType } from '../../generated/entity/feed/thread'; import { Page } from '../../generated/system/ui/page'; import { PageType } from '../../generated/system/ui/uiCustomization'; import { Include } from '../../generated/type/include'; @@ -60,7 +56,6 @@ import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; import { getDocumentByFQN } from '../../rest/DocStoreAPI'; -import { postThread } from '../../rest/feedsAPI'; import { addContainerFollower, getContainerByName, @@ -73,7 +68,6 @@ import { addToRecentViewed, getEntityMissingError, getFeedCounts, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import containerDetailsClassBase from '../../utils/ContainerDetailsClassBase'; import { getEntityName } from '../../utils/EntityUtils'; @@ -91,7 +85,6 @@ const ContainerPage = () => { const { t } = useTranslation(); const { currentUser, selectedPersona } = useApplicationStore(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { tab } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedContainerName } = useFqn(); @@ -113,11 +106,6 @@ const ContainerPage = () => { FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); - const fetchContainerDetail = async (containerFQN: string) => { setIsLoading(true); try { @@ -143,10 +131,7 @@ const ContainerPage = () => { timestamp: 0, id: response.id, }); - setContainerData({ - ...response, - tags: sortTagsCaseInsensitive(response.tags ?? []), - }); + setContainerData(response); } catch (error) { showErrorToast(error as AxiosError); setHasError(true); @@ -473,10 +458,7 @@ const ContainerPage = () => { ...containerData, tags: updatedContainer.tags, }); - setContainerData({ - ...response, - tags: sortTagsCaseInsensitive(response.tags ?? []), - }); + setContainerData(response); } catch (error) { showErrorToast(error as AxiosError); } @@ -495,10 +477,7 @@ const ContainerPage = () => { ...containerData, extension: updatedContainer.extension, }); - setContainerData({ - ...response, - tags: sortTagsCaseInsensitive(response.tags ?? []), - }); + setContainerData(response); } catch (error) { showErrorToast(error as AxiosError); } @@ -535,30 +514,6 @@ const ContainerPage = () => { ) ); - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); @@ -605,7 +560,6 @@ const ContainerPage = () => { tags: tags ?? [], fetchContainerDetail, labelMap: tabLabelMap, - onThreadLinkSelect, handleUpdateDescription, handleUpdateDataModel, handleTagSelection, @@ -641,7 +595,6 @@ const ContainerPage = () => { handleUpdateDataModel, handleUpdateDescription, handleTagSelection, - onThreadLinkSelect, handleExtensionUpdate, customizedPage?.tabs, ]); @@ -760,19 +713,6 @@ const ContainerPage = () => { <> - - {threadLink ? ( - - ) : null} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx index 39914c435ff3..e4719e4e2cc5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx @@ -28,7 +28,6 @@ import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvi import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Chart } from '../../generated/entity/data/chart'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { useApplicationStore } from '../../hooks/useApplicationStore'; @@ -40,7 +39,6 @@ import { removeFollower, updateDashboardVotes, } from '../../rest/dashboardAPI'; -import { postThread } from '../../rest/feedsAPI'; import { addToRecentViewed, getEntityMissingError, @@ -232,19 +230,6 @@ const DashboardDetailsPage = () => { ); }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleToggleDelete = (version?: number) => { setDashboardDetails((prev) => { if (!prev) { @@ -307,7 +292,6 @@ const DashboardDetailsPage = () => { return ( fetchDashboardDetail(dashboardFQN)} followDashboardHandler={followDashboard} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index e9cad03a7340..e412437e9ad1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -38,7 +38,6 @@ import { import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { DashboardDataModel } from '../../generated/entity/data/dashboardDataModel'; import { Include } from '../../generated/type/include'; @@ -51,13 +50,10 @@ import { removeDataModelFollower, updateDataModelVotes, } from '../../rest/dataModelsAPI'; -import { postThread } from '../../rest/feedsAPI'; import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; -import { getSortedDataModelColumnTags } from '../../utils/DataModelsUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTierTags } from '../../utils/TableUtils'; @@ -115,18 +111,6 @@ const DataModelsPage = () => { } }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; const fetchDataModelDetails = async (dashboardDataModelFQN: string) => { setIsLoading(true); try { @@ -217,7 +201,7 @@ const DataModelsPage = () => { setDataModelData((prev) => ({ ...(prev as DashboardDataModel), - tags: sortTagsCaseInsensitive(newTags ?? []), + tags: newTags, version, })); } catch (error) { @@ -274,7 +258,7 @@ const DataModelsPage = () => { setDataModelData((prev) => ({ ...(prev as DashboardDataModel), - columns: getSortedDataModelColumnTags(newColumns), + columns: newColumns, version, })); } catch (error) { @@ -375,7 +359,6 @@ const DataModelsPage = () => { return ( fetchDataModelDetails(dashboardDataModelFQN)} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index 90d009227975..d234896ec13a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -26,14 +26,13 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import ProfilerSettings from '../../components/Database/Profiler/ProfilerSettings/ProfilerSettings'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; +import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -56,7 +55,6 @@ import { EntityType, TabSpecificField, } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { Database } from '../../generated/entity/data/database'; import { Page } from '../../generated/system/ui/page'; @@ -74,12 +72,7 @@ import { updateDatabaseVotes, } from '../../rest/databaseAPI'; import { getDocumentByFQN } from '../../rest/DocStoreAPI'; -import { postThread } from '../../rest/feedsAPI'; -import { - getEntityMissingError, - getFeedCounts, - sortTagsCaseInsensitive, -} from '../../utils/CommonUtils'; +import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; import { getQueryFilterForDatabase } from '../../utils/Database/Database.util'; import databaseClassBase from '../../utils/Database/DatabaseClassBase'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; @@ -95,7 +88,7 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const DatabaseDetails: FunctionComponent = () => { const { t } = useTranslation(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); + const { getEntityPermissionByFqn } = usePermissionProvider(); const { withinPageSearch } = useLocationSearch<{ withinPageSearch: string }>(); @@ -124,8 +117,6 @@ const DatabaseDetails: FunctionComponent = () => { FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - const [updateProfilerSetting, setUpdateProfilerSetting] = useState(false); @@ -167,14 +158,6 @@ const DatabaseDetails: FunctionComponent = () => { } }; - const onThreadLinkSelect = (link: string) => { - setThreadLink(link); - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -274,20 +257,11 @@ const DatabaseDetails: FunctionComponent = () => { } }; - const settingsUpdateHandler = async ( - data: Database, - key?: keyof Database - ) => { + const settingsUpdateHandler = async (data: Database) => { try { const res = await saveUpdatedDatabaseData(data); - setDatabase(() => { - if (key === 'tags') { - return { ...res, tags: sortTagsCaseInsensitive(res.tags ?? []) }; - } - - return res; - }); + setDatabase(res); } catch (error) { showErrorToast( error as AxiosError, @@ -310,19 +284,6 @@ const DatabaseDetails: FunctionComponent = () => { [database, database?.owners, settingsUpdateHandler] ); - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation-lowercase'), - }) - ); - } - }; - useEffect(() => { getEntityFeedCount(); }, []); @@ -392,8 +353,8 @@ const DatabaseDetails: FunctionComponent = () => { const onTagUpdate = async (selectedTags?: Array) => { if (selectedTags) { const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedTable = { ...database, tags: updatedTags }; - await settingsUpdateHandler(updatedTable as Database, 'tags'); + const updatedDatabase = { ...database, tags: updatedTags }; + await settingsUpdateHandler(updatedDatabase); } }; @@ -519,7 +480,6 @@ const DatabaseDetails: FunctionComponent = () => { handleFeedCount, getEntityFeedCount, onDescriptionUpdate, - onThreadLinkSelect, handleTagSelection, settingsUpdateHandler, deleted: database.deleted ?? false, @@ -627,27 +587,22 @@ const DatabaseDetails: FunctionComponent = () => { onVersionClick={versionHandler} /> - - - + + data={database} + permissions={databasePermission} + type={EntityType.DATABASE} + onUpdate={settingsUpdateHandler}> + + + + - {threadLink ? ( - - ) : null} {updateProfilerSetting && ( { - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); const pagingInfo = usePaging(PAGE_SIZE); @@ -112,9 +103,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { useParams<{ tab: EntityTabs }>(); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); const history = useHistory(); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); + const [isPermissionsLoading, setIsPermissionsLoading] = useState(true); const [databaseSchema, setDatabaseSchema] = useState( {} as DatabaseSchema @@ -128,7 +117,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); + const [databaseSchemaPermission, setDatabaseSchemaPermission] = useState(DEFAULT_ENTITY_PERMISSION); const [storedProcedureCount, setStoredProcedureCount] = useState(0); @@ -190,20 +179,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchemaPermission?.ViewAll, databaseSchemaPermission?.ViewBasic] ); - const onThreadLinkSelect = useCallback( - (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }, - [] - ); - - const onThreadPanelClose = useCallback(() => { - setThreadLink(''); - }, []); - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -367,10 +342,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { const res = await saveUpdatedDatabaseSchemaData( updatedData as DatabaseSchema ); - setDatabaseSchema({ - ...res, - tags: sortTagsCaseInsensitive(res.tags ?? []), - }); + setDatabaseSchema(res); } catch (error) { showErrorToast(error as AxiosError, t('server.api-error')); } @@ -415,22 +387,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchema, saveUpdatedDatabaseSchemaData] ); - const createThread = useCallback( - async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }, - [getEntityFeedCount] - ); - const handleToggleDelete = (version?: number) => { history.replace({ state: { @@ -611,6 +567,20 @@ const DatabaseSchemaPage: FunctionComponent = () => { }); }; + const handleUpdateDatabaseSchema = async (data: DatabaseSchema) => { + try { + const response = await saveUpdatedDatabaseSchemaData(data); + setDatabaseSchema(response); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-updating-error', { + entity: t('label.database-schema'), + }) + ); + } + }; + const tabs: TabsProps['items'] = useMemo( () => databaseSchemaClassBase.getDatabaseSchemaPageTabs({ @@ -634,7 +604,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { onEditCancel, handleExtensionUpdate, handleTagSelection, - onThreadLinkSelect, tablePaginationHandler, onDescriptionEdit, onDescriptionUpdate, @@ -664,7 +633,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { databaseSchemaPermission, handleExtensionUpdate, handleTagSelection, - onThreadLinkSelect, tablePaginationHandler, onEditCancel, onDescriptionEdit, @@ -750,29 +718,21 @@ const DatabaseSchemaPage: FunctionComponent = () => { /> )} - - - - - {threadLink ? ( - + data={databaseSchema} + permissions={databaseSchemaPermission} + type={EntityType.DATABASE_SCHEMA} + onUpdate={handleUpdateDatabaseSchema}> + + - ) : null} - + + {updateProfilerSetting && ( Promise; - onThreadLinkSelect?: (link: string) => void; onShowDeletedTablesChange?: (value: boolean) => void; isVersionView?: boolean; pagingInfo: UsePagingInterface; @@ -62,7 +61,6 @@ function SchemaTablesTab({ currentTablesPage, tablePaginationHandler, onDescriptionUpdate, - onThreadLinkSelect, showDeletedTables = false, onShowDeletedTablesChange, isVersionView = false, @@ -166,7 +164,6 @@ function SchemaTablesTab({ isDescriptionExpanded={isEmpty(tableData)} showActions={!databaseSchemaDetails.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx index 091effc5af8b..8d63fc00ce22 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx @@ -31,11 +31,9 @@ import { import { ClientErrors } from '../../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../../enums/entity.enum'; -import { CreateThread } from '../../../generated/api/feed/createThread'; import { Metric } from '../../../generated/entity/data/metric'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; -import { postThread } from '../../../rest/feedsAPI'; import { addMetricFollower, getMetricByFqn, @@ -46,7 +44,6 @@ import { import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../../utils/CommonUtils'; import { getEntityName } from '../../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; @@ -75,23 +72,18 @@ const MetricDetailsPage = () => { return patchMetric(metricId, jsonPatch); }; - const handleMetricUpdate = async (updatedData: Metric, key: keyof Metric) => { + const handleMetricUpdate = async ( + updatedData: Metric, + key?: keyof Metric + ) => { try { const res = await saveUpdatedMetricData(updatedData); setMetricDetails((previous) => { - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } - return { ...previous, version: res.version, - [key]: res[key], + ...(key && { [key]: res[key] }), }; }); } catch (error) { @@ -210,19 +202,6 @@ const MetricDetailsPage = () => { ); }; - const handleCreateThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleToggleDelete = (version?: number) => { setMetricDetails((prev) => { if (!prev) { @@ -292,7 +271,6 @@ const MetricDetailsPage = () => { fetchMetricDetails={() => fetchMetricDetail(metricFqn)} metricDetails={metricDetails} metricPermissions={metricPermissions} - onCreateThread={handleCreateThread} onFollowMetric={followMetric} onMetricUpdate={handleMetricUpdate} onToggleDelete={handleToggleDelete} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx index dee96f5d3270..d3e3fc733f7e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx @@ -189,7 +189,6 @@ const MetricListPage = () => { record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={noop} /> ), }, @@ -210,7 +209,6 @@ const MetricListPage = () => { record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={noop} /> ), }, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx index fbddb5f96e91..ca5366075bf8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx @@ -28,11 +28,9 @@ import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvi import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; -import { postThread } from '../../rest/feedsAPI'; import { addFollower, getMlModelByFQN, @@ -43,7 +41,6 @@ import { import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { defaultFields } from '../../utils/MlModelDetailsUtils'; @@ -191,8 +188,8 @@ const MlModelPage = () => { const { tags, version } = await saveUpdatedMlModelData(updatedMlModel); setMlModelDetail((preVDetail) => ({ ...preVDetail, - tags: sortTagsCaseInsensitive(tags ?? []), - version: version, + tags, + version, })); } catch (error) { showErrorToast( @@ -262,19 +259,6 @@ const MlModelPage = () => { [saveUpdatedMlModelData, setMlModelDetail, mlModelDetail] ); - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const versionHandler = () => { history.push( getVersionPath( @@ -342,7 +326,6 @@ const MlModelPage = () => { return ( fetchMlModelDetails(mlModelFqn)} followMlModelHandler={followMlModel} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx index daee9f7f2079..ba4820524106 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx @@ -41,14 +41,10 @@ import { import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { - defaultFields, - getFormattedPipelineDetails, -} from '../../utils/PipelineDetailsUtils'; +import { defaultFields } from '../../utils/PipelineDetailsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; const PipelineDetailsPage = () => { @@ -204,10 +200,7 @@ const PipelineDetailsPage = () => { const settingsUpdateHandler = async (updatedPipeline: Pipeline) => { try { const res = await saveUpdatedPipelineData(updatedPipeline); - setPipelineDetails({ - ...res, - tags: sortTagsCaseInsensitive(res.tags ?? []), - }); + setPipelineDetails(res); } catch (error) { showErrorToast( error as AxiosError, @@ -221,8 +214,7 @@ const PipelineDetailsPage = () => { const onTaskUpdate = async (jsonPatch: Array) => { try { const response = await patchPipelineDetails(pipelineId, jsonPatch); - const formattedPipelineDetails = getFormattedPipelineDetails(response); - setPipelineDetails(formattedPipelineDetails); + setPipelineDetails(response); } catch (error) { showErrorToast(error as AxiosError); } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index 39685aa36cbd..368cd7466c9b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -19,11 +19,8 @@ import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import ActivityFeedProvider, { - useActivityFeedProvider, -} from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -35,6 +32,7 @@ import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/D import SampleDataWithMessages from '../../components/Database/SampleDataWithMessages/SampleDataWithMessages'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; +import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import Lineage from '../../components/Lineage/Lineage.component'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; @@ -53,17 +51,12 @@ import { } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; -import { - CreateThread, - ThreadType, -} from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { SearchIndex, TagLabel } from '../../generated/entity/data/searchIndex'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; -import { postThread } from '../../rest/feedsAPI'; import { addFollower, getSearchIndexDetailsByFQN, @@ -72,11 +65,7 @@ import { restoreSearchIndex, updateSearchIndexVotes, } from '../../rest/SearchIndexAPI'; -import { - addToRecentViewed, - getFeedCounts, - sortTagsCaseInsensitive, -} from '../../utils/CommonUtils'; +import { addToRecentViewed, getFeedCounts } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { defaultFields } from '../../utils/SearchIndexUtils'; @@ -86,7 +75,6 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import SearchIndexFieldsTab from './SearchIndexFieldsTab/SearchIndexFieldsTab'; function SearchIndexDetailsPage() { - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { getEntityPermissionByFqn } = usePermissionProvider(); const { tab: activeTab = EntityTabs.FIELDS } = useParams<{ tab: string }>(); const { fqn: decodedSearchIndexFQN } = useFqn(); @@ -100,10 +88,6 @@ function SearchIndexDetailsPage() { FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); const [searchIndexPermissions, setSearchIndexPermissions] = useState(DEFAULT_ENTITY_PERMISSION); @@ -253,7 +237,7 @@ function SearchIndexDetailsPage() { const onSearchIndexUpdate = async ( updatedSearchIndex: SearchIndex, - key: keyof SearchIndex + key?: keyof SearchIndex ) => { try { const res = await saveUpdatedSearchIndexData(updatedSearchIndex); @@ -262,18 +246,11 @@ function SearchIndexDetailsPage() { if (!previous) { return; } - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } return { ...previous, - version: res.version, - [key]: res[key], + ...res, + ...(key && { [key]: res[key] }), }; }); } catch (error) { @@ -321,13 +298,6 @@ function SearchIndexDetailsPage() { } }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const handleDisplayNameUpdate = async (data: EntityName) => { if (!searchIndexDetails) { return; @@ -392,7 +362,6 @@ function SearchIndexDetailsPage() { owner={searchIndexDetails?.owners} showActions={!searchIndexDetails?.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> @@ -428,7 +396,6 @@ function SearchIndexDetailsPage() { viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -718,23 +685,6 @@ function SearchIndexDetailsPage() { } }, [decodedSearchIndexFQN, viewPermission]); - const onThreadPanelClose = () => { - setThreadLink(''); - }; - - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - if (loading) { return ; } @@ -777,32 +727,25 @@ function SearchIndexDetailsPage() { /> - - - + + data={searchIndexDetails} + permissions={searchIndexPermissions} + type={EntityType.SEARCH_INDEX} + onUpdate={onSearchIndexUpdate}> + + + + <> - - {threadLink ? ( - - ) : null} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts index 34a7cc1336f2..503d7693f06e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts @@ -11,7 +11,6 @@ * limitations under the License. */ -import { ThreadType } from '../../../generated/api/feed/createThread'; import { SearchIndexField } from '../../../generated/entity/data/searchIndex'; export interface SearchIndexFieldsTabProps { @@ -21,6 +20,5 @@ export interface SearchIndexFieldsTabProps { hasGlossaryTermEditAccess: boolean; isReadOnly?: boolean; entityFqn: string; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; onUpdate: (fields: Array) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx index 7f057d3e4e26..97af3eff43e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx @@ -21,7 +21,6 @@ import SearchIndexFieldsTab from './SearchIndexFieldsTab'; import { SearchIndexFieldsTabProps } from './SearchIndexFieldsTab.interface'; const mockOnUpdate = jest.fn(); -const mockOnThreadLinkSelect = jest.fn(); const mockProps: SearchIndexFieldsTabProps = { fields: MOCK_SEARCH_INDEX_FIELDS, @@ -30,7 +29,6 @@ const mockProps: SearchIndexFieldsTabProps = { hasTagEditAccess: true, hasGlossaryTermEditAccess: true, isReadOnly: false, - onThreadLinkSelect: mockOnThreadLinkSelect, entityFqn: 'search_service.search_index_fqn', }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx index ae7858e75a42..95cbff776261 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx @@ -33,7 +33,6 @@ function SearchIndexFieldsTab({ hasDescriptionEditAccess, hasTagEditAccess, hasGlossaryTermEditAccess, - onThreadLinkSelect, isReadOnly = false, entityFqn, }: SearchIndexFieldsTabProps) { @@ -130,7 +129,6 @@ function SearchIndexFieldsTab({ searchIndexFields={fields} searchText={searchText} searchedFields={searchedFields} - onThreadLinkSelect={onThreadLinkSelect} onUpdate={onUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts index 08d6565271e1..82469ba7fcbd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts @@ -13,7 +13,6 @@ import { ExpandableConfig } from 'antd/lib/table/interface'; import { ReactNode } from 'react'; -import { ThreadType } from '../../../generated/api/feed/createThread'; import { SearchIndexField } from '../../../generated/entity/data/searchIndex'; export interface SearchIndexFieldsTableProps { @@ -26,7 +25,6 @@ export interface SearchIndexFieldsTableProps { isReadOnly?: boolean; entityFqn: string; onUpdate: (fields: Array) => Promise; - onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; searchText?: string; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx index 89c3dac84001..ef2161de3e40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx @@ -66,7 +66,6 @@ const SearchIndexFieldsTable = ({ hasTagEditAccess, hasGlossaryTermEditAccess, isReadOnly = false, - onThreadLinkSelect, entityFqn, searchText, }: SearchIndexFieldsTableProps) => { @@ -181,16 +180,9 @@ const SearchIndexFieldsTable = ({ index={index} isReadOnly={isReadOnly} onClick={() => handleUpdate(record, index)} - onThreadLinkSelect={onThreadLinkSelect} /> ), - [ - entityFqn, - hasDescriptionEditAccess, - isReadOnly, - handleUpdate, - onThreadLinkSelect, - ] + [entityFqn, hasDescriptionEditAccess, isReadOnly, handleUpdate] ); const fields: ColumnsType = useMemo( @@ -250,7 +242,6 @@ const SearchIndexFieldsTable = ({ record={record} tags={tags} type={TagSource.Classification} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -274,7 +265,6 @@ const SearchIndexFieldsTable = ({ record={record} tags={tags} type={TagSource.Glossary} - onThreadLinkSelect={onThreadLinkSelect} /> ), }, @@ -288,8 +278,6 @@ const SearchIndexFieldsTable = ({ handleTagSelection, renderDataTypeDisplay, renderDescription, - handleTagSelection, - onThreadLinkSelect, tagFilter, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx index 38526a1083c3..914eebefe4d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceDetailsPage.tsx @@ -104,10 +104,7 @@ import { } from '../../rest/serviceAPI'; import { getContainers } from '../../rest/storageAPI'; import { getTopics } from '../../rest/topicsAPI'; -import { - getEntityMissingError, - sortTagsCaseInsensitive, -} from '../../utils/CommonUtils'; +import { getEntityMissingError } from '../../utils/CommonUtils'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; @@ -704,10 +701,7 @@ const ServiceDetailsPage: FunctionComponent = () => { jsonPatch ); - setServiceDetails({ - ...response, - tags: sortTagsCaseInsensitive(response.tags ?? []), - }); + setServiceDetails(response); } catch (err) { showErrorToast(err as AxiosError); } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index 56b682f77965..011b56f987bc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -17,8 +17,6 @@ import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; @@ -41,10 +39,6 @@ import { import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; -import { - CreateThread, - ThreadType, -} from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { StoredProcedure, @@ -56,7 +50,6 @@ import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; -import { postThread } from '../../rest/feedsAPI'; import { addStoredProceduresFollower, getStoredProceduresByFqn, @@ -65,11 +58,7 @@ import { restoreStoredProcedures, updateStoredProcedureVotes, } from '../../rest/storedProceduresAPI'; -import { - addToRecentViewed, - getFeedCounts, - sortTagsCaseInsensitive, -} from '../../utils/CommonUtils'; +import { addToRecentViewed, getFeedCounts } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { @@ -88,10 +77,7 @@ const StoredProcedurePage = () => { const { tab: activeTab = EntityTabs.CODE } = useParams<{ tab: string }>(); const { fqn: decodedStoredProcedureFQN } = useFqn(); - const { getEntityPermissionByFqn } = usePermissionProvider(); - const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); - const [isLoading, setIsLoading] = useState(true); const [storedProcedure, setStoredProcedure] = useState(); const [storedProcedurePermissions, setStoredProcedurePermissions] = @@ -101,11 +87,6 @@ const StoredProcedurePage = () => { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [threadLink, setThreadLink] = useState(''); - - const [threadType, setThreadType] = useState( - ThreadType.Conversation - ); const { id: storedProcedureId = '', @@ -233,18 +214,11 @@ const StoredProcedurePage = () => { if (!previous) { return; } - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } return { ...previous, - version: res.version, ...res, + ...(key && { [key]: res[key] }), }; }); } catch (error) { @@ -432,13 +406,6 @@ const StoredProcedurePage = () => { } }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { - setThreadLink(link); - if (threadType) { - setThreadType(threadType); - } - }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); @@ -449,23 +416,6 @@ const StoredProcedurePage = () => { } }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - - const onThreadPanelClose = () => { - setThreadLink(''); - }; - const onExtensionUpdate = useCallback( async (updatedData: StoredProcedure) => { if (storedProcedure) { @@ -541,7 +491,6 @@ const StoredProcedurePage = () => { onCancel, onDescriptionEdit, onDescriptionUpdate, - onThreadLinkSelect, storedProcedure: storedProcedure as StoredProcedure, tags: tags ?? [], editTagsPermission, @@ -642,7 +591,7 @@ const StoredProcedurePage = () => { /> - data={storedProcedure} permissions={storedProcedurePermissions} type={EntityType.STORED_PROCEDURE} @@ -664,19 +613,6 @@ const StoredProcedurePage = () => { <> - - {threadLink ? ( - - ) : null} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.test.tsx index 2bca2475d89f..72d6076b6479 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.test.tsx @@ -15,6 +15,16 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { FrequentlyJoinedTables } from './FrequentlyJoinedTables.component'; +jest.mock('../../../utils/TableUtils', () => ({ + getJoinsFromTableJoins: jest.fn().mockReturnValue([ + { + name: 'test', + fullyQualifiedName: 'test', + joinCount: 1, + }, + ]), +})); + describe('FrequentlyJoinedTables component', () => { it('should render the component', async () => { render(, { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx index 62459b87ecc2..cbdca66b91b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx @@ -115,10 +115,6 @@ jest.mock( } ); -jest.mock('../../components/Database/SchemaTab/SchemaTab.component', () => { - return jest.fn().mockImplementation(() =>

testSchemaTab

); -}); - jest.mock( '../../components/Database/Profiler/TableProfiler/TableProfiler', () => { @@ -209,6 +205,10 @@ jest.mock('../../hoc/LimitWrapper', () => { .mockImplementation(({ children }) => <>LimitWrapper{children}); }); +jest.mock('../../components/Database/TableGenericTab/TableGenericTab', () => ({ + TableGenericTab: jest.fn().mockImplementation(() => <>TableGenericTab), +})); + describe('TestDetailsPageV1 component', () => { it('TableDetailsPageV1 should fetch permissions', () => { render(); @@ -426,6 +426,6 @@ describe('TestDetailsPageV1 component', () => { fields: COMMON_API_FIELDS, }); - expect(await screen.findByText('testSchemaTab')).toBeInTheDocument(); + expect(await screen.findByText('TableGenericTab')).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 7e5dcdd4de5d..81e93ab348dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -79,7 +79,6 @@ import { addToRecentViewed, getFeedCounts, getPartialNameFromTableFQN, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { defaultFields } from '../../utils/DatasetDetailsUtils'; import EntityLink from '../../utils/EntityLink'; @@ -381,13 +380,6 @@ const TableDetailsPageV1: React.FC = () => { if (!previous) { return; } - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } const updatedObj = { ...previous, @@ -793,7 +785,7 @@ const TableDetailsPageV1: React.FC = () => { entity: t('label.table'), })} title="Table details"> - data={tableDetails} isVersionView={false} permissions={tablePermissions} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx index 0bff0d427c90..8179873daa4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx @@ -35,11 +35,9 @@ import { import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; import { Topic } from '../../generated/entity/data/topic'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; -import { postThread } from '../../rest/feedsAPI'; import { addFollower, getTopicByFqn, @@ -50,7 +48,6 @@ import { import { addToRecentViewed, getEntityMissingError, - sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; @@ -85,18 +82,10 @@ const TopicDetailsPage: FunctionComponent = () => { const res = await saveUpdatedTopicData(updatedData); setTopicDetails((previous) => { - if (key === 'tags') { - return { - ...previous, - version: res.version, - [key]: sortTagsCaseInsensitive(res.tags ?? []), - }; - } - return { ...previous, ...res, - version: res.version, + ...(key && { [key]: res[key] }), }; }); } catch (error) { @@ -215,19 +204,6 @@ const TopicDetailsPage: FunctionComponent = () => { ); }; - const createThread = async (data: CreateThread) => { - try { - await postThread(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.create-entity-error', { - entity: t('label.conversation'), - }) - ); - } - }; - const handleToggleDelete = (version?: number) => { setTopicDetails((prev) => { if (!prev) { @@ -294,7 +270,6 @@ const TopicDetailsPage: FunctionComponent = () => { return ( fetchTopicDetail(topicFQN)} followTopicHandler={followTopic} handleToggleDelete={handleToggleDelete} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts index c6cde50a0c1d..6649d1bad0a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts @@ -11,291 +11,9 @@ * limitations under the License. */ -import { AxiosError } from 'axios'; -import { cloneDeep } from 'lodash'; -import { ERROR_MESSAGE } from '../constants/constants'; -import { - LabelType, - State, - TagLabel, - TagSource, -} from '../generated/type/tagLabel'; -import { - digitFormatter, - filterSelectOptions, - getBase64EncodedString, - getIsErrorMatch, - getNameFromFQN, - getServiceTypeExploreQueryFilter, - getTagValue, - isDeleted, - prepareLabel, - reduceColorOpacity, - sortTagsCaseInsensitive, -} from './CommonUtils'; -import { - mockFQN, - mockFQNWithSpecialChar1, - mockFQNWithSpecialChar2, - mockFQNWithSpecialChar3, - mockFQNWithSpecialChar4, - mockFQNWithSpecialChar5, - mockTableNameFromFQN, - mockTableNameWithSpecialChar, - mockTableNameWithSpecialChar3, - mockTableNameWithSpecialChar4, - mockTableNameWithSpecialChar5, - mockTags, -} from './CommonUtils.mock'; - -const AXIOS_ERROR_MESSAGE = { - isAxiosError: true, - name: '', - message: '', - toJSON: () => ({}), - config: {}, - response: { - data: { message: 'Entity already exists' }, - status: 409, - statusText: 'Conflict', - headers: {}, - config: {}, - }, -}; +import { filterSelectOptions } from './CommonUtils'; describe('Tests for CommonUtils', () => { - describe('Tests for sortTagsCaseInsensitive function', () => { - it('Input of unsorted array to sortTagsCaseInsensitive should return array of tags sorted by tagFQN', () => { - expect(sortTagsCaseInsensitive(cloneDeep(mockTags))).toEqual(mockTags); - }); - - it('Function getNameFromFQN should return the correct table name for fqn without special characters', () => { - expect(getNameFromFQN(mockFQN)).toEqual(mockTableNameFromFQN); - }); - - it('Function getNameFromFQN should return the correct table name for sample_data.ecommerce_db."dim.api/client"', () => { - expect(getNameFromFQN(mockFQNWithSpecialChar1)).toEqual( - mockTableNameWithSpecialChar - ); - }); - - it('Function getNameFromFQN should return the correct table name for sample_data."ecommerce_db"."dim.api/client"', () => { - expect(getNameFromFQN(mockFQNWithSpecialChar2)).toEqual( - mockTableNameWithSpecialChar - ); - }); - - it('Regular expression in getNameFromFQN should not match for sample_data."ecommerce_db"."dim.api/"client" and should return names by default method', () => { - expect(getNameFromFQN(mockFQNWithSpecialChar3)).toEqual( - mockTableNameWithSpecialChar3 - ); - }); - - it('Regular expression in getNameFromFQN should not match for sample_data."ecommerce_db"."dim.api/client"" and should return names by default method', () => { - expect(getNameFromFQN(mockFQNWithSpecialChar4)).toEqual( - mockTableNameWithSpecialChar4 - ); - }); - - it('Regular expression in getNameFromFQN should not match for sample_data."ecommerce_db".""dim.api/client" and should return names by default method', () => { - expect(getNameFromFQN(mockFQNWithSpecialChar5)).toEqual( - mockTableNameWithSpecialChar5 - ); - }); - - it('Table name returned from the function getNameFromFQN should not contain double quotes in it', () => { - const result = getNameFromFQN(mockFQNWithSpecialChar2); - const containsDoubleQuotes = result.search('"'); - - expect(containsDoubleQuotes > 0).toBe(false); - }); - - // digitFormatter test - it('digitFormatter formatter should format number 1000 to 1k', () => { - const values = [ - { value: 1000, result: '1K' }, - { value: 10000, result: '10K' }, - { value: 10200, result: '10.2K' }, - { value: 10230, result: '10.23K' }, - { value: 1000000, result: '1M' }, - { value: 1230000, result: '1.23M' }, - { value: 100000000, result: '100M' }, - { value: 1000000000, result: '1B' }, - { value: 1500000000, result: '1.5B' }, - { value: 1550000000, result: '1.55B' }, - { value: 1000000000000, result: '1T' }, - { value: 1100000000000, result: '1.1T' }, - { value: 1110000000000, result: '1.11T' }, - ]; - - values.map(({ value, result }) => { - expect(digitFormatter(value)).toEqual(result); - }); - }); - - describe('Tests for sortTagsCaseInsensitive function', () => { - it('GetErrorMessage match function should return true if match found', () => { - const result = getIsErrorMatch( - AXIOS_ERROR_MESSAGE as AxiosError, - ERROR_MESSAGE.alreadyExist - ); - - expect(result).toBe(true); - }); - - it('GetErrorMessage match function should return true if match found if axios message is in responseMessage', () => { - const result = getIsErrorMatch( - { - ...AXIOS_ERROR_MESSAGE, - response: { data: { responseMessage: 'Entity already exists' } }, - } as AxiosError, - ERROR_MESSAGE.alreadyExist - ); - - expect(result).toBe(true); - }); - - it('GetErrorMessage function should return false if if axios message is in data', () => { - const result = getIsErrorMatch( - { - ...AXIOS_ERROR_MESSAGE, - response: { data: 'Entity already exists' }, - } as AxiosError, - ERROR_MESSAGE.alreadyExist - ); - - expect(result).toBe(true); - }); - - it('GetErrorMessage function should return false if message is in random key in data', () => { - const result = getIsErrorMatch( - { - ...AXIOS_ERROR_MESSAGE, - response: { data: { mess: 'Entity already exists' } }, - } as AxiosError, - ERROR_MESSAGE.alreadyExist - ); - - expect(result).toBe(false); - }); - - it('GetErrorMessage function should return false if match not found', () => { - const result = getIsErrorMatch( - AXIOS_ERROR_MESSAGE as AxiosError, - 'exit' - ); - - expect(result).toBe(false); - }); - }); - - it('should reduce color opacity by the given value', () => { - expect(reduceColorOpacity('#0000FF', 0)).toBe('rgba(0, 0, 255, 0)'); - expect(reduceColorOpacity('#00FF00', 0.25)).toBe('rgba(0, 255, 0, 0.25)'); - expect(reduceColorOpacity('#FF0000', 0.5)).toBe('rgba(255, 0, 0, 0.5)'); - expect(reduceColorOpacity('#FF0000', 0.75)).toBe('rgba(255, 0, 0, 0.75)'); - expect(reduceColorOpacity('#FF0000', -0.5)).toBe('rgba(255, 0, 0, -0.5)'); - expect(reduceColorOpacity('#FF0000', 0.05)).toBe('rgba(255, 0, 0, 0.05)'); - }); - - it('should return base64 encoded string for input text', () => { - const input = 'Hello World !@#$%^&*()'; - const expectedOutput = 'SGVsbG8gV29ybGQgIUAjJCVeJiooKQ=='; - const result = getBase64EncodedString(input); - - expect(result).toBe(expectedOutput); - }); - - it('should return the correct tag value for different inputs', () => { - // Test case 1: String input that does not start with `Tier` - let tag: string | TagLabel = 'exampleTag'; - let result = getTagValue(tag); - - expect(result).toEqual(tag); - - // Test case 2: String input that starts with `Tier` - tag = 'Tier.Tier1'; - result = getTagValue(tag); - - expect(result).toEqual('Tier1'); - - tag = { - labelType: LabelType.Manual, - source: TagSource.Classification, - tagFQN: 'Tier.Tier1', - state: State.Confirmed, - }; - result = getTagValue(tag); - - expect(result).toEqual({ - labelType: 'Manual', - source: 'Classification', - state: 'Confirmed', - tagFQN: 'Tier1', - }); - }); - - describe('prepareLabel', () => { - it('should return label for table entity type with quotes', () => { - const type = 'table'; - const fqn = 'database."table_data"'; - const withQuotes = true; - const expected = ''; - const result = prepareLabel(type, fqn, withQuotes); - - expect(result).toEqual(expected); - }); - - it('should return label for non-table entity type with quotes', () => { - const type = 'topic'; - const fqn = 'topics.orders'; - const withQuotes = true; - const expected = 'orders'; - const result = prepareLabel(type, fqn, withQuotes); - - expect(result).toEqual(expected); - }); - - it('should return label for table entity type without quotes', () => { - const type = 'table'; - const fqn = 'database.table'; - const withQuotes = false; - const expected = ''; - const result = prepareLabel(type, fqn, withQuotes); - - expect(result).toEqual(expected); - }); - - it('should return label for non-table entity type without quotes', () => { - const type = 'shopify'; - const fqn = 'database.shopify'; - const withQuotes = false; - const expected = 'shopify'; - const result = prepareLabel(type, fqn, withQuotes); - - expect(result).toEqual(expected); - }); - }); - - describe('getServiceTypeExploreQueryFilter', () => { - it('should return json string with the key', () => { - const result = getServiceTypeExploreQueryFilter('mysql'); - - expect(result).toEqual( - '{"query":{"bool":{"must":[{"bool":{"should":[{"term":{"serviceType":"mysql"}}]}}]}}}' - ); - }); - }); - - it('isDeleted should return proper boolean value', () => { - expect(isDeleted(true)).toBe(true); - expect(isDeleted(false)).toBe(false); - expect(isDeleted('false')).toBe(false); - expect(isDeleted(undefined)).toBe(false); - expect(isDeleted(null)).toBe(false); - }); - }); - describe('filterSelectOptions', () => { it('should return true if input matches option labelValue', () => { const input = 'test'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 83a6f83932b1..b2bb0dabbb85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -630,10 +630,6 @@ export const getTrimmedContent = (content: string, limit: number) => { return refinedContent.join(' '); }; -export const sortTagsCaseInsensitive = (tags: TagLabel[]) => { - return tags; -}; - export const Transi18next = ({ i18nKey, values, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index cd5b2756ca9b..9a91b23d7cf7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -131,7 +131,6 @@ export const getContainerDetailPageTabs = ({ containerChildrenData, fetchContainerChildren, isChildrenLoading, - onThreadLinkSelect, handleUpdateDescription, handleUpdateDataModel, handleTagSelection, @@ -172,7 +171,6 @@ export const getContainerDetailPageTabs = ({ owner={owners} showActions={!deleted} onDescriptionUpdate={handleUpdateDescription} - onThreadLinkSelect={onThreadLinkSelect} /> {isDataModelEmpty ? ( @@ -188,7 +186,6 @@ export const getContainerDetailPageTabs = ({ hasGlossaryTermEditAccess={editGlossaryTermsPermission} hasTagEditAccess={editTagsPermission} isReadOnly={Boolean(deleted)} - onThreadLinkSelect={onThreadLinkSelect} onUpdate={handleUpdateDataModel} /> )} @@ -216,7 +213,6 @@ export const getContainerDetailPageTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={handleExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index dde70b218d51..17f48cb281fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -29,7 +29,6 @@ import { FileFormat, StorageServiceType, } from '../generated/entity/data/container'; -import { ThreadType } from '../generated/entity/feed/thread'; import { Tab } from '../generated/system/ui/uiCustomization'; import { getContainerDetailPageTabs } from './ContainerDetailUtils'; @@ -52,7 +51,6 @@ export interface ContainerDetailPageTabProps { containerChildrenData: EntityReference[]; fetchContainerChildren: () => Promise; isChildrenLoading: boolean; - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; handleUpdateDescription: (description: string) => Promise; handleUpdateDataModel: (dataModel?: ContainerDataModel) => Promise; handleTagSelection: (tags: EntityTags[]) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index d4ed02a8b1f3..5a23b3b0f7be 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -47,7 +47,6 @@ export interface DashboardDetailsTabsProps { dashboardTags: Tag[]; handleFeedCount: (data: FeedCounts) => void; onDescriptionUpdate: (value: string) => Promise; - onThreadLinkSelect: (value: string) => void; handleTagSelection: (selectedTags: EntityTags[]) => Promise; onExtensionUpdate: (data: Dashboard) => Promise; feedCount: FeedCounts; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.test.ts deleted file mode 100644 index d37bba551bb4..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2022 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 { cloneDeep } from 'lodash'; -import { sortTagsForCharts } from './DashboardDetailsUtils'; -import { mockCharts } from './DashboardDetailsUtils.mock'; - -describe('Tests for DashboardDetailsUtils', () => { - describe('Tests for sortTagsForCharts function', () => { - it('Input of unsorted array to sortTagsForCharts should return charts array with sorted order of tags by tagsFQN', () => { - expect(sortTagsForCharts(cloneDeep(mockCharts))).toEqual(mockCharts); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 11f345229d16..448224e57580 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -33,19 +33,11 @@ import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Dashboard } from '../generated/entity/data/dashboard'; import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { getChartById } from '../rest/chartAPI'; -import { sortTagsCaseInsensitive } from './CommonUtils'; import { DashboardDetailsTabsProps } from './DashboardDetailsClassBase'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.DOMAIN},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; -export const sortTagsForCharts = (charts: ChartType[]) => { - return charts.map((chart) => ({ - ...chart, - tags: sortTagsCaseInsensitive(chart.tags || []), - })); -}; - export const fetchCharts = async (charts: Dashboard['charts']) => { let chartsData: ChartType[] = []; let promiseArr: Array> = []; @@ -141,7 +133,6 @@ export const getDashboardDetailPageTabs = ({ dashboardTags, handleFeedCount, onDescriptionUpdate, - onThreadLinkSelect, handleTagSelection, onExtensionUpdate, feedCount, @@ -173,12 +164,9 @@ export const getDashboardDetailPageTabs = ({ owner={dashboardDetails.owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> - + ), ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, @@ -201,7 +189,6 @@ export const getDashboardDetailPageTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataModelsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataModelsUtils.ts index 7b5896d785db..c17889ace577 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataModelsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataModelsUtils.ts @@ -13,7 +13,6 @@ import { isEmpty } from 'lodash'; import { TabSpecificField } from '../enums/entity.enum'; import { Column } from '../generated/entity/data/dashboardDataModel'; -import { sortTagsCaseInsensitive } from './CommonUtils'; export const updateDataModelColumnDescription = ( containerColumns: Column[] = [], @@ -39,9 +38,3 @@ export const updateDataModelColumnDescription = ( }; export const defaultFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.FOLLOWERS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}`; - -export const getSortedDataModelColumnTags = (column: Column[]): Column[] => - column.map((item) => ({ - ...item, - tags: sortTagsCaseInsensitive(item.tags ?? []), - })); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index e59cd6852d40..43d80adfd8fc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -212,7 +212,6 @@ export const getDatabasePageBaseTabs = ({ handleFeedCount, getEntityFeedCount, onDescriptionUpdate, - onThreadLinkSelect, handleTagSelection, settingsUpdateHandler, deleted, @@ -248,7 +247,6 @@ export const getDatabasePageBaseTabs = ({ isDescriptionExpanded={isEmpty(database)} showActions={!database.deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} />
@@ -277,7 +275,6 @@ export const getDatabasePageBaseTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={settingsUpdateHandler} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index 8a76cddf6f37..5ecc738d4d21 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -48,7 +48,6 @@ export interface DatabaseDetailPageTabProps { handleFeedCount: (data: FeedCounts) => void; getEntityFeedCount: () => void; onDescriptionUpdate: (updatedHTML: string) => Promise; - onThreadLinkSelect: (link: string) => void; handleTagSelection: (selectedTags: EntityTags[]) => Promise; settingsUpdateHandler: ( data: Database, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 6d8cc0e1af44..2c6dff54c586 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -30,7 +30,6 @@ import { State, } from '../generated/entity/data/databaseSchema'; import { Table } from '../generated/entity/data/table'; -import { ThreadType } from '../generated/entity/feed/thread'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../generated/type/tagLabel'; import { UsePagingInterface } from '../hooks/paging/usePaging'; @@ -59,7 +58,6 @@ export interface DatabaseSchemaPageTabProps { databaseSchemaPermission: OperationPermission; handleExtensionUpdate: (schema: DatabaseSchema) => Promise; handleTagSelection: (selectedTags: EntityTags[]) => Promise; - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; tablePaginationHandler: ({ cursorType, currentPage, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index c5fd0fe2aba4..dd17b0d815c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -48,7 +48,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ storedProcedureCount, handleExtensionUpdate, handleTagSelection, - onThreadLinkSelect, tablePaginationHandler, onDescriptionUpdate, handleShowDeletedTables, @@ -88,7 +87,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ tablePaginationHandler={tablePaginationHandler} onDescriptionUpdate={onDescriptionUpdate} onShowDeletedTablesChange={handleShowDeletedTables} - onThreadLinkSelect={onThreadLinkSelect} /> ), @@ -112,7 +110,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={handleExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index 8a8912127a83..a52082a16f5d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -32,7 +32,6 @@ import { StatusType, Task, } from '../generated/entity/data/pipeline'; -import { ThreadType } from '../generated/entity/feed/thread'; import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagLabel, TagSource } from '../generated/type/tagLabel'; @@ -55,7 +54,6 @@ export interface PipelineDetailPageTabProps { handleTagSelection: (selectedTags: TagLabel[]) => Promise; onDescriptionUpdate: (value: string) => Promise; onExtensionUpdate: (updatedPipeline: Pipeline) => Promise; - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; pipelineDetails: Pipeline; pipelineFQN: string; tasksInternal: Task[]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.test.ts deleted file mode 100644 index 1175dbfe384f..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2023 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 { - mockPipelineDetails, - mockPipelineDetailsWithoutTaskTags, -} from './mocks/PipelineDetailsUtils.mock'; -import { getFormattedPipelineDetails } from './PipelineDetailsUtils'; - -describe('PipelineDetailsUtils test', () => { - it('getFormattedPipelineDetails should return pipeline details with sorted tags for tasks', () => { - const results = getFormattedPipelineDetails(mockPipelineDetails); - - expect(results).toEqual(mockPipelineDetails); - }); - - it('getFormattedPipelineDetails should return pipeline details without any changes in case no tasks are present in it', () => { - const modifiedPipelineDetails = { - ...mockPipelineDetails, - tasks: undefined, - }; - - const results = getFormattedPipelineDetails(modifiedPipelineDetails); - - expect(results).toEqual(modifiedPipelineDetails); - }); - - it('getFormattedPipelineDetails should return pipeline details without any changes in case no tags are present for the tasks', () => { - const results = getFormattedPipelineDetails( - mockPipelineDetailsWithoutTaskTags - ); - - expect(results).toEqual(mockPipelineDetailsWithoutTaskTags); - }); - - it('getFormattedPipelineDetails should return pipeline details without any changes if empty array is present for tasks field', () => { - const results = getFormattedPipelineDetails({ - ...mockPipelineDetails, - tasks: [], - }); - - expect(results).toEqual({ - ...mockPipelineDetails, - tasks: [], - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index 1166bd2f88b0..e82d65a71dca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -13,7 +13,7 @@ import { Col, Radio, Row, Table } from 'antd'; import { t } from 'i18next'; -import { isEmpty, isUndefined } from 'lodash'; +import { isEmpty } from 'lodash'; import React from 'react'; import { ReactComponent as IconFailBadge } from '../assets/svg/fail-badge.svg'; import { ReactComponent as IconSkippedBadge } from '../assets/svg/skipped-badge.svg'; @@ -31,12 +31,7 @@ import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; -import { - Pipeline, - StatusType, - TaskStatus, -} from '../generated/entity/data/pipeline'; -import { sortTagsCaseInsensitive } from './CommonUtils'; +import { StatusType, TaskStatus } from '../generated/entity/data/pipeline'; import { PipelineDetailPageTabProps } from './PipelineClassBase'; // eslint-disable-next-line max-len @@ -59,26 +54,6 @@ export const getStatusBadgeIcon = (status?: StatusType) => { } }; -export const getFormattedPipelineDetails = ( - pipelineDetails: Pipeline -): Pipeline => { - if (pipelineDetails.tasks) { - const updatedTasks = pipelineDetails.tasks.map((task) => ({ - ...task, - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - tags: isUndefined(task.tags) - ? undefined - : sortTagsCaseInsensitive(task.tags), - })); - - return { ...pipelineDetails, tasks: updatedTasks }; - } else { - return pipelineDetails; - } -}; - export const getPipelineDetailPageTabs = ({ description, editDescriptionPermission, @@ -91,7 +66,6 @@ export const getPipelineDetailPageTabs = ({ handleTagSelection, onDescriptionUpdate, onExtensionUpdate, - onThreadLinkSelect, pipelineDetails, pipelineFQN, tasksInternal, @@ -131,7 +105,6 @@ export const getPipelineDetailPageTabs = ({ owner={owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> @@ -185,7 +158,6 @@ export const getPipelineDetailPageTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx index fd85984bd5d8..7e0ff71d7885 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx @@ -15,28 +15,15 @@ import { uniqueId } from 'lodash'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; import { SearchIndexField } from '../generated/entity/data/searchIndex'; -import { sortTagsCaseInsensitive } from './CommonUtils'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.FIELDS},${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; -export const makeRow = (column: SearchIndexField) => { - return { - description: column.description ?? '', - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - tags: sortTagsCaseInsensitive(column.tags ?? []), - key: column?.name, - ...column, - }; -}; - export const makeData = ( columns: SearchIndexField[] = [] ): Array => { return columns.map((column) => ({ - ...makeRow(column), + ...column, id: uniqueId(column.name), children: column.children ? makeData(column.children) : undefined, })); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index 32466d6537c6..e6cb74d5301c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -24,7 +24,6 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { Tag } from '../generated/entity/classification/tag'; import { StoredProcedure } from '../generated/entity/data/storedProcedure'; -import { ThreadType } from '../generated/entity/feed/thread'; import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; import { FeedCounts } from '../interface/feed.interface'; @@ -48,7 +47,6 @@ export interface StoredProcedureDetailPageTabProps { onCancel: () => void; onDescriptionEdit: () => void; onDescriptionUpdate: (value: string) => Promise; - onThreadLinkSelect: (link: string, threadType?: ThreadType) => void; storedProcedure: StoredProcedure; tags: Tag[]; editTagsPermission: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 62c1f74d18e2..91d0d9a4190a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -110,7 +110,6 @@ export const getStoredProcedureDetailsPageTabs = ({ owners, editDescriptionPermission, onDescriptionUpdate, - onThreadLinkSelect, storedProcedure, tags, editTagsPermission, @@ -151,7 +150,6 @@ export const getStoredProcedureDetailsPageTabs = ({ owner={owners} showActions={!deleted} onDescriptionUpdate={onDescriptionUpdate} - onThreadLinkSelect={onThreadLinkSelect} /> @@ -187,7 +185,6 @@ export const getStoredProcedureDetailsPageTabs = ({ viewAllPermission={viewAllPermission} onExtensionUpdate={onExtensionUpdate} onTagSelectionChange={handleTagSelection} - onThreadLinkSelect={onThreadLinkSelect} /> ), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 2a186feaab73..5cbf47ff8674 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -119,7 +119,6 @@ import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; import { getPartialNameFromTableFQN, getTableFQNFromColumnFQN, - sortTagsCaseInsensitive, } from './CommonUtils'; import EntityLink from './EntityLink'; import searchClassBase from './SearchClassBase'; @@ -479,23 +478,11 @@ export const getServiceIcon = (source: SourceType) => { } }; -export const makeRow = (column: T) => { - return { - description: column.description ?? '', - // Sorting tags as the response of PATCH request does not return the sorted order - // of tags, but is stored in sorted manner in the database - // which leads to wrong PATCH payload sent after further tags removal - tags: sortTagsCaseInsensitive(column.tags ?? []), - key: column?.name, - ...column, - }; -}; - export const makeData = ( columns: T[] = [] ): Array => { return columns.map((column) => ({ - ...makeRow(column), + ...column, id: uniqueId(column.name), children: column.children ? makeData(column.children as T[]) : undefined, })); From 7203ffaea13c3e7bf49c9377137dfe2c24def401 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:43:20 +0530 Subject: [PATCH 22/63] fix errors --- .../DataAssetsHeader.component.tsx | 14 ++----- .../DataAssetsHeader.interface.ts | 37 ++++++++++++++----- .../ui/src/utils/DataAssetsHeader.utils.tsx | 8 ++++ .../src/main/resources/ui/yarn.lock | 16 +------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx index 42a8ced1be73..c113873bd3ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx @@ -57,7 +57,10 @@ import { SearchSourceAlias } from '../../../interface/search.interface'; import { getActiveAnnouncement } from '../../../rest/feedsAPI'; import { getDataQualityLineage } from '../../../rest/lineageAPI'; import { getContainerByName } from '../../../rest/storageAPI'; -import { getDataAssetsHeaderInfo } from '../../../utils/DataAssetsHeader.utils'; +import { + getDataAssetsHeaderInfo, + isDataAssetsWithServiceField, +} from '../../../utils/DataAssetsHeader.utils'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { getEntityFeedLink, @@ -81,9 +84,7 @@ import './data-asset-header.less'; import { DataAssetHeaderInfo, DataAssetsHeaderProps, - DataAssetsType, DataAssetsWithFollowersField, - DataAssetsWithServiceField, EntitiesWithDomainField, } from './DataAssetsHeader.interface'; @@ -379,13 +380,6 @@ export const DataAssetsHeader = ({ setTimeout(() => setCopyTooltip(''), 2000); }; - const isDataAssetsWithServiceField = useCallback( - (asset: DataAssetsType): asset is DataAssetsWithServiceField => { - return (asset as DataAssetsWithServiceField).service !== undefined; - }, - [] - ); - const dataAssetServiceName = useMemo(() => { if (isDataAssetsWithServiceField(dataAsset)) { return dataAsset.service?.name ?? ''; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts index 146d00123cf0..c596da8fda6d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface.ts @@ -79,17 +79,36 @@ export type DataAssetsWithoutServiceField = | MetadataService | StorageService | SearchService - | APIService; + | APIService + | Metric; -export type DataAssetsWithFollowersField = Exclude< - DataAssetsType, - DataAssetsWithoutServiceField | Database | DatabaseSchema | APICollection ->; +export type DataAssetsWithFollowersField = + | Table + | Topic + | Dashboard + | Pipeline + | Mlmodel + | Container + | SearchIndex + | DashboardDataModel + | StoredProcedure + | APIEndpoint + | Metric; -export type DataAssetsWithServiceField = Exclude< - DataAssetsType, - DataAssetsWithoutServiceField | Metric ->; +export type DataAssetsWithServiceField = + | Table + | Topic + | Dashboard + | Pipeline + | Mlmodel + | Container + | SearchIndex + | Database + | DashboardDataModel + | StoredProcedure + | DatabaseSchema + | APICollection + | APIEndpoint; export type DataAssetWithDomains = | Exclude diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx index e5aa67619a17..c580c2a71a1d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx @@ -22,6 +22,8 @@ import { import { DataAssetHeaderInfo, DataAssetsHeaderProps, + DataAssetsType, + DataAssetsWithServiceField, } from '../components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; import { getEntityDetailsPath, @@ -510,3 +512,9 @@ export const getDataAssetsHeaderInfo = ( return returnData; }; + +export const isDataAssetsWithServiceField = ( + asset: DataAssetsType +): asset is DataAssetsWithServiceField => { + return (asset as DataAssetsWithServiceField).service !== undefined; +}; diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index c97f2b9b7609..c54df1eea46c 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -13220,21 +13220,7 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.2: +serialize-javascript@6.0.2, serialize-javascript@^4.0.0, serialize-javascript@^5.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== From 38fc61f1c18bcee88df422857ff9b0bdafaf18ef Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:17:29 +0530 Subject: [PATCH 23/63] fix tests --- .../ContainerChildren.test.tsx | 6 ++ .../EntityRightPanel.test.tsx | 9 +++ .../Topic/TopicSchema/TopicSchema.test.tsx | 63 +++++++++++++------ .../ContainerPage/ContainerPage.test.tsx | 18 ------ .../DataModelPage/DataModelPage.test.tsx | 22 +------ 5 files changed, 59 insertions(+), 59 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx index 6a1914b02a77..98cec2d651b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx @@ -38,6 +38,12 @@ const mockDataProps = { fetchChildren: mockFetchChildren, }; +jest.mock('../../GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockImplementation(() => ({ + data: { children: mockChildrenList }, + })), +})); + describe('ContainerChildren', () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx index 4b0bb2e3bce9..7434ce667c1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx @@ -47,6 +47,15 @@ jest.mock('react-router-dom', () => ({ .mockImplementation(({ children, ...rest }) => {children}), })); +jest.mock( + '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component', + () => ({ + PartitionedKeys: jest + .fn() + .mockImplementation(() =>
PartitionedKeys
), + }) +); + describe('EntityRightPanel component test', () => { const mockDataProducts: EntityReference[] = []; const mockSelectedTags: EntityTags[] = []; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx index cbf32ea742c2..9af08133c0ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx @@ -29,6 +29,27 @@ import { TopicSchemaFieldsProps } from './TopicSchema.interface'; const mockProps: TopicSchemaFieldsProps = {}; +jest.mock('../../Database/TableDescription/TableDescription.component', () => + jest.fn().mockImplementation(({ onClick, isReadOnly }) => ( +
+ Table Description + {!isReadOnly && ( + + )} +
+ )) +); + +jest.mock('../../common/RichTextEditor/RichTextEditorPreviewerV1', () => + jest + .fn() + .mockReturnValue( +
Description Preview
+ ) +); + jest.mock('../../../utils/TagsUtils', () => ({ getAllTagsList: jest.fn().mockImplementation(() => Promise.resolve([])), getTagsHierarchy: jest.fn().mockReturnValue([]), @@ -81,32 +102,33 @@ jest.mock('../../Database/SchemaEditor/SchemaEditor', () => ); const mockOnUpdate = jest.fn(); +const mockTopicDetails = { + columns: [], + displayName: 'test-display-name', + description: 'test-description', + tags: [], + owner: 'test-owner', + deleted: false, + service: { + id: 'test-service-id', + name: 'test-service-name', + displayName: 'test-service-display-name', + description: 'test-service-description', + tags: [], + owner: 'test-owner', + }, + messageSchema: MESSAGE_SCHEMA as Topic['messageSchema'], +}; jest.mock('../../GenericProvider/GenericProvider', () => ({ - useGenericContext: jest.fn().mockReturnValue({ - data: { - columns: [], - displayName: 'test-display-name', - description: 'test-description', - tags: [], - owner: 'test-owner', - service: { - id: 'test-service-id', - name: 'test-service-name', - displayName: 'test-service-display-name', - description: 'test-service-description', - tags: [], - owner: 'test-owner', - }, - messageSchema: MESSAGE_SCHEMA as Topic['messageSchema'], - }, + useGenericContext: jest.fn().mockImplementation(() => ({ + data: mockTopicDetails, isVersionView: false, permissions: { EditAll: true, }, onUpdate: mockOnUpdate, - currentVersionData: {}, - }), + })), })); jest.mock('../../../hooks/useFqn', () => ({ @@ -129,7 +151,7 @@ describe('Topic Schema', () => { const name = await findByText(row1, 'Order'); const dataType = await findByText(row1, 'RECORD'); - const description = await findByText(row1, 'Description Preview'); + const description = await findByText(row1, 'Table Description'); const tagsContainer = await findAllByTestId(row1, 'table-tag-container'); expect(name).toBeInTheDocument(); @@ -184,6 +206,7 @@ describe('Topic Schema', () => { }); it('Should not render the edit action if isReadOnly', async () => { + mockTopicDetails.deleted = true; render(); const rows = await screen.findAllByRole('row'); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx index 2f1e1c90c04e..fa9468dec711 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx @@ -325,24 +325,6 @@ describe('Container Page Component', () => { expect(screen.getByText('EntityRightPanel')).toBeVisible(); }); - it('activity thread panel should render after selecting thread link', async () => { - await act(async () => { - render(); - - expect(screen.getByText('Loader')).toBeVisible(); - }); - - const DescriptionV1 = screen.getByText('DescriptionV1'); - - expect(DescriptionV1).toBeVisible(); - - expect(screen.queryByText('ActivityThreadPanel')).not.toBeInTheDocument(); - - userEvent.click(DescriptionV1); - - expect(screen.getByText('ActivityThreadPanel')).toBeInTheDocument(); - }); - it('onClick of follow container should call addContainerFollower', async () => { await act(async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx index 03e4496e6648..5f12345e9a6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx @@ -37,7 +37,6 @@ const mockGetDataModelByFqn = jest.fn().mockResolvedValue({}); const mockPatchDataModelDetails = jest.fn().mockResolvedValue({}); const mockRemoveDataModelFollower = jest.fn().mockResolvedValue({}); const mockUpdateDataModelVotes = jest.fn().mockResolvedValue({}); -const mockPostThread = jest.fn().mockResolvedValue({}); const mockGetEntityPermissionByFqn = jest.fn().mockResolvedValue({ ViewAll: true, ViewBasic: true, @@ -120,10 +119,6 @@ jest.mock('../../rest/dataModelsAPI', () => ({ updateDataModelVotes: () => mockUpdateDataModelVotes(), })); -jest.mock('../../rest/feedsAPI', () => ({ - postThread: () => mockPostThread(), -})); - jest.mock('../../utils/CommonUtils', () => ({ addToRecentViewed: jest.fn(), getEntityMissingError: jest.fn(() => ENTITY_MISSING_ERROR), @@ -218,20 +213,6 @@ describe('DataModelPage component', () => { expect(mockRemoveDataModelFollower).toHaveBeenCalled(); }); - it('create thread action checks', async () => { - render(); - await waitForElementToBeRemoved(() => screen.getByText('Loader')); - - // create thread - userEvent.click( - screen.getByRole('button', { - name: CREATE_THREAD, - }) - ); - - expect(mockPostThread).toHaveBeenCalled(); - }); - it('update data model action check', async () => { render(); await waitForElementToBeRemoved(() => screen.getByText('Loader')); @@ -265,7 +246,6 @@ describe('DataModelPage component', () => { }); it('errors check', async () => { - mockPostThread.mockRejectedValueOnce(ERROR); mockPatchDataModelDetails.mockRejectedValue(ERROR); mockAddDataModelFollower.mockRejectedValueOnce(ERROR); mockUpdateDataModelVotes.mockRejectedValueOnce(ERROR); @@ -304,7 +284,7 @@ describe('DataModelPage component', () => { ); }); - expect(mockShowErrorToast).toHaveBeenCalledTimes(9); + expect(mockShowErrorToast).toHaveBeenCalledTimes(8); mockPatchDataModelDetails.mockResolvedValue({}); }); From 3627fb35ed87c6761254d0e36827977ebece7842 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:05:07 +0530 Subject: [PATCH 24/63] update --- .../APIEndpointDetails/APIEndpointDetails.tsx | 5 - .../APIEndpointVersion/APIEndpointVersion.tsx | 1 - .../ContainerVersion.component.tsx | 1 - .../DashboardVersion.component.tsx | 1 - .../DataModelVersion.component.tsx | 1 - .../DataModels/DataModelDetails.component.tsx | 4 - .../CommonWidgets/CommonWidgets.tsx | 2 - .../DataProductsDetailsPage.component.tsx | 1 - .../StoredProcedureVersion.component.tsx | 1 - .../TableVersion/TableVersion.component.tsx | 1 - .../DocumentationTab.component.tsx | 1 - .../EntityRightPanel.test.tsx | 18 -- .../EntityRightPanel/EntityRightPanel.tsx | 15 +- .../GlossaryTermsV1.component.tsx | 1 - .../tabs/GlossaryOverviewTab.component.tsx | 1 - .../Metric/MetricDetails/MetricDetails.tsx | 5 - .../Metric/MetricVersion/MetricVersion.tsx | 1 - .../MlModelDetail/MlModelDetail.component.tsx | 5 - .../MlModelVersion.component.tsx | 1 - ...odalWithCustomPropertyEditor.component.tsx | 5 - .../PipelineVersion.component.tsx | 1 - .../SearchIndexVersion/SearchIndexVersion.tsx | 1 - .../TopicDetails/TopicDetails.component.tsx | 235 ++++++++-------- .../Topic/TopicSchemaTab/TopicSchemaTab.tsx | 6 +- .../TopicVersion/TopicVersion.component.tsx | 1 - .../CustomPropertyTable.interface.ts | 1 - .../CustomPropertyTable.tsx | 3 +- .../ui/src/enums/CustomizeDetailPage.enum.ts | 1 + .../ui/src/hooks/paging/usePaging.ts | 2 +- .../resources/ui/src/mocks/Ingestion.mock.ts | 2 +- .../APICollectionPage/APICollectionPage.tsx | 5 - .../APICollectionVersionPage.tsx | 1 - .../DatabaseSchemaPage.component.tsx | 265 ++++++------------ .../DatabaseSchemaPage/SchemaTablesTab.tsx | 171 ++++++++--- .../DatabaseSchemaVersionPage.tsx | 12 +- .../DatabaseVersionPage.tsx | 1 - .../SearchIndexDetailsPage.tsx | 5 - .../ServiceMainTabContent.tsx | 6 - .../PartitionedKeys.component.tsx | 2 +- .../ui/src/utils/ContainerDetailUtils.tsx | 5 - .../ui/src/utils/DashboardDataModelUtils.tsx | 1 - .../ui/src/utils/DashboardDetailsUtils.tsx | 7 +- .../ui/src/utils/Database/Database.util.tsx | 5 - .../ui/src/utils/DatabaseSchemaClassBase.ts | 35 +-- .../src/utils/DatabaseSchemaDetailsUtils.tsx | 35 +-- .../ui/src/utils/PipelineDetailsUtils.tsx | 5 - .../ui/src/utils/StoredProceduresUtils.tsx | 5 - .../resources/ui/src/utils/TableUtils.tsx | 3 +- .../resources/ui/src/utils/TopicClassBase.ts | 11 +- .../ui/src/utils/TopicDetailsUtils.tsx | 72 +---- 50 files changed, 360 insertions(+), 616 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index feb2ff13eb3d..ba7e8ff2c57d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -306,9 +306,6 @@ const APIEndpointDetails: React.FC = ({ children: (
- customProperties={apiEndpointDetails} - dataProducts={apiEndpointDetails?.dataProducts ?? []} - domain={apiEndpointDetails?.domain} editCustomAttributePermission={ editCustomAttributePermission } @@ -316,7 +313,6 @@ const APIEndpointDetails: React.FC = ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityId={apiEndpointDetails.id} entityType={EntityType.API_ENDPOINT} selectedTags={apiEndpointTags} viewAllPermission={viewAllPermission} @@ -381,7 +377,6 @@ const APIEndpointDetails: React.FC = ({ children: apiEndpointDetails && (
- entityDetails={apiEndpointDetails} entityType={EntityType.API_ENDPOINT} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx index 017e5c1d451d..514c5094df55 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx @@ -167,7 +167,6 @@ const APIEndpointVersion: FC = ({
= ({ children: ( = ({ children: ( = ({
- customProperties={dataModelData} - dataProducts={dataModelData?.dataProducts ?? []} - domain={dataModelData?.domain} editCustomAttributePermission={ (dataModelPermissions.EditAll || dataModelPermissions.EditCustomFields) && @@ -242,7 +239,6 @@ const DataModelDetails = ({ } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={dataModelData.id} entityType={EntityType.DASHBOARD_DATA_MODEL} selectedTags={tags} viewAllPermission={dataModelPermissions.ViewAll} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index d41db7f85d4a..5b13aff6f9a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -57,7 +57,6 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { deleted, owners, domain, - extension, dataProducts, description, entityName, @@ -197,7 +196,6 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { return ( isRenderedInRightPanel - entityDetails={extension} entityType={EntityType.TABLE} handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx index ba5bd3392e29..d23cdec38135 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx @@ -485,7 +485,6 @@ const DataProductsDetailsPage = ({ children: (
- entityDetails={dataProduct} entityType={EntityType.DATA_PRODUCT} handleExtensionUpdate={handelExtensionUpdate} hasEditAccess={ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx index 6c418515d4e0..d29008d79748 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx @@ -158,7 +158,6 @@ const StoredProcedureVersion = ({ children: ( = ({ children: ( isRenderedInRightPanel - entityDetails={domain as DataProduct} entityType={EntityType.DATA_PRODUCT} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx index 7434ce667c1b..70014b5254d8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx @@ -15,7 +15,6 @@ import { EntityTags } from 'Models'; import React from 'react'; import { EntityType } from '../../../enums/entity.enum'; import { Table } from '../../../generated/entity/data/table'; -import { EntityReference } from '../../../generated/entity/type'; import entityRightPanelClassBase from '../../../utils/EntityRightPanelClassBase'; import EntityRightPanel from './EntityRightPanel'; @@ -57,7 +56,6 @@ jest.mock( ); describe('EntityRightPanel component test', () => { - const mockDataProducts: EntityReference[] = []; const mockSelectedTags: EntityTags[] = []; const mockOnTagSelectionChange = jest.fn(); const mockCustomProperties = { @@ -73,9 +71,7 @@ describe('EntityRightPanel component test', () => { editGlossaryTermsPermission editTagPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} onExtensionUpdate={mockExtensionUpdate} @@ -93,9 +89,7 @@ describe('EntityRightPanel component test', () => { editGlossaryTermsPermission editTagPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -115,9 +109,7 @@ describe('EntityRightPanel component test', () => { afterSlot={
afterSlot
} beforeSlot={
beforeSlot
} customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -136,9 +128,7 @@ describe('EntityRightPanel component test', () => { editGlossaryTermsPermission editTagPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -163,9 +153,7 @@ describe('EntityRightPanel component test', () => { editGlossaryTermsPermission editTagPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -188,9 +176,7 @@ describe('EntityRightPanel component test', () => { editGlossaryTermsPermission editTagPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -211,9 +197,7 @@ describe('EntityRightPanel component test', () => { editTagPermission viewAllPermission customProperties={mockCustomProperties} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} @@ -235,9 +219,7 @@ describe('EntityRightPanel component test', () => { editTagPermission viewAllPermission customProperties={{} as Table} - dataProducts={mockDataProducts} editCustomAttributePermission={editPermission} - entityId="testEntityId" entityType={EntityType.TABLE} selectedTags={mockSelectedTags} showDataProductContainer={false} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index bc30eaaf29bf..4d086633814f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -25,21 +25,19 @@ import type { ExtentionEntitiesKeys, } from '../../common/CustomPropertyTable/CustomPropertyTable.interface'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; interface EntityRightPanelProps { - dataProducts: EntityReference[]; editTagPermission: boolean; editGlossaryTermsPermission: boolean; entityType: EntityType; - entityId: string; selectedTags: EntityTags[]; beforeSlot?: React.ReactNode; showTaskHandler?: boolean; showDataProductContainer?: boolean; afterSlot?: React.ReactNode; - domain?: EntityReference; onTagSelectionChange?: (selectedTags: EntityTags[]) => Promise; viewAllPermission?: boolean; customProperties?: ExtentionEntities[T]; @@ -48,8 +46,6 @@ interface EntityRightPanelProps { } const EntityRightPanel = ({ - domain, - dataProducts, entityType, selectedTags, editTagPermission, @@ -57,7 +53,6 @@ const EntityRightPanel = ({ onTagSelectionChange, beforeSlot, afterSlot, - entityId, showTaskHandler = true, showDataProductContainer = true, viewAllPermission, @@ -68,6 +63,13 @@ const EntityRightPanel = ({ const KnowledgeArticles = entityRightPanelClassBase.getKnowLedgeArticlesWidget(); const { fqn: entityFQN } = useFqn(); + const { data } = useGenericContext<{ + domain: EntityReference; + dataProducts: EntityReference[]; + id: string; + }>(); + + const { domain, dataProducts, id: entityId } = data ?? {}; return ( <> @@ -108,7 +110,6 @@ const EntityRightPanel = ({ {customProperties && ( isRenderedInRightPanel - entityDetails={customProperties} entityType={entityType as T} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index e420b67e36ed..6c32d8039227 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -287,7 +287,6 @@ const GlossaryTermsV1 = ({ children: glossaryTerm && (
- entityDetails={glossaryTerm} entityType={EntityType.GLOSSARY_TERM} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index 8cddc42bc197..0e250f2bc4d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -188,7 +188,6 @@ const GlossaryOverviewTab = ({ return ( { await onExtensionUpdate?.(updatedTable); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index e646e8496c71..660c808af4e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -298,9 +298,6 @@ const MetricDetails: React.FC = ({ />
} - customProperties={metricDetails} - dataProducts={metricDetails?.dataProducts ?? []} - domain={metricDetails?.domain} editCustomAttributePermission={ editCustomAttributePermission } @@ -308,7 +305,6 @@ const MetricDetails: React.FC = ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityId={metricDetails.id} entityType={EntityType.METRIC} selectedTags={metricTags} viewAllPermission={viewAllPermission} @@ -387,7 +383,6 @@ const MetricDetails: React.FC = ({ children: metricDetails && (
- entityDetails={metricDetails} entityType={EntityType.METRIC} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx index a7906835c87e..154b2302f48a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx @@ -165,7 +165,6 @@ const MetricVersion: FC = ({
= ({ children: (
- customProperties={mlModelDetail} - dataProducts={mlModelDetail?.dataProducts ?? []} - domain={mlModelDetail?.domain} editCustomAttributePermission={ editCustomAttributePermission } @@ -413,7 +410,6 @@ const MlModelDetail: FC = ({ editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityId={mlModelDetail.id} entityType={EntityType.MLMODEL} selectedTags={mlModelTags} viewAllPermission={viewAllPermission} @@ -489,7 +485,6 @@ const MlModelDetail: FC = ({ children: mlModelDetail && (
- entityDetails={mlModelDetail} entityType={EntityType.MLMODEL} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx index 940a20155b89..64ee78840f2b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx @@ -316,7 +316,6 @@ const MlModelVersion: FC = ({ children: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx index 134f87b8a03f..b1af6806c229 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx @@ -245,7 +245,6 @@ const PipelineVersion: FC = ({ children: ( = ({ children: ( = ({ updateTopicDetailsState, topicDetails, @@ -68,11 +74,12 @@ const TopicDetails: React.FC = ({ onUpdateVote, }: TopicDetailsProps) => { const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedTopicFQN } = useFqn(); const history = useHistory(); + const [customizedPage, setCustomizedPage] = useState(null); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -267,134 +274,114 @@ const TopicDetails: React.FC = ({ [topicPermissions, deleted] ); + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Topic) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + useEffect(() => { getEntityFeedCount(); }, [topicPermissions, decodedTopicFQN]); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.SCHEMA, - children: , - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = topicClassBase.getTopicDetailPageTabs({ + schemaCount: topicDetails.messageSchema?.schemaFields?.length ?? 0, + activityFeedTab: ( + + ), + sampleDataTab: !viewSampleDataPermission ? ( +
+ +
+ ) : ( + + ), + queryViewerTab: ( + + ), + lineageTab: ( + + - ), - }, - { - label: ( - - ), - key: EntityTabs.SAMPLE_DATA, - children: !viewSampleDataPermission ? ( -
- -
- ) : ( - + ), + customPropertiesTab: topicDetails && ( +
+ entityType={EntityType.TOPIC} + handleExtensionUpdate={onExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} /> - ), - }, - { - label: , - key: EntityTabs.CONFIG, - children: ( - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: topicDetails && ( -
- - entityDetails={topicDetails} - entityType={EntityType.TOPIC} - handleExtensionUpdate={onExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ - activeTab, - feedCount.totalCount, - topicTags, - entityName, - topicDetails, - decodedTopicFQN, - fetchTopic, - deleted, - handleFeedCount, - onExtensionUpdate, - handleTagSelection, - onDescriptionUpdate, - onDataProductsUpdate, - handleSchemaFieldsUpdate, - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, - editLineagePermission, - editAllPermission, +
+ ), viewSampleDataPermission, - viewAllPermission, - ] - ); + activeTab, + feedCount, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.SCHEMA + ); + }, [ + activeTab, + feedCount.totalCount, + topicTags, + entityName, + topicDetails, + decodedTopicFQN, + fetchTopic, + deleted, + handleFeedCount, + onExtensionUpdate, + handleTagSelection, + onDescriptionUpdate, + onDataProductsUpdate, + handleSchemaFieldsUpdate, + editTagsPermission, + editGlossaryTermsPermission, + editDescriptionPermission, + editCustomAttributePermission, + editLineagePermission, + editAllPermission, + viewSampleDataPermission, + viewAllPermission, + ]); return ( { const { currentPersonaDocStore } = useCustomizeStore(); const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); @@ -33,7 +35,7 @@ export const TopicSchemaTab = () => { } const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.Table + (p: Page) => p.pageType === PageType.Topic ); if (page) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx index f08e8acbb846..489091d3e6c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx @@ -180,7 +180,6 @@ const TopicVersion: FC = ({ children: ( { isVersionView?: boolean; entityType: T; - entityDetails: ExtentionEntities[T]; handleExtensionUpdate?: (updatedTable: ExtentionEntities[T]) => Promise; hasEditAccess: boolean; className?: string; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx index 9b2869dcfd10..c960b1a498ee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx @@ -43,6 +43,7 @@ import { getUpdatedExtensionDiffFields, } from '../../../utils/EntityVersionUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder'; import './custom-property-table.less'; import { @@ -60,12 +61,12 @@ export const CustomPropertyTable = ({ className, isVersionView, hasPermission, - entityDetails, maxDataCap, isRenderedInRightPanel = false, }: CustomPropertyProps) => { const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); + const { data: entityDetails } = useGenericContext(); const [entityTypeDetail, setEntityTypeDetail] = useState({} as Type); const [entityTypeDetailLoading, setEntityTypeDetailLoading] = diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 3f11a6ebc2a6..3390a64a9484 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -43,6 +43,7 @@ export enum DetailPageWidgetKeys { OWNERS = 'KnowledgePanel.Owners', EXPERTS = 'KnowledgePanel.Experts', DOMAIN_TYPE = 'KnowledgePanel.DomainType', + TABLES = 'KnowledgePanel.Tables', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index 669ba42d7965..0049521ab9a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -30,7 +30,7 @@ import useCustomLocation from '../useCustomLocation/useCustomLocation'; interface CursorState { cursorType: CursorType | null; - cursorValue: string | null; + cursorValue?: string; } interface PagingHistoryStateData { diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Ingestion.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Ingestion.mock.ts index 2e42ef984870..20e40136d6a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/Ingestion.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Ingestion.mock.ts @@ -215,7 +215,7 @@ const mockPaging = { const mockPagingCursor = { cursorData: { cursorType: null, - cursorValue: null, + cursorValue: undefined, }, currentPage: 1, pageSize: 10, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index bcae15bfb766..eba2d0f5db66 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -546,15 +546,11 @@ const APICollectionPage: FunctionComponent = () => { children: (
- customProperties={apiCollection} - dataProducts={apiCollection?.dataProducts ?? []} - domain={apiCollection?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={apiCollection?.id ?? ''} entityType={EntityType.API_COLLECTION} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -607,7 +603,6 @@ const APICollectionPage: FunctionComponent = () => {
className="" - entityDetails={apiCollection} entityType={EntityType.API_COLLECTION} handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx index f56e226c406b..8d6bbff49200 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx @@ -334,7 +334,6 @@ const APICollectionVersionPage = () => {
{ const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const pagingInfo = usePaging(PAGE_SIZE); - const { - paging, - pageSize, - handlePagingChange, - currentPage, - handlePageChange, - pagingCursor, - } = pagingInfo; - - const { filters: tableFilters, setFilters } = useTableFilters( - INITIAL_TABLE_FILTERS - ); + const { setFilters } = useTableFilters(INITIAL_TABLE_FILTERS); const { tab: activeTab = EntityTabs.TABLE } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); @@ -108,19 +100,17 @@ const DatabaseSchemaPage: FunctionComponent = () => { const [databaseSchema, setDatabaseSchema] = useState( {} as DatabaseSchema ); - const [tableData, setTableData] = useState>([]); - const [tableDataLoading, setTableDataLoading] = useState(true); const [isSchemaDetailsLoading, setIsSchemaDetailsLoading] = useState(true); - const [isEdit, setIsEdit] = useState(false); - const [description, setDescription] = useState(''); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - + const [customizedPage, setCustomizedPage] = useState(null); + const { selectedPersona } = useApplicationStore(); const [databaseSchemaPermission, setDatabaseSchemaPermission] = useState(DEFAULT_ENTITY_PERMISSION); const [storedProcedureCount, setStoredProcedureCount] = useState(0); + const [tableCount, setTableCount] = useState(0); const [updateProfilerSetting, setUpdateProfilerSetting] = useState(false); @@ -135,12 +125,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchemaPermission, decodedDatabaseSchemaFQN] ); - const handleShowDeletedTables = (value: boolean) => { - setFilters({ showDeletedTables: value }); - handlePageChange(INITIAL_PAGING_VALUE); - }; - - const { version: currentVersion, deleted } = useMemo( + const { version: currentVersion } = useMemo( () => databaseSchema, [databaseSchema] ); @@ -202,9 +187,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { include: Include.All, } ); - const { description: schemaDescription = '' } = response; setDatabaseSchema(response); - setDescription(schemaDescription); if (response.deleted) { setFilters({ showDeletedTables: response.deleted, @@ -220,37 +203,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }, [decodedDatabaseSchemaFQN]); - const getSchemaTables = useCallback( - async (params?: TableListParams) => { - setTableDataLoading(true); - try { - const res = await getTableList({ - ...params, - databaseSchema: decodedDatabaseSchemaFQN, - limit: pageSize, - include: tableFilters.showDeletedTables - ? Include.Deleted - : Include.NonDeleted, - }); - setTableData(res.data); - handlePagingChange(res.paging); - } catch (err) { - showErrorToast(err as AxiosError); - } finally { - setTableDataLoading(false); - } - }, - [decodedDatabaseSchemaFQN, tableFilters.showDeletedTables, pageSize] - ); - - const onDescriptionEdit = useCallback((): void => { - setIsEdit(true); - }, []); - - const onEditCancel = useCallback(() => { - setIsEdit(false); - }, []); - const saveUpdatedDatabaseSchemaData = useCallback( (updatedData: DatabaseSchema) => { let jsonPatch: Operation[] = []; @@ -263,36 +215,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchemaId, databaseSchema] ); - const onDescriptionUpdate = useCallback( - async (updatedHTML: string) => { - if (description !== updatedHTML && databaseSchema) { - const updatedDatabaseSchemaDetails = { - ...databaseSchema, - description: updatedHTML, - }; - - try { - const response = await saveUpdatedDatabaseSchemaData( - updatedDatabaseSchemaDetails - ); - if (response) { - setDatabaseSchema(response); - setDescription(updatedHTML); - } else { - throw t('server.unexpected-response'); - } - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); - } - } else { - setIsEdit(false); - } - }, - [description, databaseSchema] - ); - const activeTabHandler = useCallback( (activeKey: string) => { if (activeKey !== activeTab) { @@ -430,24 +352,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }, [databaseSchemaId]); - const tablePaginationHandler = useCallback( - ({ cursorType, currentPage }: PagingHandlerParams) => { - if (cursorType) { - getSchemaTables({ [cursorType]: paging[cursorType] }); - handlePageChange( - currentPage, - { - cursorType: cursorType, - cursorValue: paging[cursorType]!, - }, - pageSize - ); - } - handlePageChange(currentPage); - }, - [paging, getSchemaTables, handlePageChange] - ); - const versionHandler = useCallback(() => { currentVersion && history.push( @@ -488,6 +392,18 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }, [decodedDatabaseSchemaFQN]); + const fetchTableCount = useCallback(async () => { + try { + const { paging } = await getTableList({ + databaseSchema: decodedDatabaseSchemaFQN, + limit: 0, + }); + setTableCount(paging.total); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, [decodedDatabaseSchemaFQN]); + useEffect(() => { fetchDatabaseSchemaPermission(); }, [decodedDatabaseSchemaFQN]); @@ -496,35 +412,14 @@ const DatabaseSchemaPage: FunctionComponent = () => { if (viewDatabaseSchemaPermission) { fetchDatabaseSchemaDetails(); fetchStoreProcedureCount(); + fetchTableCount(); getEntityFeedCount(); } }, [viewDatabaseSchemaPermission]); - useEffect(() => { - if (viewDatabaseSchemaPermission && decodedDatabaseSchemaFQN) { - if (pagingCursor?.cursorData?.cursorType) { - // Fetch data if cursorType is present in state with cursor Value to handle browser back navigation - getSchemaTables({ - [pagingCursor?.cursorData?.cursorType]: - pagingCursor?.cursorData?.cursorValue, - }); - } else { - // Otherwise, just fetch the data without cursor value - getSchemaTables({ limit: pageSize }); - } - } - }, [ - tableFilters.showDeletedTables, - decodedDatabaseSchemaFQN, - viewDatabaseSchemaPermission, - deleted, - pageSize, - ]); - const { editTagsPermission, editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, viewAllPermission, } = useMemo( @@ -537,10 +432,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { (databaseSchemaPermission.EditGlossaryTerms || databaseSchemaPermission.EditAll) && !databaseSchema.deleted, - editDescriptionPermission: - (databaseSchemaPermission.EditDescription || - databaseSchemaPermission.EditAll) && - !databaseSchema.deleted, editCustomAttributePermission: (databaseSchemaPermission.EditAll || databaseSchemaPermission.EditCustomFields) && @@ -581,69 +472,51 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }; - const tabs: TabsProps['items'] = useMemo( - () => - databaseSchemaClassBase.getDatabaseSchemaPageTabs({ - feedCount, - tableData, - activeTab, - currentTablesPage: currentPage, - databaseSchema, - description, - editDescriptionPermission, - isEdit, - showDeletedTables: tableFilters.showDeletedTables, - tableDataLoading, - editCustomAttributePermission, - editTagsPermission, - editGlossaryTermsPermission, - tags, - viewAllPermission, - databaseSchemaPermission, - storedProcedureCount, - onEditCancel, - handleExtensionUpdate, - handleTagSelection, - tablePaginationHandler, - onDescriptionEdit, - onDescriptionUpdate, - handleShowDeletedTables, - getEntityFeedCount, - fetchDatabaseSchemaDetails, - handleFeedCount, - pagingInfo, - }), - [ + const tabs: TabsProps['items'] = useMemo(() => { + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = databaseSchemaClassBase.getDatabaseSchemaPageTabs({ feedCount, - tableData, activeTab, - currentPage, - databaseSchema, - description, - editDescriptionPermission, - isEdit, - tableFilters.showDeletedTables, - tableDataLoading, editCustomAttributePermission, editTagsPermission, editGlossaryTermsPermission, tags, viewAllPermission, - storedProcedureCount, databaseSchemaPermission, + storedProcedureCount, handleExtensionUpdate, handleTagSelection, - tablePaginationHandler, - onEditCancel, - onDescriptionEdit, - onDescriptionUpdate, - handleShowDeletedTables, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, - pagingInfo, - ] - ); + tableCount, + labelMap: tabLabelMap, + }); + + return getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.TABLE + ); + }, [ + feedCount, + activeTab, + databaseSchema, + editCustomAttributePermission, + editTagsPermission, + editGlossaryTermsPermission, + tags, + tableCount, + viewAllPermission, + storedProcedureCount, + databaseSchemaPermission, + handleExtensionUpdate, + handleTagSelection, + getEntityFeedCount, + fetchDatabaseSchemaDetails, + handleFeedCount, + ]); const updateVote = async (data: QueryVote, id: string) => { try { @@ -666,6 +539,26 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find( + (p: Page) => p.pageType === PageType.DatabaseSchema + ) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + if (isPermissionsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index a388c4fb3e13..b96c94c6785c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -22,54 +22,65 @@ import DisplayName from '../../components/common/DisplayName/DisplayName'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; -import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface'; +import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; import RichTextEditorPreviewerV1 from '../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; import TableAntd from '../../components/common/Table/Table'; +import { useGenericContext } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; +import { + INITIAL_PAGING_VALUE, + INITIAL_TABLE_FILTERS, + PAGE_SIZE, +} from '../../constants/constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType } from '../../enums/entity.enum'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { Table } from '../../generated/entity/data/table'; -import { UsePagingInterface } from '../../hooks/paging/usePaging'; -import { patchTableDetails } from '../../rest/tableAPI'; +import { Include } from '../../generated/type/include'; +import { usePaging } from '../../hooks/paging/usePaging'; +import { useFqn } from '../../hooks/useFqn'; +import { useTableFilters } from '../../hooks/useTableFilters'; +import { + getTableList, + patchTableDetails, + TableListParams, +} from '../../rest/tableAPI'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; import { showErrorToast } from '../../utils/ToastUtils'; interface SchemaTablesTabProps { - databaseSchemaDetails: DatabaseSchema; - tableDataLoading: boolean; - description: string; - editDescriptionPermission?: boolean; - showDeletedTables?: boolean; - tableData: Table[]; - currentTablesPage: number; - tablePaginationHandler: NextPreviousProps['pagingHandler']; - onDescriptionUpdate?: (updatedHTML: string) => Promise; - onShowDeletedTablesChange?: (value: boolean) => void; isVersionView?: boolean; - pagingInfo: UsePagingInterface; } function SchemaTablesTab({ - databaseSchemaDetails, - tableDataLoading, - description, - editDescriptionPermission = false, - tableData, - currentTablesPage, - tablePaginationHandler, - onDescriptionUpdate, - showDeletedTables = false, - onShowDeletedTablesChange, isVersionView = false, - pagingInfo, }: Readonly) { const { t } = useTranslation(); - const [localTableData, setLocalTableData] = useState([]); - + const [tableData, setTableData] = useState>([]); + const [tableDataLoading, setTableDataLoading] = useState(true); const { permissions } = usePermissionProvider(); + const { fqn: decodedDatabaseSchemaFQN } = useFqn(); + const pagingInfo = usePaging(PAGE_SIZE); + const { + data: databaseSchemaDetails, + permissions: databaseSchemaPermission, + onUpdate, + } = useGenericContext(); + + const { filters: tableFilters, setFilters } = useTableFilters( + INITIAL_TABLE_FILTERS + ); + + const { + paging, + pageSize, + handlePagingChange, + currentPage, + handlePageChange, + pagingCursor, + } = pagingInfo; const allowEditDisplayNamePermission = useMemo(() => { return ( @@ -78,10 +89,21 @@ function SchemaTablesTab({ ); }, [permissions, isVersionView]); + const { viewDatabaseSchemaPermission, editDescriptionPermission } = useMemo( + () => ({ + viewDatabaseSchemaPermission: + databaseSchemaPermission.ViewAll || databaseSchemaPermission.ViewBasic, + editDescriptionPermission: + databaseSchemaPermission.EditAll || + databaseSchemaPermission.EditDescription, + }), + [databaseSchemaPermission?.ViewAll, databaseSchemaPermission?.ViewBasic] + ); + const handleDisplayNameUpdate = useCallback( async (data: EntityName, id?: string) => { try { - const tableDetails = localTableData.find((table) => table.id === id); + const tableDetails = tableData.find((table) => table.id === id); if (!tableDetails) { return; } @@ -92,19 +114,61 @@ function SchemaTablesTab({ const jsonPatch = compare(tableDetails, updatedData); const response = await patchTableDetails(tableDetails.id, jsonPatch); - setLocalTableData((prevData) => + setTableData((prevData) => prevData.map((table) => (table.id === id ? response : table)) ); } catch (error) { showErrorToast(error as AxiosError); } }, - [localTableData] + [tableData] ); - useEffect(() => { - setLocalTableData(tableData); - }, [tableData]); + const handleShowDeletedTables = (value: boolean) => { + setFilters({ showDeletedTables: value }); + handlePageChange(INITIAL_PAGING_VALUE); + }; + + const getSchemaTables = useCallback( + async (params?: TableListParams) => { + setTableDataLoading(true); + try { + const res = await getTableList({ + ...params, + databaseSchema: decodedDatabaseSchemaFQN, + limit: pageSize, + include: tableFilters.showDeletedTables + ? Include.Deleted + : Include.NonDeleted, + }); + setTableData(res.data); + handlePagingChange(res.paging); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setTableDataLoading(false); + } + }, + [decodedDatabaseSchemaFQN, tableFilters.showDeletedTables, pageSize] + ); + + const tablePaginationHandler = useCallback( + ({ cursorType, currentPage }: PagingHandlerParams) => { + if (cursorType && paging[cursorType]) { + getSchemaTables({ [cursorType]: paging[cursorType] }); + handlePageChange( + currentPage, + { + cursorType: cursorType, + cursorValue: paging[cursorType], + }, + pageSize + ); + } + handlePageChange(currentPage); + }, + [paging, getSchemaTables, handlePageChange] + ); const tableColumn: ColumnsType
= useMemo( () => [ @@ -145,25 +209,52 @@ function SchemaTablesTab({ [handleDisplayNameUpdate, allowEditDisplayNamePermission] ); + useEffect(() => { + if (viewDatabaseSchemaPermission && decodedDatabaseSchemaFQN) { + if (pagingCursor?.cursorData?.cursorType) { + // Fetch data if cursorType is present in state with cursor Value to handle browser back navigation + getSchemaTables({ + [pagingCursor?.cursorData?.cursorType]: + pagingCursor?.cursorData?.cursorValue, + }); + } else { + // Otherwise, just fetch the data without cursor value + getSchemaTables({ limit: pageSize }); + } + } + }, [ + tableFilters.showDeletedTables, + decodedDatabaseSchemaFQN, + viewDatabaseSchemaPermission, + + pageSize, + ]); + + useEffect(() => { + setFilters({ showDeletedTables: databaseSchemaDetails.deleted ?? false }); + }, [databaseSchemaDetails.deleted]); + return ( {isVersionView ? ( ) : ( + onUpdate({ ...databaseSchemaDetails, description: value }) + } /> )} @@ -172,9 +263,9 @@ function SchemaTablesTab({ {t('label.deleted')} @@ -189,7 +280,7 @@ function SchemaTablesTab({ bordered columns={tableColumn} data-testid="databaseSchema-tables" - dataSource={localTableData} + dataSource={tableData} loading={tableDataLoading} locale={{ emptyText: ( @@ -207,7 +298,7 @@ function SchemaTablesTab({ {!isUndefined(pagingInfo) && pagingInfo.showPagination && ( - + - customProperties={searchIndexDetails} - dataProducts={searchIndexDetails?.dataProducts ?? []} - domain={searchIndexDetails?.domain} editCustomAttributePermission={ editCustomAttributePermission } @@ -390,7 +387,6 @@ function SearchIndexDetailsPage() { editGlossaryTermsPermission } editTagPermission={editTagsPermission} - entityId={searchIndexDetails?.id ?? ''} entityType={EntityType.SEARCH_INDEX} selectedTags={searchIndexTags} viewAllPermission={viewAllPermission} @@ -491,7 +487,6 @@ function SearchIndexDetailsPage() { children: searchIndexDetails && (
- entityDetails={searchIndexDetails} entityType={EntityType.SEARCH_INDEX} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx index e964e1129d85..e5575de2391c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx @@ -32,7 +32,6 @@ import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.co import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../../enums/entity.enum'; -import { DatabaseService } from '../../generated/entity/services/databaseService'; import { Paging } from '../../generated/type/paging'; import { UsePagingInterface } from '../../hooks/paging/usePaging'; import { ServicesType } from '../../interface/service.interface'; @@ -295,13 +294,8 @@ function ServiceMainTabContent({ children: (
{ return data; }, []); - if (!data.tablePartition) { + if (!data?.tablePartition) { return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index 9a91b23d7cf7..df38ed1e700e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -197,9 +197,6 @@ export const getContainerDetailPageTabs = ({ children: (
- customProperties={containerData} - dataProducts={containerData?.dataProducts ?? []} - domain={containerData?.domain} editCustomAttributePermission={ editCustomAttributePermission } @@ -207,7 +204,6 @@ export const getContainerDetailPageTabs = ({ editTagPermission={ editTagsPermission && !containerData?.deleted } - entityId={containerData?.id ?? ''} entityType={EntityType.CONTAINER} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -294,7 +290,6 @@ export const getContainerDetailPageTabs = ({ children: containerData && (
- entityDetails={containerData} entityType={EntityType.CONTAINER} handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index b108ee5cee85..e78388fe49fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -130,7 +130,6 @@ export const getDashboardDataModelDetailPageTabs = ({ children: (
- entityDetails={dataModelData} entityType={EntityType.DASHBOARD_DATA_MODEL} handleExtensionUpdate={handelExtensionUpdate} hasEditAccess={ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 448224e57580..4cf46af25b78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -175,15 +175,11 @@ export const getDashboardDetailPageTabs = ({ children: (
- customProperties={dashboardDetails} - dataProducts={dashboardDetails?.dataProducts ?? []} - domain={dashboardDetails?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={dashboardDetails.id} entityType={EntityType.DASHBOARD} selectedTags={dashboardTags} viewAllPermission={viewAllPermission} @@ -244,10 +240,9 @@ export const getDashboardDetailPageTabs = ({ /> ), key: EntityTabs.CUSTOM_PROPERTIES, - children: dashboardDetails && ( + children: (
- entityDetails={dashboardDetails} entityType={EntityType.DASHBOARD} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 43d80adfd8fc..70fe89ebfdc8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -261,15 +261,11 @@ export const getDatabasePageBaseTabs = ({ children: (
- customProperties={database} - dataProducts={database?.dataProducts ?? []} - domain={database?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={database?.id ?? ''} entityType={EntityType.DATABASE} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -320,7 +316,6 @@ export const getDatabasePageBaseTabs = ({ children: database && (
- entityDetails={database} entityType={EntityType.DATABASE} handleExtensionUpdate={settingsUpdateHandler} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 2c6dff54c586..40085354e070 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -11,7 +11,6 @@ * limitations under the License. */ import { EntityTags } from 'Models'; -import { PagingHandlerParams } from '../components/common/NextPrevious/NextPrevious.interface'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -29,10 +28,8 @@ import { DatabaseServiceType, State, } from '../generated/entity/data/databaseSchema'; -import { Table } from '../generated/entity/data/table'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../generated/type/tagLabel'; -import { UsePagingInterface } from '../hooks/paging/usePaging'; import { FeedCounts } from '../interface/feed.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import { getDataBaseSchemaPageBaseTabs } from './DatabaseSchemaDetailsUtils'; @@ -40,36 +37,21 @@ import i18n from './i18next/LocalUtil'; export interface DatabaseSchemaPageTabProps { feedCount: FeedCounts; - tableData: Table[]; activeTab: EntityTabs; - currentTablesPage: number; - databaseSchema: DatabaseSchema; - description: string; - editDescriptionPermission: boolean; - isEdit: boolean; - showDeletedTables: boolean; - tableDataLoading: boolean; editCustomAttributePermission: boolean; editTagsPermission: boolean; editGlossaryTermsPermission: boolean; tags: EntityTags[]; viewAllPermission: boolean; - storedProcedureCount: number; databaseSchemaPermission: OperationPermission; + storedProcedureCount: number; handleExtensionUpdate: (schema: DatabaseSchema) => Promise; handleTagSelection: (selectedTags: EntityTags[]) => Promise; - tablePaginationHandler: ({ - cursorType, - currentPage, - }: PagingHandlerParams) => void; - onEditCancel: () => void; - onDescriptionEdit: () => void; - onDescriptionUpdate: (updatedHTML: string) => Promise; - handleShowDeletedTables: (value: boolean) => void; getEntityFeedCount: () => void; fetchDatabaseSchemaDetails: () => Promise; handleFeedCount: (data: FeedCounts) => void; - pagingInfo: UsePagingInterface; + tableCount: number; + labelMap: Record; } class DatabaseSchemaClassBase { @@ -81,7 +63,8 @@ class DatabaseSchemaClassBase { public getDatabaseSchemaPageTabsIds(): Tab[] { return [ - EntityTabs.SCHEMA, + EntityTabs.TABLE, + EntityTabs.STORED_PROCEDURE, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, ].map((tab: EntityTabs) => ({ @@ -89,13 +72,13 @@ class DatabaseSchemaClassBase { name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [EntityTabs.SCHEMA].includes(tab), + editable: [EntityTabs.TABLE].includes(tab), })); } public getDefaultLayout(tab: EntityTabs) { switch (tab) { - case EntityTabs.SCHEMA: + case EntityTabs.TABLE: return [ { h: 2, @@ -106,8 +89,8 @@ class DatabaseSchemaClassBase { static: false, }, { - h: 11, - i: DetailPageWidgetKeys.DATABASE_SCHEMA, + h: 8, + i: DetailPageWidgetKeys.TABLES, w: 6, x: 0, y: 0, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index dd17b0d815c2..6da675d4642b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -32,14 +32,7 @@ export const defaultFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS export const getDataBaseSchemaPageBaseTabs = ({ feedCount, - tableData, activeTab, - currentTablesPage, - databaseSchema, - description, - editDescriptionPermission, - showDeletedTables, - tableDataLoading, editGlossaryTermsPermission, editCustomAttributePermission, editTagsPermission, @@ -48,19 +41,16 @@ export const getDataBaseSchemaPageBaseTabs = ({ storedProcedureCount, handleExtensionUpdate, handleTagSelection, - tablePaginationHandler, - onDescriptionUpdate, - handleShowDeletedTables, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, - pagingInfo, + tableCount, }: DatabaseSchemaPageTabProps): TabProps[] => { return [ { label: ( - +
), ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, @@ -96,15 +74,11 @@ export const getDataBaseSchemaPageBaseTabs = ({ children: (
- customProperties={databaseSchema} - dataProducts={databaseSchema?.dataProducts ?? []} - domain={databaseSchema?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={databaseSchema?.id ?? ''} entityType={EntityType.DATABASE_SCHEMA} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -165,11 +139,10 @@ export const getDataBaseSchemaPageBaseTabs = ({ /> ), key: EntityTabs.CUSTOM_PROPERTIES, - children: databaseSchema && ( + children: (
className="" - entityDetails={databaseSchema} entityType={EntityType.DATABASE_SCHEMA} handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index e82d65a71dca..29589c5b1f57 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -144,15 +144,11 @@ export const getPipelineDetailPageTabs = ({ children: (
- customProperties={pipelineDetails} - dataProducts={pipelineDetails?.dataProducts ?? []} - domain={pipelineDetails?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={pipelineDetails.id} entityType={EntityType.PIPELINE} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -231,7 +227,6 @@ export const getPipelineDetailPageTabs = ({ children: pipelineDetails && (
- entityDetails={pipelineDetails} entityType={EntityType.PIPELINE} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 91d0d9a4190a..6a87b0d618a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -171,15 +171,11 @@ export const getStoredProcedureDetailsPageTabs = ({ children: (
- customProperties={storedProcedure} - dataProducts={storedProcedure?.dataProducts ?? []} - domain={storedProcedure?.domain} editCustomAttributePermission={ editCustomAttributePermission } editGlossaryTermsPermission={editGlossaryTermsPermission} editTagPermission={editTagsPermission} - entityId={storedProcedure?.id ?? ''} entityType={EntityType.STORED_PROCEDURE} selectedTags={tags} viewAllPermission={viewAllPermission} @@ -243,7 +239,6 @@ export const getStoredProcedureDetailsPageTabs = ({ key: EntityTabs.CUSTOM_PROPERTIES, children: storedProcedure && ( - entityDetails={storedProcedure} entityType={EntityType.STORED_PROCEDURE} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 5cbf47ff8674..2291d12c28c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -936,10 +936,9 @@ export const getTableDetailPageBaseTabs = ({ /> ), key: EntityTabs.CUSTOM_PROPERTIES, - children: tableDetails && ( + children: (
- entityDetails={tableDetails} entityType={EntityType.TABLE} handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts index 4c05eb4a88a3..456b1d628be9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -29,7 +29,6 @@ import { getTopicDetailsPageTabs } from './TopicDetailsUtils'; export interface TopicDetailPageTabProps { schemaCount: number; - schemaTab: JSX.Element; activityFeedTab: JSX.Element; sampleDataTab: JSX.Element; queryViewerTab: JSX.Element; @@ -50,7 +49,7 @@ class TopicClassBase { this.tabs = []; } - public getTableDetailPageTabs( + public getTopicDetailPageTabs( tableDetailsPageProps: TopicDetailPageTabProps ): TabProps[] { return getTopicDetailsPageTabs(tableDetailsPageProps); @@ -69,11 +68,7 @@ class TopicClassBase { name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [ - EntityTabs.SCHEMA, - EntityTabs.OVERVIEW, - EntityTabs.TERMS, - ].includes(tab), + editable: tab === EntityTabs.SCHEMA, })); } @@ -90,7 +85,7 @@ class TopicClassBase { static: false, }, { - h: 11, + h: 8, i: DetailPageWidgetKeys.TOPIC_SCHEMA, w: 6, x: 0, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx index bad8f65576ac..ff320378ea63 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx @@ -14,82 +14,14 @@ import React from 'react'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { TopicSchemaTab } from '../components/Topic/TopicSchemaTab/TopicSchemaTab'; import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum'; -import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import i18n from './i18next/LocalUtil'; import { TopicDetailPageTabProps } from './TopicClassBase'; -export const getTopicDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - export const getTopicDetailsPageTabs = ({ schemaCount, - schemaTab, activityFeedTab, sampleDataTab, queryViewerTab, @@ -110,7 +42,7 @@ export const getTopicDetailsPageTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: schemaTab, + children: , }, { label: ( From 0cefc8e0a7d5f92420a8d0525d50045c428e0504 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:17:58 +0530 Subject: [PATCH 25/63] fix detail pages --- .../CommonWidgets/CommonWidgets.tsx | 16 ++- .../DatabaseSchemaTable.interface.ts | 1 - .../DatabaseSchemaTable.tsx | 7 +- .../DatabaseSchemaTableTab.tsx | 87 ++++++++++++++ .../DatabaseSchemaTab/DatabaseSchemaTab.tsx | 87 ++++++++++++++ .../StoredProcedureCodeCard.tsx | 46 ++++++++ .../StoredProcedureGenericTab.tsx | 89 ++++++++++++++ .../TableGenericTab/TableGenericTab.tsx | 41 +++++-- .../DatabaseDetailsPage.tsx | 109 ++---------------- .../DatabaseSchemaPage.component.tsx | 65 +---------- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 38 +----- .../StoredProcedure/StoredProcedurePage.tsx | 108 ++++++++--------- .../ui/src/utils/Database/Database.util.tsx | 72 +----------- .../src/utils/Database/DatabaseClassBase.ts | 20 +--- .../ui/src/utils/DatabaseSchemaClassBase.ts | 5 - .../src/utils/DatabaseSchemaDetailsUtils.tsx | 49 +------- .../ui/src/utils/StoredProcedureBase.ts | 12 -- .../ui/src/utils/StoredProceduresUtils.tsx | 79 +------------ .../resources/ui/src/utils/TableClassBase.ts | 18 ++- 19 files changed, 434 insertions(+), 515 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index 5b13aff6f9a8..cd23b479af3c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -134,7 +134,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { onSelectionChange={handleTagSelection} /> ); - }, [type]); + }, [editTagsPermission, tags, type]); const glossaryWidget = useMemo(() => { return ( @@ -148,7 +148,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { onSelectionChange={handleTagSelection} /> ); - }, []); + }, [editGlossaryTermsPermission, tags, type]); const descriptionWidget = useMemo(() => { return ( @@ -169,7 +169,15 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { }} /> ); - }, []); + }, [ + description, + entityName, + type, + editDescriptionPermission, + deleted, + columns, + owners, + ]); const handleExtensionUpdate = async (updatedTable: Table) => { await onUpdate(updatedTable as unknown as GenericEntity); @@ -212,7 +220,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { handleRemoveWidget: noop, isEditView: false, }); - }, [widgetConfig]); + }, [widgetConfig, descriptionWidget, glossaryWidget, tagsWidget]); return (
) => { const { fqn: decodedDatabaseFQN } = useFqn(); const history = useHistory(); const location = useCustomLocation(); const { permissions } = usePermissionProvider(); - const [schemas, setSchemas] = useState([]); const [isLoading, setIsLoading] = useState(true); const [showDeletedSchemas, setShowDeletedSchemas] = useState(false); + const { data } = useGenericContext(); + + const { deleted: isDatabaseDeleted } = data ?? {}; const allowEditDisplayNamePermission = useMemo(() => { return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx new file mode 100644 index 000000000000..e92e16c4e6a3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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 React, { useMemo } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../../enums/entity.enum'; +import { Page, PageType, Tab } from '../../../../generated/system/ui/page'; +import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; +import SchemaTablesTab from '../../../../pages/DatabaseSchemaPage/SchemaTablesTab'; +import { getDefaultWidgetForTab } from '../../../../utils/CustomizePage/CustomizePageUtils'; +import databaseSchemaClassBase from '../../../../utils/DatabaseSchemaClassBase'; +import { CommonWidgets } from '../../../DataAssets/CommonWidgets/CommonWidgets'; + +const ReactGridLayout = WidthProvider(RGL); + +export const DatabaseSchemaTableTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.TABLE } = useParams<{ tab: EntityTabs }>(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return databaseSchemaClassBase.getDefaultLayout(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.DatabaseSchema + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return getDefaultWidgetForTab(PageType.DatabaseSchema, tab); + } + }, [currentPersonaDocStore, tab]); + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLES)) { + return ; + } else { + return ; + } + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx new file mode 100644 index 000000000000..428e5438c41d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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 React, { useMemo } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; +import { Page, PageType, Tab } from '../../../generated/system/ui/page'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; +import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; +import databaseClassBase from '../../../utils/Database/DatabaseClassBase'; +import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; +import { DatabaseSchemaTable } from '../DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; + +const ReactGridLayout = WidthProvider(RGL); + +export const DatabaseSchemaTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return databaseClassBase.getDefaultLayout(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.Database + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return getDefaultWidgetForTab(PageType.Database, tab); + } + }, [currentPersonaDocStore, tab]); + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DATABASE_SCHEMA)) { + return ; + } else { + return ; + } + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx new file mode 100644 index 000000000000..a4ea7f484377 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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 { Card } from 'antd'; +import React, { useMemo } from 'react'; +import { CSMode } from '../../../enums/codemirror.enum'; +import { + StoredProcedure, + StoredProcedureCodeObject, +} from '../../../generated/entity/data/storedProcedure'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import SchemaEditor from '../SchemaEditor/SchemaEditor'; + +export const StoredProcedureCodeCard = () => { + const { data } = useGenericContext(); + + const { code } = useMemo(() => { + return { + code: + (data?.storedProcedureCode as StoredProcedureCodeObject)?.code ?? '', + }; + }, [data]); + + return ( + + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx new file mode 100644 index 000000000000..b51d8a279fe9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2024 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 React, { useMemo } from 'react'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { useParams } from 'react-router-dom'; +import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../../enums/entity.enum'; +import { Page, PageType, Tab } from '../../../generated/system/ui/page'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; +import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; +import storedProcedureClassBase from '../../../utils/StoredProcedureBase'; +import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; +import { StoredProcedureCodeCard } from '../StoredProcedureCodeCard/StoredProcedureCodeCard'; + +const ReactGridLayout = WidthProvider(RGL); + +export const StoredProcedureGenericTab = () => { + const { currentPersonaDocStore } = useCustomizeStore(); + const { tab = EntityTabs.CODE } = useParams<{ tab: EntityTabs }>(); + + const layout = useMemo(() => { + if (!currentPersonaDocStore) { + return storedProcedureClassBase.getDefaultLayout(tab); + } + + const page = currentPersonaDocStore?.data?.pages?.find( + (p: Page) => p.pageType === PageType.StoredProcedure + ); + + if (page) { + return page.tabs.find((t: Tab) => t.id === tab)?.layout; + } else { + return getDefaultWidgetForTab(PageType.StoredProcedure, tab); + } + }, [currentPersonaDocStore, tab]); + + const widgets = useMemo(() => { + const getWidgetFromKeyInternal = ( + widgetConfig: WidgetConfig + ): JSX.Element | null => { + if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) + ) { + return ; + } else { + return ; + } + }; + + return layout.map((widget: WidgetConfig) => ( +
+ {getWidgetFromKeyInternal(widget)} +
+ )); + }, [layout]); + + // call the hook to set the direction of the grid layout + useGridLayoutDirection(); + + return ( + <> + + {widgets} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx index ce725f5f3c2c..9e295728c06b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx @@ -10,11 +10,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { isEmpty } from 'lodash'; import React, { useMemo } from 'react'; import RGL, { WidthProvider } from 'react-grid-layout'; import { useParams } from 'react-router-dom'; import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../../enums/entity.enum'; +import { Table } from '../../../generated/entity/data/table'; import { Page, PageType, Tab } from '../../../generated/system/ui/page'; import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; @@ -24,8 +26,11 @@ import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKe import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; import tableClassBase from '../../../utils/TableClassBase'; +import { getJoinsFromTableJoins } from '../../../utils/TableUtils'; import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; +import { useGenericContext } from '../../GenericProvider/GenericProvider'; import SchemaTable from '../SchemaTable/SchemaTable.component'; +import { StoredProcedureCodeCard } from '../StoredProcedureCodeCard/StoredProcedureCodeCard'; const ReactGridLayout = WidthProvider(RGL); @@ -33,6 +38,12 @@ export const TableGenericTab = () => { const { currentPersonaDocStore } = useCustomizeStore(); const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + const { data: tableDetails } = useGenericContext
(); + const joins = useMemo( + () => getJoinsFromTableJoins(tableDetails?.joins), + [tableDetails?.joins] + ); + const layout = useMemo(() => { if (!currentPersonaDocStore) { return tableClassBase.getDefaultLayout(tab); @@ -62,26 +73,34 @@ export const TableGenericTab = () => { } else if ( widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) ) { - return ; + return isEmpty(joins) ? null : ; } else if ( widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS) ) { return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) + ) { + return ; } else { return ; } }; - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout]); + return layout.map((widget: WidgetConfig) => { + const renderedWidget = getWidgetFromKeyInternal(widget); + + return renderedWidget ? ( +
+ {renderedWidget} +
+ ) : null; + }); + }, [layout, joins]); // call the hook to set the direction of the grid layout useGridLayoutDirection(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index d234896ec13a..4579f2fdd6f2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -15,7 +15,6 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; import { compare, Operation } from 'fast-json-patch'; import { isEmpty, isUndefined, toString } from 'lodash'; -import { EntityTags } from 'Models'; import React, { FunctionComponent, useCallback, @@ -82,8 +81,8 @@ import { getTabLabelMap, } from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { getTierTags } from '../../utils/TableUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const DatabaseDetails: FunctionComponent = () => { @@ -103,13 +102,8 @@ const DatabaseDetails: FunctionComponent = () => { const [database, setDatabase] = useState({} as Database); const [serviceType, setServiceType] = useState(); - const [databaseName, setDatabaseName] = useState( - decodedDatabaseFQN.split(FQN_SEPARATOR_CHAR).slice(-1).pop() ?? '' - ); const [isDatabaseDetailsLoading, setIsDatabaseDetailsLoading] = useState(true); - const [description, setDescription] = useState(''); - const [databaseId, setDatabaseId] = useState(''); const [schemaInstanceCount, setSchemaInstanceCount] = useState(0); @@ -129,7 +123,6 @@ const DatabaseDetails: FunctionComponent = () => { ); const tier = getTierTags(database?.tags ?? []); - const tags = getTagsWithoutTier(database?.tags ?? []); const [databasePermission, setDatabasePermission] = useState(DEFAULT_ENTITY_PERMISSION); @@ -194,11 +187,8 @@ const DatabaseDetails: FunctionComponent = () => { }) .then((res) => { if (res) { - const { description, id, name, serviceType } = res; + const { serviceType } = res; setDatabase(res); - setDescription(description ?? ''); - setDatabaseId(id ?? ''); - setDatabaseName(name); setServiceType(serviceType); } }) @@ -222,27 +212,7 @@ const DatabaseDetails: FunctionComponent = () => { jsonPatch = compare(database, updatedData); } - return patchDatabaseDetails(databaseId, jsonPatch); - }; - - const onDescriptionUpdate = async (updatedHTML: string) => { - if (description !== updatedHTML && database) { - const updatedDatabaseDetails = { - ...database, - description: updatedHTML, - }; - try { - const response = await saveUpdatedDatabaseData(updatedDatabaseDetails); - if (response) { - setDatabase(response); - setDescription(updatedHTML); - } else { - throw t('server.unexpected-response'); - } - } catch (error) { - showErrorToast(error as AxiosError); - } - } + return patchDatabaseDetails(database.id ?? '', jsonPatch); }; const activeTabHandler = (key: string) => { @@ -295,7 +265,7 @@ const DatabaseDetails: FunctionComponent = () => { search: withinPageSearch, isPersistFilters: false, extraParameters: { - quickFilter: getQueryFilterForDatabase(serviceType, databaseName), + quickFilter: getQueryFilterForDatabase(serviceType, database.name), }, }) ); @@ -346,37 +316,6 @@ const DatabaseDetails: FunctionComponent = () => { return settingsUpdateHandler(updatedTableDetails); }; - /** - * Formulates updated tags and updates table entity data for API call - * @param selectedTags - */ - const onTagUpdate = async (selectedTags?: Array) => { - if (selectedTags) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedDatabase = { ...database, tags: updatedTags }; - await settingsUpdateHandler(updatedDatabase); - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - if (selectedTags) { - const prevTags = - tags?.filter((tag) => - selectedTags - .map((selTag) => selTag.tagFQN) - .includes(tag?.tagFQN as string) - ) || []; - const newTags = createTagObject( - selectedTags.filter((tag) => { - return !prevTags - ?.map((prevTag) => prevTag.tagFQN) - .includes(tag.tagFQN); - }) - ); - await onTagUpdate([...prevTags, ...newTags]); - } - }; - const handleToggleDelete = (version?: number) => { setDatabase((prev) => { if (!prev) { @@ -393,7 +332,7 @@ const DatabaseDetails: FunctionComponent = () => { const handleRestoreDatabase = useCallback(async () => { try { - const { version: newVersion } = await restoreDatabase(databaseId); + const { version: newVersion } = await restoreDatabase(database.id ?? ''); showSuccessToast( t('message.restore-entities-success', { entity: t('label.database'), @@ -409,7 +348,7 @@ const DatabaseDetails: FunctionComponent = () => { }) ); } - }, [databaseId]); + }, [database.id]); const versionHandler = useCallback(() => { currentVersion && @@ -423,23 +362,8 @@ const DatabaseDetails: FunctionComponent = () => { ); }, [currentVersion, decodedDatabaseFQN]); - const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, - viewAllPermission, - } = useMemo( + const { editCustomAttributePermission, viewAllPermission } = useMemo( () => ({ - editTagsPermission: - (databasePermission.EditTags || databasePermission.EditAll) && - !database.deleted, - editGlossaryTermsPermission: - (databasePermission.EditGlossaryTerms || databasePermission.EditAll) && - !database.deleted, - editDescriptionPermission: - (databasePermission.EditDescription || databasePermission.EditAll) && - !database.deleted, editCustomAttributePermission: (databasePermission.EditAll || databasePermission.EditCustomFields) && !database.deleted, @@ -469,18 +393,11 @@ const DatabaseDetails: FunctionComponent = () => { const tabs = databaseClassBase.getDatabaseDetailPageTabs({ activeTab, database, - description, - editDescriptionPermission, - editGlossaryTermsPermission, - editTagsPermission, viewAllPermission, - tags, schemaInstanceCount, feedCount, handleFeedCount, getEntityFeedCount, - onDescriptionUpdate, - handleTagSelection, settingsUpdateHandler, deleted: database.deleted ?? false, editCustomAttributePermission, @@ -494,18 +411,10 @@ const DatabaseDetails: FunctionComponent = () => { EntityTabs.CHILDREN ); }, [ - tags, - database, - description, - databaseName, - decodedDatabaseFQN, activeTab, - databasePermission, + database, schemaInstanceCount, feedCount.totalCount, - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, viewAllPermission, deleted, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index b6e058ab8bfa..bdb532e2c472 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -15,7 +15,6 @@ import { Col, Row, Skeleton, Tabs, TabsProps } from 'antd'; import { AxiosError } from 'axios'; import { compare, Operation } from 'fast-json-patch'; import { isEmpty, isUndefined } from 'lodash'; -import { EntityTags } from 'Models'; import React, { FunctionComponent, useCallback, @@ -59,7 +58,6 @@ import { Tag } from '../../generated/entity/classification/tag'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { Page, PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; -import { TagLabel } from '../../generated/type/tagLabel'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useTableFilters } from '../../hooks/useTableFilters'; @@ -82,8 +80,7 @@ import { getTabLabelMap, } from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const DatabaseSchemaPage: FunctionComponent = () => { @@ -125,24 +122,11 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchemaPermission, decodedDatabaseSchemaFQN] ); - const { version: currentVersion } = useMemo( + const { version: currentVersion, id: databaseSchemaId = '' } = useMemo( () => databaseSchema, [databaseSchema] ); - const { tags, tier } = useMemo( - () => ({ - tier: getTierTags(databaseSchema.tags ?? []), - tags: getTagsWithoutTier(databaseSchema.tags ?? []), - }), - [databaseSchema] - ); - - const databaseSchemaId = useMemo( - () => databaseSchema?.id ?? '', - [databaseSchema] - ); - const fetchDatabaseSchemaPermission = useCallback(async () => { setIsPermissionsLoading(true); try { @@ -255,27 +239,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { [databaseSchema, databaseSchema?.owners] ); - const handleTagsUpdate = async (selectedTags?: Array) => { - if (selectedTags) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedData = { ...databaseSchema, tags: updatedTags }; - - try { - const res = await saveUpdatedDatabaseSchemaData( - updatedData as DatabaseSchema - ); - setDatabaseSchema(res); - } catch (error) { - showErrorToast(error as AxiosError, t('server.api-error')); - } - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleTagsUpdate(updatedTags); - }; - const handleUpdateTier = useCallback( async (newTier?: Tag) => { const tierTag = updateTierTag(databaseSchema?.tags ?? [], newTier); @@ -417,27 +380,15 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }, [viewDatabaseSchemaPermission]); - const { - editTagsPermission, - editGlossaryTermsPermission, - editCustomAttributePermission, - viewAllPermission, - } = useMemo( + const { editCustomAttributePermission, viewAllPermission } = useMemo( () => ({ - editTagsPermission: - (databaseSchemaPermission.EditTags || - databaseSchemaPermission.EditAll) && - !databaseSchema.deleted, - editGlossaryTermsPermission: - (databaseSchemaPermission.EditGlossaryTerms || - databaseSchemaPermission.EditAll) && - !databaseSchema.deleted, editCustomAttributePermission: (databaseSchemaPermission.EditAll || databaseSchemaPermission.EditCustomFields) && !databaseSchema.deleted, viewAllPermission: databaseSchemaPermission.ViewAll, }), + [databaseSchemaPermission, databaseSchema] ); @@ -479,14 +430,10 @@ const DatabaseSchemaPage: FunctionComponent = () => { feedCount, activeTab, editCustomAttributePermission, - editTagsPermission, - editGlossaryTermsPermission, - tags, viewAllPermission, databaseSchemaPermission, storedProcedureCount, handleExtensionUpdate, - handleTagSelection, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, @@ -504,15 +451,11 @@ const DatabaseSchemaPage: FunctionComponent = () => { activeTab, databaseSchema, editCustomAttributePermission, - editTagsPermission, - editGlossaryTermsPermission, - tags, tableCount, viewAllPermission, storedProcedureCount, databaseSchemaPermission, handleExtensionUpdate, - handleTagSelection, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index b96c94c6785c..816cafaf2fb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -15,11 +15,10 @@ import { Col, Row, Switch, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; -import { isEmpty, isUndefined } from 'lodash'; +import { isUndefined } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import DisplayName from '../../components/common/DisplayName/DisplayName'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; @@ -47,7 +46,6 @@ import { TableListParams, } from '../../rest/tableAPI'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; -import { getEntityName } from '../../utils/EntityUtils'; import { showErrorToast } from '../../utils/ToastUtils'; interface SchemaTablesTabProps { @@ -63,11 +61,8 @@ function SchemaTablesTab({ const { permissions } = usePermissionProvider(); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); const pagingInfo = usePaging(PAGE_SIZE); - const { - data: databaseSchemaDetails, - permissions: databaseSchemaPermission, - onUpdate, - } = useGenericContext(); + const { data: databaseSchemaDetails, permissions: databaseSchemaPermission } = + useGenericContext(); const { filters: tableFilters, setFilters } = useTableFilters( INITIAL_TABLE_FILTERS @@ -89,13 +84,10 @@ function SchemaTablesTab({ ); }, [permissions, isVersionView]); - const { viewDatabaseSchemaPermission, editDescriptionPermission } = useMemo( + const { viewDatabaseSchemaPermission } = useMemo( () => ({ viewDatabaseSchemaPermission: databaseSchemaPermission.ViewAll || databaseSchemaPermission.ViewBasic, - editDescriptionPermission: - databaseSchemaPermission.EditAll || - databaseSchemaPermission.EditDescription, }), [databaseSchemaPermission?.ViewAll, databaseSchemaPermission?.ViewBasic] ); @@ -236,28 +228,6 @@ function SchemaTablesTab({ return ( -
- {isVersionView ? ( - - ) : ( - - onUpdate({ ...databaseSchemaDetails, description: value }) - } - /> - )} - {!isVersionView && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index 011b56f987bc..f8eedd7de1a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -13,7 +13,6 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; -import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; @@ -25,6 +24,7 @@ import { QueryVote } from '../../components/Database/TableQueries/TableQueries.i import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; +import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -44,12 +44,13 @@ import { StoredProcedure, StoredProcedureCodeObject, } from '../../generated/entity/data/storedProcedure'; +import { Page, PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; -import { TagLabel } from '../../generated/type/tagLabel'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { addStoredProceduresFollower, getStoredProceduresByFqn, @@ -60,18 +61,22 @@ import { } from '../../rest/storedProceduresAPI'; import { addToRecentViewed, getFeedCounts } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; +import { + getGlossaryTermDetailTabs, + getTabLabelMap, +} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getStoredProcedureDetailsPageTabs, STORED_PROCEDURE_DEFAULT_FIELDS, } from '../../utils/StoredProceduresUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const StoredProcedurePage = () => { const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const USER_ID = currentUser?.id ?? ''; const history = useHistory(); const { tab: activeTab = EntityTabs.CODE } = useParams<{ tab: string }>(); @@ -82,8 +87,7 @@ const StoredProcedurePage = () => { const [storedProcedure, setStoredProcedure] = useState(); const [storedProcedurePermissions, setStoredProcedurePermissions] = useState(DEFAULT_ENTITY_PERMISSION); - const [isEdit, setIsEdit] = useState(false); - + const [customizedPage, setCustomizedPage] = useState(null); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -93,10 +97,8 @@ const StoredProcedurePage = () => { followers, owners, tags, - tier, version, code, - description, deleted, entityName, entityFQN, @@ -381,41 +383,6 @@ const StoredProcedurePage = () => { } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const onCancel = () => { - setIsEdit(false); - }; - - const onDescriptionUpdate = async (updatedHTML: string) => { - if (description !== updatedHTML && storedProcedure) { - const updatedData = { - ...storedProcedure, - description: updatedHTML, - }; - try { - await handleStoreProcedureUpdate(updatedData, 'description'); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); - } - } else { - setIsEdit(false); - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - - if (updatedTags && storedProcedure) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedData = { ...storedProcedure, tags: updatedTags }; - await handleStoreProcedureUpdate(updatedData, 'tags'); - } - }; - const onExtensionUpdate = useCallback( async (updatedData: StoredProcedure) => { if (storedProcedure) { @@ -439,9 +406,6 @@ const StoredProcedurePage = () => { ); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, editLineagePermission, viewAllPermission, @@ -477,51 +441,49 @@ const StoredProcedurePage = () => { ); const tabs = useMemo(() => { - return getStoredProcedureDetailsPageTabs({ + const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + + const tabs = getStoredProcedureDetailsPageTabs({ activeTab: activeTab as EntityTabs, feedCount, - description: description ?? '', decodedStoredProcedureFQN, entityName, code, - isEdit, deleted: deleted ?? false, owners: owners ?? [], - editDescriptionPermission, - onCancel, - onDescriptionEdit, - onDescriptionUpdate, storedProcedure: storedProcedure as StoredProcedure, - tags: tags ?? [], - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, onExtensionUpdate, - handleTagSelection, getEntityFeedCount: getEntityFeedCount, fetchStoredProcedureDetails, handleFeedCount: handleFeedCount, + labelMap: tabLabelMap, }); + + const updatedTabs = getGlossaryTermDetailTabs( + tabs, + customizedPage?.tabs, + EntityTabs.CODE + ); + + return updatedTabs; }, [ code, - tags, - isEdit, deleted, feedCount.totalCount, activeTab, entityFQN, entityName, - description, storedProcedure, decodedStoredProcedureFQN, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, - editDescriptionPermission, editCustomAttributePermission, viewAllPermission, + onExtensionUpdate, + getEntityFeedCount, + fetchStoredProcedureDetails, handleFeedCount, ]); @@ -540,6 +502,26 @@ const StoredProcedurePage = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find( + (p: Page) => p.pageType === PageType.StoredProcedure + ) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + useEffect(() => { if (decodedStoredProcedureFQN) { fetchResourcePermission(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 70fe89ebfdc8..197c3e069b80 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -10,27 +10,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Col, Row, Typography } from 'antd'; +import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { t } from 'i18next'; -import { isEmpty, isUndefined, toLower } from 'lodash'; +import { isUndefined, toLower } from 'lodash'; import React from 'react'; import { Link } from 'react-router-dom'; import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import RichTextEditorPreviewerV1 from '../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; -import { DatabaseSchemaTable } from '../../components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; +import { DatabaseSchemaTab } from '../../components/Database/DatabaseSchemaTab/DatabaseSchemaTab'; import { getEntityDetailsPath, NO_DATA_PLACEHOLDER, } from '../../constants/constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs, @@ -201,20 +197,12 @@ export const getDatabaseDetailsPageDefaultLayout = (tab: EntityTabs) => { export const getDatabasePageBaseTabs = ({ activeTab, database, - description, - editDescriptionPermission, - editGlossaryTermsPermission, - editTagsPermission, viewAllPermission, - tags, schemaInstanceCount, feedCount, handleFeedCount, getEntityFeedCount, - onDescriptionUpdate, - handleTagSelection, settingsUpdateHandler, - deleted, editCustomAttributePermission, getDetailsByFQN, }: DatabaseDetailPageTabProps): TabProps[] => { @@ -229,59 +217,7 @@ export const getDatabasePageBaseTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: ( - - - - - - - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.DATABASE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={settingsUpdateHandler} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index 5ecc738d4d21..3eeda0254611 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityTags } from 'Models'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -37,18 +36,11 @@ import { getDatabasePageBaseTabs } from './Database.util'; export interface DatabaseDetailPageTabProps { activeTab: EntityTabs; database: Database; - description: string; - editDescriptionPermission: boolean; - editGlossaryTermsPermission: boolean; - editTagsPermission: boolean; viewAllPermission: boolean; - tags: EntityTags[]; schemaInstanceCount: number; feedCount: FeedCounts; handleFeedCount: (data: FeedCounts) => void; getEntityFeedCount: () => void; - onDescriptionUpdate: (updatedHTML: string) => Promise; - handleTagSelection: (selectedTags: EntityTags[]) => Promise; settingsUpdateHandler: ( data: Database, key?: keyof Database @@ -70,23 +62,13 @@ class DatabaseClassBase { return [ EntityTabs.SCHEMA, EntityTabs.ACTIVITY_FEED, - EntityTabs.SAMPLE_DATA, - EntityTabs.TABLE_QUERIES, - EntityTabs.PROFILER, - EntityTabs.INCIDENTS, - EntityTabs.LINEAGE, - EntityTabs.VIEW_DEFINITION, EntityTabs.CUSTOM_PROPERTIES, ].map((tab: EntityTabs) => ({ id: tab, name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [ - EntityTabs.SCHEMA, - EntityTabs.OVERVIEW, - EntityTabs.TERMS, - ].includes(tab), + editable: tab === EntityTabs.SCHEMA, })); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 40085354e070..d18a94a0ad8f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityTags } from 'Models'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -39,14 +38,10 @@ export interface DatabaseSchemaPageTabProps { feedCount: FeedCounts; activeTab: EntityTabs; editCustomAttributePermission: boolean; - editTagsPermission: boolean; - editGlossaryTermsPermission: boolean; - tags: EntityTags[]; viewAllPermission: boolean; databaseSchemaPermission: OperationPermission; storedProcedureCount: number; handleExtensionUpdate: (schema: DatabaseSchema) => Promise; - handleTagSelection: (selectedTags: EntityTags[]) => Promise; getEntityFeedCount: () => void; fetchDatabaseSchemaDetails: () => Promise; handleFeedCount: (data: FeedCounts) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index 6da675d4642b..f4d57f22def6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -11,19 +11,15 @@ * limitations under the License. */ -import { Col, Row } from 'antd'; import { t } from 'i18next'; import React from 'react'; import ActivityFeedProvider from '../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; -import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; -import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; +import { DatabaseSchemaTableTab } from '../components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; -import SchemaTablesTab from '../pages/DatabaseSchemaPage/SchemaTablesTab'; import StoredProcedureTab from '../pages/StoredProcedure/StoredProcedureTab'; import { DatabaseSchemaPageTabProps } from './DatabaseSchemaClassBase'; @@ -33,14 +29,10 @@ export const defaultFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS export const getDataBaseSchemaPageBaseTabs = ({ feedCount, activeTab, - editGlossaryTermsPermission, editCustomAttributePermission, - editTagsPermission, - tags, viewAllPermission, storedProcedureCount, handleExtensionUpdate, - handleTagSelection, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, @@ -57,44 +49,7 @@ export const getDataBaseSchemaPageBaseTabs = ({ /> ), key: EntityTabs.TABLE, - children: ( - -
- - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.DATABASE_SCHEMA} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={handleExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index e6cb74d5301c..6871ba75da4f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityTags } from 'Models'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -22,7 +21,6 @@ import { } from '../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { Tag } from '../generated/entity/classification/tag'; import { StoredProcedure } from '../generated/entity/data/storedProcedure'; import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; @@ -36,27 +34,17 @@ export interface StoredProcedureDetailPageTabProps { feedCount: { totalCount: number; }; - description: string; decodedStoredProcedureFQN: string; entityName: string; code: string; - isEdit: boolean; deleted: boolean; owners: EntityReference[]; - editDescriptionPermission: boolean; - onCancel: () => void; - onDescriptionEdit: () => void; - onDescriptionUpdate: (value: string) => Promise; storedProcedure: StoredProcedure; - tags: Tag[]; - editTagsPermission: boolean; - editGlossaryTermsPermission: boolean; editLineagePermission: boolean; editCustomAttributePermission: boolean; viewAllPermission: boolean; labelMap?: Record; onExtensionUpdate: (value: StoredProcedure) => Promise; - handleTagSelection: (selectedTags: EntityTags[]) => Promise; getEntityFeedCount: () => void; fetchStoredProcedureDetails: () => Promise; handleFeedCount: (data: FeedCounts) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 6a87b0d618a4..45630f7b1705 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -10,22 +10,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Card, Col, Row } from 'antd'; import { t } from 'i18next'; -import { isEmpty } from 'lodash'; import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; -import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; -import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; +import { TableGenericTab } from '../components/Database/TableGenericTab/TableGenericTab'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; -import { CSMode } from '../enums/codemirror.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { StoredProcedureDetailPageTabProps } from './StoredProcedureBase'; @@ -103,22 +96,12 @@ export const getStoredProceduresPageDefaultLayout = (tab: EntityTabs) => { export const getStoredProcedureDetailsPageTabs = ({ activeTab, feedCount, - description, - entityName, - code, deleted, - owners, - editDescriptionPermission, - onDescriptionUpdate, storedProcedure, - tags, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, onExtensionUpdate, - handleTagSelection, getEntityFeedCount, fetchStoredProcedureDetails, handleFeedCount, @@ -133,65 +116,7 @@ export const getStoredProcedureDetailsPageTabs = ({ /> ), key: EntityTabs.CODE, - children: ( - -
- - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.STORED_PROCEDURE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 01b03f239c3e..30a75cf359cc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -84,11 +84,7 @@ class TableClassBase { name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [ - EntityTabs.SCHEMA, - EntityTabs.OVERVIEW, - EntityTabs.TERMS, - ].includes(tab), + editable: tab === EntityTabs.SCHEMA, })); } @@ -97,7 +93,7 @@ class TableClassBase { case EntityTabs.SCHEMA: return [ { - h: 2, + h: 1, i: DetailPageWidgetKeys.DESCRIPTION, w: 6, x: 0, @@ -113,7 +109,7 @@ class TableClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, w: 2, x: 6, @@ -129,7 +125,7 @@ class TableClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.TAGS, w: 2, x: 6, @@ -137,7 +133,7 @@ class TableClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.GLOSSARY_TERMS, w: 2, x: 6, @@ -145,7 +141,7 @@ class TableClassBase { static: false, }, { - h: 3, + h: 1, i: DetailPageWidgetKeys.TABLE_CONSTRAINTS, w: 2, x: 6, @@ -153,7 +149,7 @@ class TableClassBase { static: false, }, { - h: 4, + h: 1, i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, w: 2, x: 6, From e5b5560899c33dc72feaac71909973fd11562b87 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:35:47 +0530 Subject: [PATCH 26/63] fix playwright tests --- .../ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index cd23b479af3c..f0956d7abcb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -225,6 +225,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { return (
From c2a915d9f3e4c2bdb7c50f23b9af1da822f01f9a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:57:34 +0530 Subject: [PATCH 27/63] fix unit tests --- .../EntityRightPanel.test.tsx | 15 ++++ .../DatabaseSchemaVersionPage.test.tsx | 79 ++++--------------- 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx index 70014b5254d8..a1c8def048bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx @@ -55,6 +55,21 @@ jest.mock( }) ); +jest.mock('../../../components/GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockImplementation(() => ({ + data: { + tableDetails: { + joins: [], + }, + extension: { + test1: 'test', + test2: '', + }, + }, + onThreadLinkSelect: jest.fn(), + })), +})); + describe('EntityRightPanel component test', () => { const mockSelectedTags: EntityTags[] = []; const mockOnTagSelectionChange = jest.fn(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx index 9a87f13c5ad6..4e3e433df6d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx @@ -11,13 +11,10 @@ * limitations under the License. */ import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; import DatabaseSchemaVersionPage from './DatabaseSchemaVersionPage'; import { - CURRENT_TABLE_PAGE, CUSTOM_PROPERTY_TABLE, - CUSTOM_PROPERTY_TAB_NAME, DATABASE_SCHEMA_ID, DATA_ASSET_VERSION_HEADER, DATA_PRODUCT_CONTAINER, @@ -99,24 +96,24 @@ jest.mock('../../hooks/useFqn', () => ({ })); jest.mock('../../pages/DatabaseSchemaPage/SchemaTablesTab', () => - jest - .fn() - .mockImplementation(({ tablePaginationHandler, currentTablesPage }) => ( - <> -

- {currentTablesPage ? `currentTablesPage is ${currentTablesPage}` : ''} -

- - - )) + jest.fn().mockImplementation(() => ( + <> + + + )) ); +jest.mock('../../components/GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockImplementation(() => ({ + data: { + tableDetails: { + joins: [], + }, + }, + onThreadLinkSelect: jest.fn(), + })), +})); + const mockGetDatabaseSchemaDetailsByFQN = jest .fn() .mockResolvedValue({ id: DATABASE_SCHEMA_ID }); @@ -175,50 +172,6 @@ describe('DatabaseSchemaVersionPage', () => { expect(screen.getAllByText(TAGS_CONTAINER_V2)).toHaveLength(2); }); - it('actions on table tab', async () => { - await act(async () => { - render(); - }); - - // tablePaginationHandler - act(() => { - userEvent.click(screen.getByText(SCHEMA_TABLE_TAB)); - }); - - expect( - screen.getByText(`currentTablesPage is ${CURRENT_TABLE_PAGE}`) - ).toBeInTheDocument(); - }); - - it('tab change, version handler, back handler should work', async () => { - await act(async () => { - render(); - }); - - // for tab change - userEvent.click( - screen.getByRole('tab', { - name: CUSTOM_PROPERTY_TAB_NAME, - }) - ); - - // for back handler - userEvent.click( - screen.getByRole('button', { - name: DATA_ASSET_VERSION_HEADER, - }) - ); - - // for version handler - userEvent.click( - screen.getByRole('button', { - name: ENTITY_VERSION_TIMELINE, - }) - ); - - expect(mockPush).toHaveBeenCalledTimes(3); - }); - it('should show ErrorPlaceHolder if not have view permission', async () => { mockGetEntityPermissionByFqn.mockResolvedValueOnce({}); From 51d71048eaa099cdfaff5184cde89598efceb7a8 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:20:51 +0530 Subject: [PATCH 28/63] fix unit tests --- .../StoredProcedure/StoredProcedurePage.test.tsx | 13 +++++++++++-- .../ui/src/utils/StoredProceduresUtils.tsx | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.test.tsx index c6a685a040f3..8e72e9bb4c87 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.test.tsx @@ -133,7 +133,14 @@ jest.mock('../../hoc/LimitWrapper', () => { return jest.fn().mockImplementation(({ children }) =>

{children}

); }); -jest.useFakeTimers(); +jest.mock( + '../../components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab', + () => ({ + StoredProcedureGenericTab: jest + .fn() + .mockImplementation(() =>

testStoredProcedureGenericTab

), + }) +); describe('StoredProcedure component', () => { it('StoredProcedurePage should fetch permissions', () => { @@ -244,6 +251,8 @@ describe('StoredProcedure component', () => { include: 'all', }); - expect(await screen.findByText('testSchemaEditor')).toBeInTheDocument(); + expect( + await screen.findByText('testStoredProcedureGenericTab') + ).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 45630f7b1705..fb7b3651b27f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; -import { TableGenericTab } from '../components/Database/TableGenericTab/TableGenericTab'; +import { StoredProcedureGenericTab } from '../components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import LineageProvider from '../context/LineageProvider/LineageProvider'; @@ -116,7 +116,7 @@ export const getStoredProcedureDetailsPageTabs = ({ /> ), key: EntityTabs.CODE, - children: , + children: , }, { label: ( From 783e1a8871e7bb609a7c06cd0e26663a7aee78d4 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:40:31 +0530 Subject: [PATCH 29/63] update generic tab --- .../APIEndpointDetails/APIEndpointDetails.tsx | 2 +- .../ContainerChildren/ContainerChildren.tsx | 2 +- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 30 ++-- .../GenericProvider/GenericProvider.tsx | 18 +-- .../GenericTab/GenericTab.tsx} | 58 ++++---- .../GenericWidget}/GenericWidget.tsx | 74 +++++----- .../DashboardChartTable.tsx | 2 +- .../DashboardDetails.component.tsx | 99 ++----------- .../DataModels/DataModelDetails.component.tsx | 2 +- .../ModelTab/ModelTab.component.tsx | 2 +- .../CommonWidgets/CommonWidgets.tsx | 27 +++- .../DomainLabelV2/DomainLabelV2.tsx | 2 +- .../DataAssets/OwnerLabelV2/OwnerLabelV2.tsx | 2 +- .../ReviewerLabelV2/ReviewerLabelV2.tsx | 19 +-- .../DataProductsDetailsPage.component.tsx | 34 ++--- .../DatabaseSchemaTable.tsx | 2 +- .../DatabaseSchemaTableTab.tsx | 87 ----------- .../DatabaseSchemaTab/DatabaseSchemaTab.tsx | 87 ----------- .../SchemaTable/SchemaTable.component.tsx | 2 +- .../StoredProcedureCodeCard.tsx | 2 +- .../TableDescription.component.tsx | 2 +- .../TableGenericTab/TableGenericTab.tsx | 121 ---------------- .../TableTags/TableTags.component.tsx | 2 +- .../DomainDetailsPage.component.tsx | 28 ++-- .../DocumentationTab.component.tsx | 12 +- .../DocumentationTab.interface.ts | 7 - .../EntityRightPanel/EntityRightPanel.tsx | 2 +- .../GlossaryDetails.component.tsx | 2 +- .../GlossaryHeader.component.tsx | 2 +- .../GlossaryTermsV1.component.tsx | 2 +- .../tabs/GlossaryOverviewTab.component.tsx | 2 +- .../tabs/GlossaryTermReferences.tsx | 2 +- .../tabs/GlossaryTermSynonyms.tsx | 2 +- .../GlossaryTerms/tabs/RelatedTerms.tsx | 2 +- .../Metric/MetricDetails/MetricDetails.tsx | 2 +- .../MlModelDetail/MlModelDetail.component.tsx | 2 +- .../CustomiseGlossaryTermDetailPage.tsx | 2 +- .../PipelineDetails.component.tsx | 2 +- .../Tag/TagsContainerV2/TagsContainerV2.tsx | 2 +- .../TopicDetails/TopicDetails.component.tsx | 2 +- .../Topic/TopicSchema/TopicSchema.tsx | 2 +- .../CustomPropertyTable.tsx | 2 +- .../EntityDescription/DescriptionV1.tsx | 2 +- .../APICollectionPage/APICollectionPage.tsx | 34 +++-- .../src/pages/ContainerPage/ContainerPage.tsx | 2 +- .../CustomizableDomainPage.tsx | 2 +- .../CustomizeTableDetailPage.tsx | 2 +- .../DatabaseDetailsPage.tsx | 2 +- .../DatabaseSchemaPage.component.tsx | 2 +- .../DatabaseSchemaPage/SchemaTablesTab.tsx | 2 +- .../SearchIndexDetailsPage.tsx | 2 +- .../StoredProcedure/StoredProcedurePage.tsx | 2 +- .../FrequentlyJoinedTables.component.tsx | 2 +- .../PartitionedKeys.component.tsx | 2 +- .../TableConstraints/TableConstraints.tsx | 2 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 2 +- .../ui/src/utils/ContainerDetailUtils.tsx | 6 + .../ui/src/utils/ContainerDetailsClassBase.ts | 6 + .../CustomizeDetailPage.ts | 2 +- .../CustomizeGlossaryPage.ts | 4 +- .../CustomizeGlossaryTermBaseClass.ts | 4 +- .../utils/CustomizePage/CustomizePageUtils.ts | 41 ++++++ .../ui/src/utils/DashboardDataModelBase.ts | 12 +- .../ui/src/utils/DashboardDataModelUtils.tsx | 8 ++ .../ui/src/utils/DashboardDetailsClassBase.ts | 31 ++-- .../ui/src/utils/DashboardDetailsUtils.tsx | 135 ++---------------- .../ui/src/utils/Database/Database.util.tsx | 83 ++--------- .../src/utils/Database/DatabaseClassBase.ts | 10 +- .../ui/src/utils/DatabaseSchemaClassBase.ts | 18 ++- .../src/utils/DatabaseSchemaDetailsUtils.tsx | 17 ++- .../ui/src/utils/Domain/DomainClassBase.ts | 8 +- .../resources/ui/src/utils/DomainUtils.tsx | 32 +++-- .../ui/src/utils/PipelineClassBase.ts | 10 +- .../ui/src/utils/PipelineDetailsUtils.tsx | 6 + .../src/utils/SearchIndexDetailsClassBase.ts | 7 +- .../ui/src/utils/SearchIndexUtils.tsx | 7 + .../ui/src/utils/StoredProcedureBase.ts | 12 +- .../ui/src/utils/StoredProceduresUtils.tsx | 85 +++-------- .../resources/ui/src/utils/TableClassBase.ts | 10 +- .../resources/ui/src/utils/TableUtils.tsx | 41 +++++- .../resources/ui/src/utils/TopicClassBase.ts | 10 +- .../ui/src/utils/TopicDetailsUtils.tsx | 17 ++- 82 files changed, 547 insertions(+), 892 deletions(-) rename openmetadata-ui/src/main/resources/ui/src/components/{Glossary/CustomiseWidgets => Customization}/CustomizeTabWidget/CustomizeTabWidget.tsx (88%) rename openmetadata-ui/src/main/resources/ui/src/components/{ => Customization}/GenericProvider/GenericProvider.tsx (83%) rename openmetadata-ui/src/main/resources/ui/src/components/{Topic/TopicSchemaTab/TopicSchemaTab.tsx => Customization/GenericTab/GenericTab.tsx} (60%) rename openmetadata-ui/src/main/resources/ui/src/components/{Glossary/CustomiseWidgets/SynonymsWidget => Customization/GenericWidget}/GenericWidget.tsx (88%) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index ba7e8ff2c57d..8fd26aafd0b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -45,9 +45,9 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx index f780250ee39d..9af8f78b8d40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx @@ -23,7 +23,7 @@ import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import Table from '../../common/Table/Table'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; interface ContainerChildrenProps { isLoading?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx similarity index 88% rename from openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx index 850f26a5074a..196ab0373bd6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -16,35 +16,35 @@ import { isEmpty, isNil, toString, uniqueId } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import RGL, { Layout, WidthProvider } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; -import { CommonWidgetType } from '../../../../constants/CustomizeWidgets.constants'; -import { EntityTabs } from '../../../../enums/entity.enum'; -import { Document } from '../../../../generated/entity/docStore/document'; -import { Page, Tab } from '../../../../generated/system/ui/page'; -import { PageType } from '../../../../generated/system/ui/uiCustomization'; -import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection'; +import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; +import { CommonWidgetType } from '../../../constants/CustomizeWidgets.constants'; +import { EntityTabs } from '../../../enums/entity.enum'; +import { Document } from '../../../generated/entity/docStore/document'; +import { Page, Tab } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/uiCustomization'; +import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { WidgetCommonProps, WidgetConfig, -} from '../../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; +} from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; import { getAddWidgetHandler, getLayoutUpdateHandler, getLayoutWithEmptyWidgetPlaceholder, getRemoveWidgetHandler, getUniqueFilteredLayout, -} from '../../../../utils/CustomizableLandingPageUtils'; +} from '../../../utils/CustomizableLandingPageUtils'; import { getCustomizableWidgetByPage, getDefaultTabs, getDefaultWidgetForTab, -} from '../../../../utils/CustomizePage/CustomizePageUtils'; -import { getEntityName } from '../../../../utils/EntityUtils'; -import { getWidgetFromKey } from '../../../../utils/GlossaryTerm/GlossaryTermUtil'; -import { DraggableTabs } from '../../../common/DraggableTabs/DraggableTabs'; -import AddDetailsPageWidgetModal from '../../../MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal'; +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../../utils/EntityUtils'; +import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import { DraggableTabs } from '../../common/DraggableTabs/DraggableTabs'; +import AddDetailsPageWidgetModal from '../../MyData/CustomizableComponents/AddDetailsPageWidgetModal/AddDetailsPageWidgetModal'; const ReactGridLayout = WidthProvider(RGL); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx similarity index 83% rename from openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index 590795a819f6..c0ff62af7be5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -14,15 +14,15 @@ import { AxiosError } from 'axios'; import { once } from 'lodash'; import React, { useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; -import { EntityType } from '../../enums/entity.enum'; -import { CreateThread } from '../../generated/api/feed/createThread'; -import { ThreadType } from '../../generated/entity/feed/thread'; -import { EntityReference } from '../../generated/entity/type'; -import { postThread } from '../../rest/feedsAPI'; -import { showErrorToast } from '../../utils/ToastUtils'; -import { useActivityFeedProvider } from '../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; +import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; +import { EntityType } from '../../../enums/entity.enum'; +import { CreateThread } from '../../../generated/api/feed/createThread'; +import { ThreadType } from '../../../generated/entity/feed/thread'; +import { EntityReference } from '../../../generated/entity/type'; +import { postThread } from '../../../rest/feedsAPI'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import { useActivityFeedProvider } from '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import ActivityThreadPanel from '../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; interface GenericProviderProps> { children?: React.ReactNode; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx similarity index 60% rename from openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index 179cf5a390b0..b17f31e2237f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchemaTab/TopicSchemaTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2025 Collate. + * Copyright 2024 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 @@ -13,59 +13,57 @@ import React, { useMemo } from 'react'; import RGL, { WidthProvider } from 'react-grid-layout'; import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../../enums/entity.enum'; import { Page, PageType, Tab } from '../../../generated/system/ui/page'; import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import topicClassBase from '../../../utils/TopicClassBase'; -import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; -import TopicSchemaFields from '../TopicSchema/TopicSchema'; +import { + getDefaultWidgetForTab, + getWidgetsFromKey, +} from '../../../utils/CustomizePage/CustomizePageUtils'; const ReactGridLayout = WidthProvider(RGL); -export const TopicSchemaTab = () => { +interface GenericTabProps { + type: PageType; +} + +export const GenericTab = ({ type }: GenericTabProps) => { const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); + const { tab = EntityTabs.CODE } = useParams<{ tab: EntityTabs }>(); const layout = useMemo(() => { if (!currentPersonaDocStore) { - return topicClassBase.getDefaultLayout(tab); + return getDefaultWidgetForTab(type, tab); } const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.Topic + (p: Page) => p.pageType === type ); if (page) { return page.tabs.find((t: Tab) => t.id === tab)?.layout; } else { - return topicClassBase.getDefaultLayout(tab); + return getDefaultWidgetForTab(type, tab); } - }, [currentPersonaDocStore, tab]); + }, [currentPersonaDocStore, tab, type]); const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { - return ; - } else { - return ; - } - }; + return layout.map((widget: WidgetConfig) => { + const renderedWidget = getWidgetsFromKey(type, widget); - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout]); + return renderedWidget ? ( +
+ {renderedWidget} +
+ ) : null; + }); + }, [layout, type]); // call the hook to set the direction of the grid layout useGridLayoutDirection(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx similarity index 88% rename from openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx rename to openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx index b5f5e52dc0ed..c3c487288230 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx @@ -17,40 +17,46 @@ import React, { useMemo } from 'react'; import { DetailPageWidgetKeys, GlossaryTermDetailPageWidgetKeys, -} from '../../../../enums/CustomizeDetailPage.enum'; -import { EntityType } from '../../../../enums/entity.enum'; -import { Container } from '../../../../generated/entity/data/container'; -import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; -import { DataType, Table } from '../../../../generated/entity/data/table'; -import { Topic } from '../../../../generated/entity/data/topic'; -import { EntityReference } from '../../../../generated/tests/testCase'; -import { TagSource } from '../../../../generated/type/tagLabel'; -import { WidgetCommonProps } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; -import { FrequentlyJoinedTables } from '../../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; -import TableConstraints from '../../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; -import containerDetailsClassBase from '../../../../utils/ContainerDetailsClassBase'; -import dashboardDataModelClassBase from '../../../../utils/DashboardDataModelBase'; -import domainClassBase from '../../../../utils/Domain/DomainClassBase'; -import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; -import { DEFAULT_ENTITY_PERMISSION } from '../../../../utils/PermissionsUtils'; -import tableClassBase from '../../../../utils/TableClassBase'; -import topicClassBase from '../../../../utils/TopicClassBase'; -import { ExtensionTable } from '../../../common/CustomPropertyTable/ExtensionTable'; -import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; -import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; -import RichTextEditorPreviewerV1 from '../../../common/RichTextEditor/RichTextEditorPreviewerV1'; -import TagButton from '../../../common/TagButton/TagButton.component'; -import ContainerChildren from '../../../Container/ContainerChildren/ContainerChildren'; -import { DashboardChartTable } from '../../../Dashboard/DashboardChartTable/DashboardChartTable'; -import ModelTab from '../../../Dashboard/DataModel/DataModels/ModelTab/ModelTab.component'; -import SchemaTable from '../../../Database/SchemaTable/SchemaTable.component'; -import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; -import { GenericProvider } from '../../../GenericProvider/GenericProvider'; -import TagsViewer from '../../../Tag/TagsViewer/TagsViewer'; -import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; -import TopicSchemaFields from '../../../Topic/TopicSchema/TopicSchema'; -import GlossaryTermTab from '../../GlossaryTermTab/GlossaryTermTab.component'; -import { ModifiedGlossary, useGlossaryStore } from '../../useGlossary.store'; +} from '../../../enums/CustomizeDetailPage.enum'; +import { EntityType } from '../../../enums/entity.enum'; +import { + Container, + DataType, + EntityReference, + TagSource, +} from '../../../generated/entity/data/container'; +import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel'; +import { Table } from '../../../generated/entity/data/table'; +import { Topic } from '../../../generated/entity/data/topic'; +import { WidgetCommonProps } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; +import containerDetailsClassBase from '../../../utils/ContainerDetailsClassBase'; +import dashboardDataModelClassBase from '../../../utils/DashboardDataModelBase'; +import domainClassBase from '../../../utils/Domain/DomainClassBase'; +import { renderReferenceElement } from '../../../utils/GlossaryUtils'; +import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import tableClassBase from '../../../utils/TableClassBase'; +import topicClassBase from '../../../utils/TopicClassBase'; +import { ExtensionTable } from '../../common/CustomPropertyTable/ExtensionTable'; +import { DomainLabel } from '../../common/DomainLabel/DomainLabel.component'; +import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; +import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; +import TagButton from '../../common/TagButton/TagButton.component'; +import ContainerChildren from '../../Container/ContainerChildren/ContainerChildren'; +import { DashboardChartTable } from '../../Dashboard/DashboardChartTable/DashboardChartTable'; +import ModelTab from '../../Dashboard/DataModel/DataModels/ModelTab/ModelTab.component'; +import SchemaTable from '../../Database/SchemaTable/SchemaTable.component'; +import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; +import GlossaryTermTab from '../../Glossary/GlossaryTermTab/GlossaryTermTab.component'; +import { + ModifiedGlossary, + useGlossaryStore, +} from '../../Glossary/useGlossary.store'; +import TagsViewer from '../../Tag/TagsViewer/TagsViewer'; +import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; +import TopicSchemaFields from '../../Topic/TopicSchema/TopicSchema'; +import { GenericProvider } from '../GenericProvider/GenericProvider'; export const GenericWidget = (props: WidgetCommonProps) => { const handleRemoveClick = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx index 073e7be4d163..a52f7b9c8a09 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx @@ -40,10 +40,10 @@ import { createTagObject } from '../../../utils/TagsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Table from '../../common/Table/Table'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import { ChartsPermissions } from '../DashboardDetails/DashboardDetails.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index 7060e45b9fa3..d543efcc7ba4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -13,7 +13,6 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; @@ -27,7 +26,6 @@ import { Tag } from '../../../generated/entity/classification/tag'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { Page } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/uiCustomization'; -import { TagLabel } from '../../../generated/type/tagLabel'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; @@ -36,25 +34,22 @@ import { restoreDashboard } from '../../../rest/dashboardAPI'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import dashboardDetailsClassBase from '../../../utils/DashboardDetailsClassBase'; -import { getEntityName } from '../../../utils/EntityUtils'; import { getGlossaryTermDetailTabs, getTabLabelMap, } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; -import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; +import { updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; import { DashboardDetailsProps } from './DashboardDetails.interface'; const DashboardDetails = ({ updateDashboardDetailsState, - charts, dashboardDetails, fetchDashboard, followDashboardHandler, @@ -71,9 +66,6 @@ const DashboardDetails = ({ useParams<{ tab: EntityTabs }>(); const [customizedPage, setCustomizedPage] = useState(null); const { fqn: decodedDashboardFQN } = useFqn(); - - const [isEdit, setIsEdit] = useState(false); - const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -84,22 +76,15 @@ const DashboardDetails = ({ const { owners, - description, - entityName, followers = [], deleted, - dashboardTags, - tier, } = useMemo(() => { - const { tags = [] } = dashboardDetails; - - return { - ...dashboardDetails, - tier: getTierTags(tags), - dashboardTags: getTagsWithoutTier(tags), - entityName: getEntityName(dashboardDetails), - }; - }, [dashboardDetails]); + return dashboardDetails; + }, [ + dashboardDetails.owners, + dashboardDetails.followers, + dashboardDetails.deleted, + ]); const { isFollowing } = useMemo(() => { return { @@ -154,31 +139,6 @@ const DashboardDetails = ({ } }; - const onDescriptionEdit = (): void => { - setIsEdit(true); - }; - const onCancel = () => { - setIsEdit(false); - }; - - const onDescriptionUpdate = async (updatedHTML: string) => { - if (description !== updatedHTML) { - const updatedDashboard = { - ...dashboardDetails, - description: updatedHTML, - }; - try { - await onDashboardUpdate(updatedDashboard); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsEdit(false); - } - } else { - setIsEdit(false); - } - }; - const onOwnerUpdate = useCallback( async (newOwners?: Dashboard['owners']) => { const updatedDashboard = { @@ -241,16 +201,6 @@ const DashboardDetails = ({ : await followDashboardHandler(); }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - - if (updatedTags && dashboardDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedDashboard = { ...dashboardDetails, tags: updatedTags }; - await onDashboardUpdate(updatedDashboard); - } - }; - const afterDeleteAction = useCallback( (isSoftDelete?: boolean, version?: number) => isSoftDelete ? handleToggleDelete(version) : history.push('/'), @@ -258,26 +208,12 @@ const DashboardDetails = ({ ); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, editAllPermission, editLineagePermission, viewAllPermission, } = useMemo( () => ({ - editTagsPermission: - (dashboardPermissions.EditTags || dashboardPermissions.EditAll) && - !deleted, - editGlossaryTermsPermission: - (dashboardPermissions.EditGlossaryTerms || - dashboardPermissions.EditAll) && - !deleted, - editDescriptionPermission: - (dashboardPermissions.EditDescription || - dashboardPermissions.EditAll) && - !deleted, editCustomAttributePermission: (dashboardPermissions.EditAll || dashboardPermissions.EditCustomFields) && @@ -295,20 +231,12 @@ const DashboardDetails = ({ const tabLabelMap = getTabLabelMap(customizedPage?.tabs); const tabs = dashboardDetailsClassBase.getDashboardDetailPageTabs({ - entityName, - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, dashboardDetails, - charts: charts ?? [], deleted: deleted ?? false, - dashboardTags, handleFeedCount, - handleTagSelection, - onDescriptionUpdate, onExtensionUpdate, feedCount, activeTab, @@ -325,21 +253,10 @@ const DashboardDetails = ({ }, [ feedCount.totalCount, activeTab, - isEdit, dashboardDetails, - charts, deleted, - entityName, - dashboardTags, - onCancel, handleFeedCount, - onDescriptionEdit, - onDescriptionUpdate, - handleTagSelection, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, - editDescriptionPermission, editCustomAttributePermission, editAllPermission, viewAllPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 97f65ca5f8e9..d1925e3b54d4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -39,9 +39,9 @@ import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import { withActivityFeed } from '../../../AppRouter/withActivityFeed'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; +import { GenericProvider } from '../../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../../Entity/EntityRightPanel/EntityRightPanel'; -import { GenericProvider } from '../../../GenericProvider/GenericProvider'; import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; import { DataModelDetailsProps } from './DataModelDetails.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx index d45ac532869f..1c7b32c11509 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx @@ -34,10 +34,10 @@ import { } from '../../../../../utils/TableTags/TableTags.utils'; import { updateFieldTags } from '../../../../../utils/TableUtils'; import Table from '../../../../common/Table/Table'; +import { useGenericContext } from '../../../../Customization/GenericProvider/GenericProvider'; import { ColumnFilter } from '../../../../Database/ColumnFilter/ColumnFilter.component'; import TableDescription from '../../../../Database/TableDescription/TableDescription.component'; import TableTags from '../../../../Database/TableTags/TableTags.component'; -import { useGenericContext } from '../../../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; const ModelTab = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index f0956d7abcb2..68af1ca360b2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -13,8 +13,13 @@ import { isEmpty, noop } from 'lodash'; import { EntityTags } from 'Models'; import React, { useMemo } from 'react'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; +import { + DetailPageWidgetKeys, + GlossaryTermDetailPageWidgetKeys, +} from '../../../enums/CustomizeDetailPage.enum'; import { EntityType } from '../../../enums/entity.enum'; +import { Chart } from '../../../generated/entity/data/chart'; +import { Column } from '../../../generated/entity/data/container'; import { Table } from '../../../generated/entity/data/table'; import { EntityReference } from '../../../generated/entity/type'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; @@ -25,10 +30,12 @@ import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject } from '../../../utils/TagsUtils'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; +import { OwnerLabelV2 } from '../OwnerLabelV2/OwnerLabelV2'; +import { ReviewerLabelV2 } from '../ReviewerLabelV2/ReviewerLabelV2'; interface GenericEntity extends Exclude, @@ -71,7 +78,10 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { }; }, [data, data?.tags]); - const { columns } = data as unknown as Table; + const { columns = [], charts = [] } = data as unknown as { + columns: Array; + charts: Array; + }; const { editTagsPermission, @@ -158,7 +168,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { entityName={entityName} entityType={type} hasEditAccess={editDescriptionPermission} - isDescriptionExpanded={isEmpty(columns)} + isDescriptionExpanded={isEmpty(columns) && isEmpty(charts)} owner={owners} removeBlur={widgetConfig.h > 1} showActions={!deleted} @@ -177,6 +187,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { deleted, columns, owners, + widgetConfig.h, ]); const handleExtensionUpdate = async (updatedTable: Table) => { @@ -211,6 +222,14 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { maxDataCap={5} /> ); + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.OWNERS)) { + return />; + } else if ( + widgetConfig.i.startsWith(GlossaryTermDetailPageWidgetKeys.REVIEWER) + ) { + return />; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.EXPERTS)) { + return />; } return getWidgetFromKey({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx index 94d30616c14f..51820a76fab2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx @@ -33,7 +33,7 @@ import { getEntityName } from '../../../utils/EntityUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import { DomainLabelProps } from '../../common/DomainLabel/DomainLabel.interface'; import DomainSelectableList from '../../common/DomainSelectableList/DomainSelectableList.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { AssetsUnion } from '../AssetsSelectionModal/AssetSelectionModal.interface'; import { DataAssetWithDomains } from '../DataAssetsHeader/DataAssetsHeader.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/OwnerLabelV2/OwnerLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/OwnerLabelV2/OwnerLabelV2.tsx index 2791ed0bba62..ed3fb0c98dfd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/OwnerLabelV2/OwnerLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/OwnerLabelV2/OwnerLabelV2.tsx @@ -21,7 +21,7 @@ import { EntityReference } from '../../../generated/entity/type'; import { getOwnerVersionLabel } from '../../../utils/EntityVersionUtils'; import TagButton from '../../common/TagButton/TagButton.component'; import { UserTeamSelectableList } from '../../common/UserTeamSelectableList/UserTeamSelectableList.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; export const OwnerLabelV2 = < T extends { owners?: EntityReference[]; id: string } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx index 8ed7a583083c..78214d9fd6f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx @@ -17,18 +17,21 @@ import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg'; import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { TabSpecificField } from '../../../enums/entity.enum'; -import { Glossary } from '../../../generated/entity/data/glossary'; -import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { EntityReference } from '../../../generated/entity/type'; +import { ChangeDescription } from '../../../generated/type/changeEvent'; import { getOwnerVersionLabel } from '../../../utils/EntityVersionUtils'; import TagButton from '../../common/TagButton/TagButton.component'; import { UserTeamSelectableList } from '../../common/UserTeamSelectableList/UserTeamSelectableList.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; -export const ReviewerLabelV2 = () => { - const { data, onUpdate, permissions, isVersionView } = useGenericContext< - GlossaryTerm | Glossary - >(); +export const ReviewerLabelV2 = < + T extends { + reviewers?: EntityReference[]; + id: string; + changeDescription?: ChangeDescription; + } +>() => { + const { data, onUpdate, permissions, isVersionView } = useGenericContext(); const hasEditReviewerAccess = useMemo(() => { return permissions.EditAll || permissions.EditReviewers; @@ -95,7 +98,7 @@ export const ReviewerLabelV2 = () => {
{getOwnerVersionLabel( - data, + data as T, isVersionView ?? false, TabSpecificField.REVIEWERS, hasEditReviewerAccess diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx index d23cdec38135..88fa75d70edd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx @@ -49,7 +49,6 @@ import { ChangeDescription, DataProduct, } from '../../../generated/entity/domains/dataProduct'; -import { Domain } from '../../../generated/entity/domains/domain'; import { Operation } from '../../../generated/entity/policies/policy'; import { Style } from '../../../generated/type/tagLabel'; import { useFqn } from '../../../hooks/useFqn'; @@ -73,6 +72,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import { ManageButtonItemLabel } from '../../common/ManageButtonContentItem/ManageButtonContentItem.component'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { AssetSelectionModal } from '../../DataAssets/AssetsSelectionModal/AssetSelectionModal'; import { DomainTabs } from '../../Domain/DomainPage.interface'; import DocumentationTab from '../../Domain/DomainTabs/DocumentationTab/DocumentationTab.component'; @@ -409,14 +409,8 @@ const DataProductsDetailsPage = ({ key: DataProductTabs.DOCUMENTATION, children: ( - onUpdate(data as DataProduct) - } /> ), }, @@ -622,16 +616,22 @@ const DataProductsDetailsPage = ({
-
- - + + data={dataProduct} + permissions={dataProductPermission} + type={EntityType.DATA_PRODUCT} + onUpdate={onUpdate}> + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx index 303809d7f8de..c562b220b3bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx @@ -56,7 +56,7 @@ import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.i import RichTextEditorPreviewerV1 from '../../../common/RichTextEditor/RichTextEditorPreviewerV1'; import Searchbar from '../../../common/SearchBarComponent/SearchBar.component'; import Table from '../../../common/Table/Table'; -import { useGenericContext } from '../../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface'; import { DatabaseSchemaTableProps } from './DatabaseSchemaTable.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx deleted file mode 100644 index e92e16c4e6a3..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2024 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 React, { useMemo } from 'react'; -import RGL, { WidthProvider } from 'react-grid-layout'; -import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../../../../enums/entity.enum'; -import { Page, PageType, Tab } from '../../../../generated/system/ui/page'; -import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; -import SchemaTablesTab from '../../../../pages/DatabaseSchemaPage/SchemaTablesTab'; -import { getDefaultWidgetForTab } from '../../../../utils/CustomizePage/CustomizePageUtils'; -import databaseSchemaClassBase from '../../../../utils/DatabaseSchemaClassBase'; -import { CommonWidgets } from '../../../DataAssets/CommonWidgets/CommonWidgets'; - -const ReactGridLayout = WidthProvider(RGL); - -export const DatabaseSchemaTableTab = () => { - const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.TABLE } = useParams<{ tab: EntityTabs }>(); - - const layout = useMemo(() => { - if (!currentPersonaDocStore) { - return databaseSchemaClassBase.getDefaultLayout(tab); - } - - const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.DatabaseSchema - ); - - if (page) { - return page.tabs.find((t: Tab) => t.id === tab)?.layout; - } else { - return getDefaultWidgetForTab(PageType.DatabaseSchema, tab); - } - }, [currentPersonaDocStore, tab]); - - const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLES)) { - return ; - } else { - return ; - } - }; - - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout]); - - // call the hook to set the direction of the grid layout - useGridLayoutDirection(); - - return ( - <> - - {widgets} - - - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx deleted file mode 100644 index 428e5438c41d..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchemaTab/DatabaseSchemaTab.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2024 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 React, { useMemo } from 'react'; -import RGL, { WidthProvider } from 'react-grid-layout'; -import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../../../enums/entity.enum'; -import { Page, PageType, Tab } from '../../../generated/system/ui/page'; -import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; -import databaseClassBase from '../../../utils/Database/DatabaseClassBase'; -import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; -import { DatabaseSchemaTable } from '../DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; - -const ReactGridLayout = WidthProvider(RGL); - -export const DatabaseSchemaTab = () => { - const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); - - const layout = useMemo(() => { - if (!currentPersonaDocStore) { - return databaseClassBase.getDefaultLayout(tab); - } - - const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.Database - ); - - if (page) { - return page.tabs.find((t: Tab) => t.id === tab)?.layout; - } else { - return getDefaultWidgetForTab(PageType.Database, tab); - } - }, [currentPersonaDocStore, tab]); - - const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DATABASE_SCHEMA)) { - return ; - } else { - return ; - } - }; - - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout]); - - // call the hook to set the direction of the grid layout - useGridLayoutDirection(); - - return ( - <> - - {widgets} - - - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index 2c73b3301325..6aeddbda4401 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -75,7 +75,7 @@ import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTabl import Searchbar from '../../common/SearchBarComponent/SearchBar.component'; import Table from '../../common/Table/Table'; import TestCaseStatusSummaryIndicator from '../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component'; import { EntityName, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx index a4ea7f484377..52c318a19cd7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard.tsx @@ -17,7 +17,7 @@ import { StoredProcedure, StoredProcedureCodeObject, } from '../../../generated/entity/data/storedProcedure'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import SchemaEditor from '../SchemaEditor/SchemaEditor'; export const StoredProcedureCodeCard = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx index fe21f9aa2be0..8e635a532d4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDescription/TableDescription.component.tsx @@ -22,7 +22,7 @@ import EntityTasks from '../../../pages/TasksPage/EntityTasks/EntityTasks.compon import EntityLink from '../../../utils/EntityLink'; import { getEntityFeedLink } from '../../../utils/EntityUtils'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert'; import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; import { TableDescriptionProps } from './TableDescription.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx deleted file mode 100644 index 9e295728c06b..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableGenericTab/TableGenericTab.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2024 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 { isEmpty } from 'lodash'; -import React, { useMemo } from 'react'; -import RGL, { WidthProvider } from 'react-grid-layout'; -import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../../../enums/entity.enum'; -import { Table } from '../../../generated/entity/data/table'; -import { Page, PageType, Tab } from '../../../generated/system/ui/page'; -import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; -import { PartitionedKeys } from '../../../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; -import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; -import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; -import tableClassBase from '../../../utils/TableClassBase'; -import { getJoinsFromTableJoins } from '../../../utils/TableUtils'; -import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; -import SchemaTable from '../SchemaTable/SchemaTable.component'; -import { StoredProcedureCodeCard } from '../StoredProcedureCodeCard/StoredProcedureCodeCard'; - -const ReactGridLayout = WidthProvider(RGL); - -export const TableGenericTab = () => { - const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); - - const { data: tableDetails } = useGenericContext
(); - const joins = useMemo( - () => getJoinsFromTableJoins(tableDetails?.joins), - [tableDetails?.joins] - ); - - const layout = useMemo(() => { - if (!currentPersonaDocStore) { - return tableClassBase.getDefaultLayout(tab); - } - - const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.Table - ); - - if (page) { - return page.tabs.find((t: Tab) => t.id === tab)?.layout; - } else { - return getDefaultWidgetForTab(PageType.Table, tab); - } - }, [currentPersonaDocStore, tab]); - - const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { - return ; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) - ) { - return ; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) - ) { - return isEmpty(joins) ? null : ; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS) - ) { - return ; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) - ) { - return ; - } else { - return ; - } - }; - - return layout.map((widget: WidgetConfig) => { - const renderedWidget = getWidgetFromKeyInternal(widget); - - return renderedWidget ? ( -
- {renderedWidget} -
- ) : null; - }); - }, [layout, joins]); - - // call the hook to set the direction of the grid layout - useGridLayoutDirection(); - - return ( - <> - - {widgets} - - - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx index 633afbfc0f41..453758341cd7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableTags/TableTags.component.tsx @@ -16,7 +16,7 @@ import { lowerCase } from 'lodash'; import React from 'react'; import { EntityField } from '../../../constants/Feeds.constants'; import EntityTasks from '../../../pages/TasksPage/EntityTasks/EntityTasks.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { TableTagsComponentProps, TableUnion } from './TableTags.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx index 8b13e5806c33..b92d83b097ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx @@ -103,6 +103,7 @@ import { } from '../../../utils/StringsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { AssetSelectionModal } from '../../DataAssets/AssetsSelectionModal/AssetSelectionModal'; import { EntityDetailsObjectInterface } from '../../Explore/ExplorePage.interface'; import StyleModal from '../../Modals/StyleModal/StyleModal.component'; @@ -521,7 +522,6 @@ const DomainDetailsPage = ({ dataProductsCount, assetCount, activeTab: activeTab as EntityTabs, - onUpdate, onAddDataProduct, isSubDomainsLoading, queryFilter, @@ -716,16 +716,22 @@ const DomainDetailsPage = ({ -
- - + + data={domain} + permissions={domainPermission} + type={EntityType.DOMAIN} + onUpdate={onUpdate}> + + + + {showAddDataProductModal && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx index 6ee906f1a1f6..00229776305b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx @@ -44,6 +44,7 @@ import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomP import FormItemLabel from '../../../common/Form/FormItemLabel'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import TagButton from '../../../common/TagButton/TagButton.component'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import '../../domain.less'; import { DocumentationEntity, @@ -51,12 +52,8 @@ import { } from './DocumentationTab.interface'; const DocumentationTab = ({ - domain, - onUpdate, - onExtensionUpdate, isVersionsView = false, type = DocumentationEntity.DOMAIN, - permissions, }: DocumentationTabProps) => { const { t } = useTranslation(); const [editDomainType, setEditDomainType] = useState(false); @@ -64,6 +61,11 @@ const DocumentationTab = ({ type === DocumentationEntity.DOMAIN ? ResourceEntity.DOMAIN : ResourceEntity.DATA_PRODUCT; + const { + data: domain, + onUpdate, + permissions, + } = useGenericContext(); const { editDescriptionPermission, @@ -357,7 +359,7 @@ const DocumentationTab = ({ isRenderedInRightPanel entityType={EntityType.DATA_PRODUCT} - handleExtensionUpdate={onExtensionUpdate} + handleExtensionUpdate={onUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={Boolean(viewAllPermission)} maxDataCap={5} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface.ts index faf3bb363808..fd8ae426abe8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface.ts @@ -10,17 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; -import { DataProduct } from '../../../../generated/entity/domains/dataProduct'; -import { Domain } from '../../../../generated/entity/domains/domain'; export interface DocumentationTabProps { - domain: Domain | DataProduct; - onUpdate: (value: Domain | DataProduct) => Promise; isVersionsView?: boolean; type?: DocumentationEntity; - onExtensionUpdate?: (updatedDataProduct: DataProduct) => Promise; - permissions?: OperationPermission; } export enum DocumentationEntity { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index 4d086633814f..6034071c5950 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -24,8 +24,8 @@ import type { ExtentionEntities, ExtentionEntitiesKeys, } from '../../common/CustomPropertyTable/CustomPropertyTable.interface'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 08d72a6b85e2..db484b5ddde4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -50,10 +50,10 @@ import { import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DomainLabelV2 } from '../../DataAssets/DomainLabelV2/DomainLabelV2'; import { OwnerLabelV2 } from '../../DataAssets/OwnerLabelV2/OwnerLabelV2'; import { ReviewerLabelV2 } from '../../DataAssets/ReviewerLabelV2/ReviewerLabelV2'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx index 872cb57f05a1..16195f3fee17 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx @@ -68,8 +68,8 @@ import { } from '../../../utils/RouterUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcrumb.interface'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import Voting from '../../Entity/Voting/Voting.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import ChangeParentHierarchy from '../../Modals/ChangeParentHierarchy/ChangeParentHierarchy.component'; import StyleModal from '../../Modals/StyleModal/StyleModal.component'; import { GlossaryStatusBadge } from '../GlossaryStatusBadge/GlossaryStatusBadge.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index 6c32d8039227..ae64d9096b37 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -58,8 +58,8 @@ import { import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { AssetSelectionModal } from '../../DataAssets/AssetsSelectionModal/AssetSelectionModal'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component'; import GlossaryTermTab from '../GlossaryTermTab/GlossaryTermTab.component'; import { useGlossaryStore } from '../useGlossary.store'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index 0e250f2bc4d3..949a74474dc8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -34,10 +34,10 @@ import { import { getWidgetFromKey } from '../../../../utils/GlossaryTerm/GlossaryTermUtil'; import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import { DomainLabelV2 } from '../../../DataAssets/DomainLabelV2/DomainLabelV2'; import { OwnerLabelV2 } from '../../../DataAssets/OwnerLabelV2/OwnerLabelV2'; import { ReviewerLabelV2 } from '../../../DataAssets/ReviewerLabelV2/ReviewerLabelV2'; -import { useGenericContext } from '../../../GenericProvider/GenericProvider'; import TagsContainerV2 from '../../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; import { GlossaryUpdateConfirmationModal } from '../../GlossaryUpdateConfirmationModal/GlossaryUpdateConfirmationModal'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.tsx index a72a5a1a5a92..7f3827372707 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.tsx @@ -35,7 +35,7 @@ import { } from '../../../../utils/EntityVersionUtils'; import { renderReferenceElement } from '../../../../utils/GlossaryUtils'; import TagButton from '../../../common/TagButton/TagButton.component'; -import { useGenericContext } from '../../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import GlossaryTermReferencesModal from '../GlossaryTermReferencesModal.component'; const GlossaryTermReferences = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.tsx index a1fb34d9a824..39b32d5cd108 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.tsx @@ -32,7 +32,7 @@ import { getDiffByFieldName, } from '../../../../utils/EntityVersionUtils'; import TagButton from '../../../common/TagButton/TagButton.component'; -import { useGenericContext } from '../../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; const GlossaryTermSynonyms = () => { const [isViewMode, setIsViewMode] = useState(true); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index d9a528bb19e7..81a42f78359c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -46,7 +46,7 @@ import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; import { getGlossaryPath } from '../../../../utils/RouterUtils'; import { SelectOption } from '../../../common/AsyncSelectList/AsyncSelectList.interface'; import TagButton from '../../../common/TagButton/TagButton.component'; -import { useGenericContext } from '../../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; const RelatedTerms = () => { const history = useHistory(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index 660c808af4e1..440612e99ca2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -45,9 +45,9 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index 55db3dc414f6..f908c418f6ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -47,9 +47,9 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx index f67234918b24..6ec8d0da9756 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomiseGlossaryTermDetailPage/CustomiseGlossaryTermDetailPage.tsx @@ -19,7 +19,7 @@ import { useGridLayoutDirection } from '../../../../hooks/useGridLayoutDirection import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; import '../../../../pages/MyDataPage/my-data.less'; import { getEntityName } from '../../../../utils/EntityUtils'; -import { CustomizeTabWidget } from '../../../Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; +import { CustomizeTabWidget } from '../../../Customization/CustomizeTabWidget/CustomizeTabWidget'; import { GlossaryHeaderWidget } from '../../../Glossary/GlossaryHeader/GlossaryHeaderWidget'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; import { CustomizablePageHeader } from '../CustomizablePageHeader/CustomizablePageHeader'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index 59a84f20b77a..e62cf75d143b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -66,11 +66,11 @@ import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx index 48afec36aadd..1a62cd93d73e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx @@ -38,8 +38,8 @@ import { getUpdateTagsPath, } from '../../../utils/TasksUtils'; import { SelectOption } from '../../common/AsyncSelectList/AsyncSelectList.interface'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { TableTagsProps } from '../../Database/TableTags/TableTags.interface'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import TagSelectForm from '../TagsSelectForm/TagsSelectForm.component'; import TagsV1 from '../TagsV1/TagsV1.component'; import TagsViewer from '../TagsViewer/TagsViewer'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index d41279cc1cff..207d645e483c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -51,9 +51,9 @@ import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeed import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import SampleDataWithMessages from '../../Database/SampleDataWithMessages/SampleDataWithMessages'; -import { GenericProvider } from '../../GenericProvider/GenericProvider'; import Lineage from '../../Lineage/Lineage.component'; import QueryViewer from '../../common/QueryViewer/QueryViewer.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index 56a90a4a7225..d4cdaf0daeb9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -54,11 +54,11 @@ import { import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import ToggleExpandButton from '../../common/ToggleExpandButton/ToggleExpandButton'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import { SchemaViewType, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx index c960b1a498ee..df39f1dfdcd7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx @@ -43,7 +43,7 @@ import { getUpdatedExtensionDiffFields, } from '../../../utils/EntityVersionUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder'; import './custom-property-table.less'; import { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index 182f58653e80..51afd745bedf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -31,7 +31,7 @@ import { getUpdateDescriptionPath, TASK_ENTITIES, } from '../../../utils/TasksUtils'; -import { useGenericContext } from '../../GenericProvider/GenericProvider'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert'; import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index eba2d0f5db66..31a95eff2059 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -34,6 +34,7 @@ import Loader from '../../components/common/Loader/Loader'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; @@ -506,6 +507,14 @@ const APICollectionPage: FunctionComponent = () => { }); }; + const handleAPICollectionUpdate = async (updatedData: APICollection) => { + const response = await saveUpdatedAPICollectionData({ + ...apiCollection, + ...updatedData, + }); + setAPICollection(response); + }; + const tabs: TabsProps['items'] = [ { label: ( @@ -678,15 +687,22 @@ const APICollectionPage: FunctionComponent = () => { /> )} - - - + + data={apiCollection} + isVersionView={false} + permissions={apiCollectionPermission} + type={EntityType.API_COLLECTION} + onUpdate={handleAPICollectionUpdate}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 139b627ba19b..1bcecb0d8aa5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -21,9 +21,9 @@ import { useHistory, useParams } from 'react-router-dom'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx index b0a5e1f7c30b..b4760e2e6986 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx @@ -15,8 +15,8 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import gridBgImg from '../../assets/img/grid-bg-img.png'; import { ReactComponent as DomainIcon } from '../../assets/svg/ic-domain.svg'; +import { CustomizeTabWidget } from '../../components/Customization/CustomizeTabWidget/CustomizeTabWidget'; import { EntityHeader } from '../../components/Entity/EntityHeader/EntityHeader.component'; -import { CustomizeTabWidget } from '../../components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; import { CustomizablePageHeader } from '../../components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader'; import { CustomizeMyDataProps } from '../../components/MyData/CustomizableComponents/CustomizeMyData/CustomizeMyData.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx index ed940c12eadf..118e0c75f128 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizeTableDetailPage/CustomizeTableDetailPage.tsx @@ -14,8 +14,8 @@ import { camelCase, noop } from 'lodash'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import gridBgImg from '../../assets/img/grid-bg-img.png'; +import { CustomizeTabWidget } from '../../components/Customization/CustomizeTabWidget/CustomizeTabWidget'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import { CustomizeTabWidget } from '../../components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; import { CustomizablePageHeader } from '../../components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader'; import { CustomizeMyDataProps } from '../../components/MyData/CustomizableComponents/CustomizeMyData/CustomizeMyData.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index 4579f2fdd6f2..e1eb437e296e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -28,10 +28,10 @@ import { useHistory, useParams } from 'react-router-dom'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import ProfilerSettings from '../../components/Database/Profiler/ProfilerSettings/ProfilerSettings'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index bdb532e2c472..b1f5b0acd6a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -27,10 +27,10 @@ import { useHistory, useParams } from 'react-router-dom'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import ProfilerSettings from '../../components/Database/Profiler/ProfilerSettings/ProfilerSettings'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx index 816cafaf2fb2..b9ef515b4134 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/SchemaTablesTab.tsx @@ -24,7 +24,7 @@ import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; import RichTextEditorPreviewerV1 from '../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; import TableAntd from '../../components/common/Table/Table'; -import { useGenericContext } from '../../components/GenericProvider/GenericProvider'; +import { useGenericContext } from '../../components/Customization/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import { INITIAL_PAGING_VALUE, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index 2b7078fac1e6..6e0d47c9bb2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -28,11 +28,11 @@ import Loader from '../../components/common/Loader/Loader'; import QueryViewer from '../../components/common/QueryViewer/QueryViewer.component'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import SampleDataWithMessages from '../../components/Database/SampleDataWithMessages/SampleDataWithMessages'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import Lineage from '../../components/Lineage/Lineage.component'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index f8eedd7de1a1..1e8ff45185fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -19,9 +19,9 @@ import { useHistory, useParams } from 'react-router-dom'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx index fef044b402e8..ae85ee426912 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component.tsx @@ -15,7 +15,7 @@ import { isEmpty } from 'lodash'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../components/Customization/GenericProvider/GenericProvider'; import { getEntityDetailsPath } from '../../../constants/constants'; import { EntityType } from '../../../enums/entity.enum'; import { JoinedWith, Table } from '../../../generated/entity/data/table'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx index 0436e690acb5..615a098befed 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component.tsx @@ -15,7 +15,7 @@ import { ColumnsType } from 'antd/lib/table'; import { t } from 'i18next'; import React, { useMemo } from 'react'; import Table from '../../../components/common/Table/Table'; -import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../components/Customization/GenericProvider/GenericProvider'; import { PartitionColumnDetails, Table as TableType, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx index 73ac4cd6f2bf..2b3a3e4f893c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableConstraints/TableConstraints.tsx @@ -18,7 +18,7 @@ import { Link } from 'react-router-dom'; import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg'; import TagButton from '../../../components/common/TagButton/TagButton.component'; -import { useGenericContext } from '../../../components/GenericProvider/GenericProvider'; +import { useGenericContext } from '../../../components/Customization/GenericProvider/GenericProvider'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants'; import { EntityType, FqnPart } from '../../../enums/entity.enum'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 81e93ab348dc..32ac829a9a4c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -25,9 +25,9 @@ import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import { withSuggestions } from '../../components/AppRouter/withSuggestions'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import { GenericProvider } from '../../components/GenericProvider/GenericProvider'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index df38ed1e700e..45dcd475d0c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -22,6 +22,7 @@ import ResizablePanels from '../components/common/ResizablePanels/ResizablePanel import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import ContainerChildren from '../components/Container/ContainerChildren/ContainerChildren'; import ContainerDataModel from '../components/Container/ContainerDataModel/ContainerDataModel'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; @@ -33,6 +34,7 @@ import { ContainerDataModel as ContainerDataModelType, } from '../generated/entity/data/container'; import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { ContainerDetailPageTabProps } from './ContainerDetailsClassBase'; const getUpdatedContainerColumnTags = ( @@ -300,3 +302,7 @@ export const getContainerDetailPageTabs = ({ }, ]; }; + +export const getContainerWidgetsFromKey = (widgetConfig: WidgetConfig) => { + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index 17f48cb281fa..ebfa9c58d5e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -34,6 +34,8 @@ import { getContainerDetailPageTabs } from './ContainerDetailUtils'; import { EntityReference } from '../generated/entity/type'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; +import { getContainerWidgetsFromKey } from './ContainerDetailUtils'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; @@ -225,6 +227,10 @@ class ContainerDetailsClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getContainerWidgetsFromKey(widgetConfig); + } } const containerDetailsClassBase = new ContainerDetailsClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts index 16c4fc698ed2..68465ed292a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts @@ -12,7 +12,7 @@ */ import { FC } from 'react'; -import { GenericWidget } from '../../components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget'; +import { GenericWidget } from '../../components/Customization/GenericWidget/GenericWidget'; import { CUSTOM_PROPERTIES_WIDGET, DESCRIPTION_WIDGET, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryPage/CustomizeGlossaryPage.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryPage/CustomizeGlossaryPage.ts index ced51bdae083..4700e8a7d548 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryPage/CustomizeGlossaryPage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryPage/CustomizeGlossaryPage.ts @@ -14,8 +14,8 @@ import { CustomizeTabWidget, CustomizeTabWidgetProps, -} from '../../components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; -import { GenericWidget } from '../../components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget'; +} from '../../components/Customization/CustomizeTabWidget/CustomizeTabWidget'; +import { GenericWidget } from '../../components/Customization/GenericWidget/GenericWidget'; import GlossaryHeader from '../../components/Glossary/GlossaryHeader/GlossaryHeader.component'; import { GlossaryHeaderProps } from '../../components/Glossary/GlossaryHeader/GlossaryHeader.interface'; import { GlossaryHeaderWidget } from '../../components/Glossary/GlossaryHeader/GlossaryHeaderWidget'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts index 37ebf303ee78..435c9995e356 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts @@ -14,8 +14,8 @@ import { CustomizeTabWidget, CustomizeTabWidgetProps, -} from '../../components/Glossary/CustomiseWidgets/CustomizeTabWidget/CustomizeTabWidget'; -import { GenericWidget } from '../../components/Glossary/CustomiseWidgets/SynonymsWidget/GenericWidget'; +} from '../../components/Customization/CustomizeTabWidget/CustomizeTabWidget'; +import { GenericWidget } from '../../components/Customization/GenericWidget/GenericWidget'; import GlossaryHeader from '../../components/Glossary/GlossaryHeader/GlossaryHeader.component'; import { GlossaryHeaderProps } from '../../components/Glossary/GlossaryHeader/GlossaryHeader.interface'; import { GlossaryHeaderWidget } from '../../components/Glossary/GlossaryHeader/GlossaryHeaderWidget'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index e09f23ed2825..9fddcd369d19 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -14,6 +14,7 @@ import { TabsProps } from 'antd'; import { CommonWidgetType } from '../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../enums/entity.enum'; import { PageType, Tab } from '../../generated/system/ui/page'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import containerDetailsClassBase from '../ContainerDetailsClassBase'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; @@ -268,6 +269,8 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return containerDetailsClassBase.getDefaultLayout(tab); case PageType.Domain: return domainClassBase.getDefaultLayout(tab); + case PageType.Dashboard: + return dashboardDetailsClassBase.getDefaultLayout(tab); default: return []; } @@ -359,3 +362,41 @@ export const getDummyDataByPage = (pageType: PageType) => { return {}; } }; + +export const getWidgetsFromKey = ( + pageType: PageType, + widgetConfig: WidgetConfig +): JSX.Element | null => { + switch (pageType) { + case PageType.Table: + return tableClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Topic: + return topicClassBase.getWidgetsFromKey(widgetConfig); + case PageType.StoredProcedure: + return storedProcedureClassBase.getWidgetsFromKey(widgetConfig); + // case PageType.GlossaryTerm: + // return customizeGlossaryTermPageClassBase.getWidgetsFromKey(widgetConfig); + // case PageType.Glossary: + // return customizeGlossaryPageClassBase.getWidgetsFromKey(widgetConfig); + // case PageType.LandingPage: + // return customizeMyDataPageClassBase.getWidgetsFromKey(widgetConfig); + case PageType.DashboardDataModel: + return dashboardDataModelClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Container: + return containerDetailsClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Database: + return databaseClassBase.getWidgetsFromKey(widgetConfig); + case PageType.DatabaseSchema: + return databaseSchemaClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Pipeline: + return pipelineClassBase.getWidgetsFromKey(widgetConfig); + case PageType.SearchIndex: + return searchIndexClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Dashboard: + return dashboardDetailsClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Domain: + return domainClassBase.getWidgetsFromKey(widgetConfig); + default: + return null; + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index 40068289d0f7..bd06a71dfa8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -25,8 +25,12 @@ import { EntityTabs } from '../enums/entity.enum'; import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; -import { getDashboardDataModelDetailPageTabs } from './DashboardDataModelUtils'; +import { + getDashboardDataModelDetailPageTabs, + getDashboardDataModelWidgetsFromKey, +} from './DashboardDataModelUtils'; import i18n from './i18next/LocalUtil'; export interface DashboardDataModelDetailPageTabProps { @@ -65,7 +69,7 @@ class DashboardDataModelBase { name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [EntityTabs.MODEL].includes(tab), + editable: tab === EntityTabs.MODEL, })); } @@ -241,6 +245,10 @@ class DashboardDataModelBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getDashboardDataModelWidgetsFromKey(widgetConfig); + } } const dashboardDataModelClassBase = new DashboardDataModelBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index e78388fe49fa..a99cc8be908a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -17,12 +17,14 @@ import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/Acti import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import LineageProvider from '../context/LineageProvider/LineageProvider'; import { CSMode } from '../enums/codemirror.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; import i18n from './i18next/LocalUtil'; @@ -144,3 +146,9 @@ export const getDashboardDataModelDetailPageTabs = ({ }, ]; }; + +export const getDashboardDataModelWidgetsFromKey = ( + widgetConfig: WidgetConfig +) => { + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 5a23b3b0f7be..0831136c2f78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -10,7 +10,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityTags } from 'Models'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -22,32 +21,26 @@ import { } from '../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { Tag } from '../generated/entity/classification/tag'; import { Dashboard, DashboardServiceType, } from '../generated/entity/data/dashboard'; -import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; -import { getDashboardDetailPageTabs } from './DashboardDetailsUtils'; +import { + getDashboardDetailPageTabs, + getDashboardWidgetsFromKey, +} from './DashboardDetailsUtils'; import i18n from './i18next/LocalUtil'; export interface DashboardDetailsTabsProps { dashboardDetails: Dashboard; - charts: EntityReference[]; - entityName: string; - editDescriptionPermission: boolean; - editTagsPermission: boolean; - editGlossaryTermsPermission: boolean; editLineagePermission: boolean; editCustomAttributePermission: boolean; viewAllPermission: boolean; - dashboardTags: Tag[]; handleFeedCount: (data: FeedCounts) => void; - onDescriptionUpdate: (value: string) => Promise; - handleTagSelection: (selectedTags: EntityTags[]) => Promise; onExtensionUpdate: (data: Dashboard) => Promise; feedCount: FeedCounts; activeTab: EntityTabs; @@ -82,9 +75,10 @@ class DashboardDetailsClassBase { public getDefaultLayout(tab: EntityTabs) { switch (tab) { case EntityTabs.DETAILS: + default: return [ { - h: 2, + h: 1, i: DetailPageWidgetKeys.DESCRIPTION, w: 6, x: 0, @@ -108,7 +102,7 @@ class DashboardDetailsClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.TAGS, w: 2, x: 6, @@ -116,7 +110,7 @@ class DashboardDetailsClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.GLOSSARY_TERMS, w: 2, x: 6, @@ -132,9 +126,6 @@ class DashboardDetailsClassBase { static: false, }, ]; - - default: - return []; } } @@ -227,6 +218,10 @@ class DashboardDetailsClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getDashboardWidgetsFromKey(widgetConfig); + } } const dashboardDetailsClassBase = new DashboardDetailsClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 4cf46af25b78..1a61e5745bd1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -11,26 +11,24 @@ * limitations under the License. */ -import { Col, Row } from 'antd'; import { AxiosError } from 'axios'; import { t } from 'i18next'; -import { isEmpty } from 'lodash'; import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { DashboardChartTable } from '../components/Dashboard/DashboardChartTable/DashboardChartTable'; -import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Dashboard } from '../generated/entity/data/dashboard'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { getChartById } from '../rest/chartAPI'; import { DashboardDetailsTabsProps } from './DashboardDetailsClassBase'; @@ -61,79 +59,12 @@ export const fetchCharts = async (charts: Dashboard['charts']) => { return chartsData; }; -export const getDashboardDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.DETAILS: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - export const getDashboardDetailPageTabs = ({ dashboardDetails, - charts, - entityName, - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, - dashboardTags, handleFeedCount, - onDescriptionUpdate, - handleTagSelection, onExtensionUpdate, feedCount, activeTab, @@ -147,55 +78,7 @@ export const getDashboardDetailPageTabs = ({ ), key: EntityTabs.DETAILS, - children: ( - - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.DASHBOARD} - selectedTags={dashboardTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( @@ -253,3 +136,11 @@ export const getDashboardDetailPageTabs = ({ }, ]; }; + +export const getDashboardWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { + return ; + } + + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 197c3e069b80..7102f3bb64b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -22,7 +22,9 @@ import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.compon import RichTextEditorPreviewerV1 from '../../components/common/RichTextEditor/RichTextEditorPreviewerV1'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; -import { DatabaseSchemaTab } from '../../components/Database/DatabaseSchemaTab/DatabaseSchemaTab'; +import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../../components/DataAssets/CommonWidgets/CommonWidgets'; +import { DatabaseSchemaTable } from '../../components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; import { getEntityDetailsPath, NO_DATA_PLACEHOLDER, @@ -35,7 +37,9 @@ import { } from '../../enums/entity.enum'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; import { EntityReference } from '../../generated/entity/type'; +import { PageType } from '../../generated/system/ui/page'; import { UsageDetails } from '../../generated/type/entityUsage'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import { getEntityName } from '../EntityUtils'; import { getUsagePercentile } from '../TableUtils'; import { DatabaseDetailPageTabProps } from './DatabaseClassBase'; @@ -127,73 +131,6 @@ export const schemaTableColumns: ColumnsType = [ }, ]; -export const getDatabaseDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - export const getDatabasePageBaseTabs = ({ activeTab, database, @@ -217,7 +154,7 @@ export const getDatabasePageBaseTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: , + children: , }, { label: ( @@ -263,3 +200,11 @@ export const getDatabasePageBaseTabs = ({ }, ]; }; + +export const getDatabaseWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DATABASE_SCHEMA)) { + return ; + } + + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index 3eeda0254611..80c8829f15fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -29,9 +29,13 @@ import { import { Tab } from '../../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../../generated/type/tagLabel'; import { FeedCounts } from '../../interface/feed.interface'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; import i18n from '../i18next/LocalUtil'; -import { getDatabasePageBaseTabs } from './Database.util'; +import { + getDatabasePageBaseTabs, + getDatabaseWidgetsFromKey, +} from './Database.util'; export interface DatabaseDetailPageTabProps { activeTab: EntityTabs; @@ -253,6 +257,10 @@ class DatabaseClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getDatabaseWidgetsFromKey(widgetConfig); + } } const databaseClassBase = new DatabaseClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index d18a94a0ad8f..45755c1329c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -30,8 +30,12 @@ import { import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; -import { getDataBaseSchemaPageBaseTabs } from './DatabaseSchemaDetailsUtils'; +import { + getDataBaseSchemaPageBaseTabs, + getDatabaseSchemaWidgetsFromKey, +} from './DatabaseSchemaDetailsUtils'; import i18n from './i18next/LocalUtil'; export interface DatabaseSchemaPageTabProps { @@ -76,7 +80,7 @@ class DatabaseSchemaClassBase { case EntityTabs.TABLE: return [ { - h: 2, + h: 1, i: DetailPageWidgetKeys.DESCRIPTION, w: 6, x: 0, @@ -100,7 +104,7 @@ class DatabaseSchemaClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.TAGS, w: 2, x: 6, @@ -108,7 +112,7 @@ class DatabaseSchemaClassBase { static: false, }, { - h: 2, + h: 1, i: DetailPageWidgetKeys.GLOSSARY_TERMS, w: 2, x: 6, @@ -116,7 +120,7 @@ class DatabaseSchemaClassBase { static: false, }, { - h: 4, + h: 3, i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, w: 2, x: 6, @@ -338,6 +342,10 @@ class DatabaseSchemaClassBase { }, }; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getDatabaseSchemaWidgetsFromKey(widgetConfig); + } } const databaseSchemaClassBase = new DatabaseSchemaClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index f4d57f22def6..7360b9024cda 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -18,8 +18,13 @@ import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/Acti import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; -import { DatabaseSchemaTableTab } from '../components/Database/DatabaseSchema/DatabaseSchemaTableTab/DatabaseSchemaTableTab'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; +import SchemaTablesTab from '../pages/DatabaseSchemaPage/SchemaTablesTab'; import StoredProcedureTab from '../pages/StoredProcedure/StoredProcedureTab'; import { DatabaseSchemaPageTabProps } from './DatabaseSchemaClassBase'; @@ -49,7 +54,7 @@ export const getDataBaseSchemaPageBaseTabs = ({ /> ), key: EntityTabs.TABLE, - children: , + children: , }, { label: ( @@ -109,3 +114,11 @@ export const getDataBaseSchemaPageBaseTabs = ({ }, ]; }; + +export const getDatabaseSchemaWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLES)) { + return ; + } + + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 39f10a84614d..85fa83a0ad33 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -25,8 +25,9 @@ import { EntityTabs } from '../../enums/entity.enum'; import { CreateDomain } from '../../generated/api/domains/createDomain'; import { Domain } from '../../generated/entity/domains/domain'; import { Tab } from '../../generated/system/ui/uiCustomization'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; -import { getDomainDetailTabs } from '../DomainUtils'; +import { getDomainDetailTabs, getDomainWidgetsFromKey } from '../DomainUtils'; import i18n from '../i18next/LocalUtil'; export interface DomainDetailPageTabProps { @@ -37,7 +38,6 @@ export interface DomainDetailPageTabProps { dataProductsCount: number; assetCount: number; activeTab: EntityTabs; - onUpdate: (domain: Domain) => Promise; onAddDataProduct: () => void; onAddSubDomain: (subDomain: CreateDomain) => Promise; isSubDomainsLoading: boolean; @@ -201,6 +201,10 @@ class DomainClassBase { }, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getDomainWidgetsFromKey(widgetConfig); + } } const domainClassBase = new DomainClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index a4cb2f11ff97..55264706a8c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -22,8 +22,11 @@ import { TreeListItem } from '../components/common/DomainSelectableTree/DomainSe import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import DataProductsTab from '../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.component'; import DocumentationTab from '../components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component'; +import { DocumentationEntity } from '../components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface'; import SubDomainsTable from '../components/Domain/SubDomainsTable/SubDomainsTable.component'; import EntitySummaryPanel from '../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import AssetsTabs from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; @@ -34,10 +37,12 @@ import { NO_DATA_PLACEHOLDER, } from '../constants/constants'; import { DOMAIN_TYPE_DATA } from '../constants/Domain.constants'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum'; -import { DataProduct } from '../generated/entity/domains/dataProduct'; import { Domain } from '../generated/entity/domains/domain'; import { EntityReference } from '../generated/entity/type'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { QueryFieldInterface, QueryFilterInterface, @@ -326,7 +331,6 @@ export const getDomainDetailTabs = ({ dataProductsCount, assetCount, activeTab, - onUpdate, onAddDataProduct, isSubDomainsLoading, queryFilter, @@ -348,14 +352,14 @@ export const getDomainDetailTabs = ({ /> ), key: EntityTabs.DOCUMENTATION, - children: ( - onUpdate(data as Domain)} - /> - ), + children: , + // onUpdate(data as Domain)} + // /> + // ), }, ...(!isVersionsView ? [ @@ -451,3 +455,11 @@ export const getDomainDetailTabs = ({ : []), ]; }; + +export const getDomainWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DOCUMENTATION)) { + return ; + } + + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index a52082a16f5d..6c71c5bbffa1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -36,9 +36,13 @@ import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagLabel, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; -import { getPipelineDetailPageTabs } from './PipelineDetailsUtils'; +import { + getPipelineDetailPageTabs, + getPipelineWidgetsFromKey, +} from './PipelineDetailsUtils'; export interface PipelineDetailPageTabProps { description: string; @@ -314,6 +318,10 @@ class PipelineClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getPipelineWidgetsFromKey(widgetConfig); + } } const pipelineClassBase = new PipelineClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index 29589c5b1f57..979c3d2429a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -23,6 +23,7 @@ import { CustomPropertyTable } from '../components/common/CustomPropertyTable/Cu import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; import Lineage from '../components/Lineage/Lineage.component'; import ExecutionsTab from '../components/Pipeline/Execution/Execution.component'; @@ -32,6 +33,7 @@ import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.const import LineageProvider from '../context/LineageProvider/LineageProvider'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { StatusType, TaskStatus } from '../generated/entity/data/pipeline'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { PipelineDetailPageTabProps } from './PipelineClassBase'; // eslint-disable-next-line max-len @@ -237,3 +239,7 @@ export const getPipelineDetailPageTabs = ({ }, ]; }; + +export const getPipelineWidgetsFromKey = (widgetConfig: WidgetConfig) => { + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts index e72ad259e4ca..c40adca9265e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -31,9 +31,10 @@ import { } from '../generated/entity/data/searchIndex'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../generated/type/tagLabel'; - +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; +import { getSearchIndexWidgetsFromKey } from './SearchIndexUtils'; export interface SearchIndexDetailPageTabProps { labelMap?: Record; @@ -325,6 +326,10 @@ class SearchIndexClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getSearchIndexWidgetsFromKey(widgetConfig); + } } const searchIndexClassBase = new SearchIndexClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx index 7e0ff71d7885..fe3fcd513319 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx @@ -12,9 +12,12 @@ */ import { uniqueId } from 'lodash'; +import React from 'react'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; import { SearchIndexField } from '../generated/entity/data/searchIndex'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.FIELDS},${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; @@ -95,3 +98,7 @@ export const getSearchIndexDetailsPageDefaultLayout = (tab: EntityTabs) => { return []; } }; + +export const getSearchIndexWidgetsFromKey = (widgetConfig: WidgetConfig) => { + return ; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index 6871ba75da4f..8b9290d8eb1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -25,9 +25,13 @@ import { StoredProcedure } from '../generated/entity/data/storedProcedure'; import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; -import { getStoredProcedureDetailsPageTabs } from './StoredProceduresUtils'; +import { + getStoredProcedureDetailsPageTabs, + getStoredProcedureWidgetsFromKey, +} from './StoredProceduresUtils'; export interface StoredProcedureDetailPageTabProps { activeTab: EntityTabs; @@ -92,7 +96,7 @@ class StoredProcedureClassBase { }, { h: 7, - i: DetailPageWidgetKeys.TOPIC_SCHEMA, + i: DetailPageWidgetKeys.STORED_PROCEDURE_CODE, w: 6, x: 0, y: 0, @@ -280,6 +284,10 @@ class StoredProcedureClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getStoredProcedureWidgetsFromKey(widgetConfig); + } } const storedProcedureClassBase = new StoredProcedureClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index fb7b3651b27f..b06a0a5ad759 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -15,84 +15,21 @@ import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; -import { StoredProcedureGenericTab } from '../components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import { StoredProcedureCodeCard } from '../components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import LineageProvider from '../context/LineageProvider/LineageProvider'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { StoredProcedureDetailPageTabProps } from './StoredProcedureBase'; // eslint-disable-next-line max-len export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}, ${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; -export const getStoredProceduresPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } -}; - export const getStoredProcedureDetailsPageTabs = ({ activeTab, feedCount, @@ -116,7 +53,7 @@ export const getStoredProcedureDetailsPageTabs = ({ /> ), key: EntityTabs.CODE, - children: , + children: , }, { label: ( @@ -173,3 +110,13 @@ export const getStoredProcedureDetailsPageTabs = ({ }, ]; }; + +export const getStoredProcedureWidgetsFromKey = ( + widgetConfig: WidgetConfig +): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE)) { + return ; + } else { + return ; + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 30a75cf359cc..ce888a2cfd92 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -34,9 +34,13 @@ import { import { Tab } from '../generated/system/ui/uiCustomization'; import { TestSummary } from '../generated/tests/testCase'; import { FeedCounts } from '../interface/feed.interface'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; -import { getTableDetailPageBaseTabs } from './TableUtils'; +import { + getTableDetailPageBaseTabs, + getTableWidgetFromKey, +} from './TableUtils'; export interface TableDetailPageTabProps { queryCount: number; @@ -331,6 +335,10 @@ class TableClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getTableWidgetFromKey(widgetConfig); + } } const tableClassBase = new TableClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 2291d12c28c3..59f5ea8b1f0a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -157,9 +157,20 @@ import { ReactComponent as IconUnknown } from '../assets/svg/data-type-icon/unkn import { ReactComponent as IconVarchar } from '../assets/svg/data-type-icon/varchar.svg'; import { ReactComponent as IconVariant } from '../assets/svg/data-type-icon/variant.svg'; import { ReactComponent as IconXML } from '../assets/svg/data-type-icon/xml.svg'; -import { TableGenericTab } from '../components/Database/TableGenericTab/TableGenericTab'; -import { Joined } from '../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import SchemaTable from '../components/Database/SchemaTable/SchemaTable.component'; +import { StoredProcedureCodeCard } from '../components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { PageType } from '../generated/system/ui/uiCustomization'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; +import { + FrequentlyJoinedTables, + Joined, +} from '../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; +import { PartitionedKeys } from '../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; import ConstraintIcon from '../pages/TableDetailsPageV1/TableConstraints/ConstraintIcon'; +import TableConstraints from '../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; export const getUsagePercentile = (pctRank: number, isLiteral = false) => { const percentile = Math.round(pctRank * 10) / 10; @@ -760,7 +771,7 @@ export const getTableDetailPageBaseTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: , + children: , }, { label: ( @@ -1055,3 +1066,27 @@ export const getColumnOptionsFromTableColumn = (columns: Column[]) => { return options; }; + +export const getTableWidgetFromKey = ( + widgetConfig: WidgetConfig +): JSX.Element | null => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.TABLE_CONSTRAINTS) + ) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES) + ) { + return ; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS)) { + return ; + } else if ( + widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) + ) { + return ; + } else { + return ; + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts index 456b1d628be9..5601bbd260a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -23,9 +23,13 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { Topic } from '../generated/entity/data/topic'; import { Tab } from '../generated/system/ui/uiCustomization'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; -import { getTopicDetailsPageTabs } from './TopicDetailsUtils'; +import { + getTopicDetailsPageTabs, + getTopicWidgetsFromKey, +} from './TopicDetailsUtils'; export interface TopicDetailPageTabProps { schemaCount: number; @@ -302,6 +306,10 @@ class TopicClassBase { CUSTOM_PROPERTIES_WIDGET, ]; } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getTopicWidgetsFromKey(widgetConfig); + } } const topicClassBase = new TopicClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx index ff320378ea63..6b2c742c506d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx @@ -14,9 +14,14 @@ import React from 'react'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; -import { TopicSchemaTab } from '../components/Topic/TopicSchemaTab/TopicSchemaTab'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import TopicSchemaFields from '../components/Topic/TopicSchema/TopicSchema'; import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import i18n from './i18next/LocalUtil'; import { TopicDetailPageTabProps } from './TopicClassBase'; @@ -42,7 +47,7 @@ export const getTopicDetailsPageTabs = ({ /> ), key: EntityTabs.SCHEMA, - children: , + children: , }, { label: ( @@ -96,3 +101,11 @@ export const getTopicDetailsPageTabs = ({ }, ]; }; + +export const getTopicWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { + return ; + } else { + return ; + } +}; From 5ea86fece988593916a1db550faae84f0e1438ce Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:06:42 +0530 Subject: [PATCH 30/63] update --- .../ContainerChildren.test.tsx | 13 +- .../ContainerChildren/ContainerChildren.tsx | 48 ++- .../ContainerWidget/ContainerWidget.tsx | 76 ++++ .../Customization/GenericTab/GenericTab.tsx | 2 +- .../GenericWidget/GenericWidget.tsx | 2 +- .../DashboardDetails.component.tsx | 12 +- .../DataModels/DataModelDetails.component.tsx | 161 +++----- .../DataModels/DataModelDetails.interface.tsx | 5 - .../CommonWidgets/CommonWidgets.tsx | 8 +- .../StoredProcedureGenericTab.tsx | 89 ----- .../DomainDetailsPage.component.tsx | 12 +- .../GlossaryDetails.component.tsx | 14 +- .../GlossaryTermsV1.component.tsx | 16 +- .../PipelineDetails.component.tsx | 363 ++---------------- .../PipelineTaskTab/PipelineTaskTab.tsx | 343 +++++++++++++++++ .../TopicDetails/TopicDetails.component.tsx | 12 +- .../src/pages/ContainerPage/ContainerPage.tsx | 172 +-------- .../DataModelPage/DataModelPage.component.tsx | 60 +-- .../DatabaseDetailsPage.tsx | 12 +- .../DatabaseSchemaPage.component.tsx | 12 +- .../SearchIndexDetailsPage.tsx | 258 +++---------- .../SearchIndexFieldsTab.interface.ts | 24 -- .../SearchIndexFieldsTab.test.tsx | 40 +- .../SearchIndexFieldsTab.tsx | 76 ++-- .../StoredProcedure/StoredProcedurePage.tsx | 12 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 12 +- .../ui/src/utils/ContainerDetailUtils.tsx | 137 ++----- .../ui/src/utils/ContainerDetailsClassBase.ts | 126 +++--- .../CustomizeDetailPage.ts | 2 +- .../utils/CustomizePage/CustomizePageUtils.ts | 46 +++ .../ui/src/utils/DashboardDataModelBase.ts | 112 +++--- .../ui/src/utils/DashboardDataModelUtils.tsx | 37 +- .../ui/src/utils/DashboardDetailsClassBase.ts | 108 +++--- .../ui/src/utils/DashboardDetailsUtils.tsx | 7 +- .../ui/src/utils/Database/Database.util.tsx | 7 +- .../src/utils/Database/DatabaseClassBase.ts | 110 +++--- .../ui/src/utils/DatabaseSchemaClassBase.ts | 177 +++------ .../src/utils/DatabaseSchemaDetailsUtils.tsx | 7 +- .../ui/src/utils/Domain/DomainClassBase.ts | 78 ++-- .../resources/ui/src/utils/DomainUtils.tsx | 4 +- .../utils/GlossaryTerm/GlossaryTermUtil.tsx | 47 --- .../ui/src/utils/PipelineClassBase.ts | 145 +++---- .../ui/src/utils/PipelineDetailsUtils.tsx | 116 +----- .../src/utils/SearchIndexDetailsClassBase.ts | 143 +++---- .../ui/src/utils/SearchIndexUtils.tsx | 226 +++++++---- .../ui/src/utils/StoredProcedureBase.ts | 110 +++--- .../ui/src/utils/StoredProceduresUtils.tsx | 7 +- .../resources/ui/src/utils/TableClassBase.ts | 142 ++++--- .../resources/ui/src/utils/TableUtils.tsx | 12 +- .../resources/ui/src/utils/TopicClassBase.ts | 110 +++--- .../ui/src/utils/TopicDetailsUtils.tsx | 9 +- 51 files changed, 1699 insertions(+), 2180 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx index 98cec2d651b9..41a57ab49257 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx @@ -33,11 +33,6 @@ const mockChildrenList = [ }, ]; -const mockDataProps = { - childrenList: mockChildrenList, - fetchChildren: mockFetchChildren, -}; - jest.mock('../../GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => ({ data: { children: mockChildrenList }, @@ -57,7 +52,7 @@ describe('ContainerChildren', () => { it('Should call fetch container function on load', () => { render( - + ); @@ -67,7 +62,7 @@ describe('ContainerChildren', () => { it('Should render table with correct columns', () => { render( - + ); @@ -79,7 +74,7 @@ describe('ContainerChildren', () => { it('Should render container names as links', () => { render( - + ); @@ -99,7 +94,7 @@ describe('ContainerChildren', () => { it('Should render container descriptions as rich text', () => { render( - + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx index 9af8f78b8d40..2d058e786b1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx @@ -12,33 +12,29 @@ */ import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; -import React, { FC, useEffect, useMemo } from 'react'; +import { AxiosError } from 'axios'; +import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { getEntityDetailsPath } from '../../../constants/constants'; -import { EntityType } from '../../../enums/entity.enum'; -import { Container } from '../../../generated/entity/data/container'; +import { EntityType, TabSpecificField } from '../../../enums/entity.enum'; import { EntityReference } from '../../../generated/type/entityReference'; +import { useFqn } from '../../../hooks/useFqn'; +import { getContainerByName } from '../../../rest/storageAPI'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import Table from '../../common/Table/Table'; -import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; -interface ContainerChildrenProps { - isLoading?: boolean; - fetchChildren: () => void; -} - -const ContainerChildren: FC = ({ - isLoading, - fetchChildren, -}) => { +const ContainerChildren: FC = () => { const { t } = useTranslation(); - const { data } = useGenericContext(); - const { children: childrenList } = useMemo(() => { - return data ?? {}; - }, [data?.children]); + + const { fqn: decodedContainerName } = useFqn(); + const [isChildrenLoading, setIsChildrenLoading] = useState(false); + const [containerChildrenData, setContainerChildrenData] = useState< + EntityReference[] + >([]); const columns: ColumnsType = useMemo( () => [ @@ -84,6 +80,20 @@ const ContainerChildren: FC = ({ [] ); + const fetchChildren = async () => { + setIsChildrenLoading(true); + try { + const { children } = await getContainerByName(decodedContainerName, { + fields: TabSpecificField.CHILDREN, + }); + setContainerChildrenData(children ?? []); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsChildrenLoading(false); + } + }; + useEffect(() => { fetchChildren(); }, []); @@ -93,8 +103,8 @@ const ContainerChildren: FC = ({ bordered columns={columns} data-testid="container-list-table" - dataSource={childrenList} - loading={isLoading} + dataSource={containerChildrenData} + loading={isChildrenLoading} locale={{ emptyText: , }} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx new file mode 100644 index 000000000000..f9fd3034a8da --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2025 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 { isEmpty } from 'lodash'; +import React, { useMemo } from 'react'; +import { Container } from '../../../generated/entity/data/container'; +import { useFqn } from '../../../hooks/useFqn'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; +import ContainerChildren from '../ContainerChildren/ContainerChildren'; +import ContainerDataModel from '../ContainerDataModel/ContainerDataModel'; + +export const ContainerWidget = () => { + const { + data: containerData, + permissions, + onUpdate, + } = useGenericContext(); + const { fqn: decodedContainerName } = useFqn(); + + const { + editDescriptionPermission, + editGlossaryTermsPermission, + editTagsPermission, + deleted, + } = useMemo(() => { + return { + editDescriptionPermission: + permissions.EditAll || + permissions.EditDescription || + permissions.EditCustomFields, + editGlossaryTermsPermission: + permissions.EditAll || permissions.EditGlossaryTerms, + editTagsPermission: permissions.EditAll || permissions.EditTags, + deleted: containerData?.deleted, + }; + }, [permissions, containerData]); + + const isDataModelEmpty = useMemo( + () => isEmpty(containerData?.dataModel), + [containerData] + ); + + const handleUpdateDataModel = async ( + updatedDataModel: Container['dataModel'] + ) => { + await onUpdate({ + ...containerData, + dataModel: updatedDataModel, + }); + }; + + if (isDataModelEmpty) { + return ; + } + + return ( + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index b17f31e2237f..e56fed197e15 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -31,7 +31,7 @@ interface GenericTabProps { export const GenericTab = ({ type }: GenericTabProps) => { const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.CODE } = useParams<{ tab: EntityTabs }>(); + const { tab } = useParams<{ tab: EntityTabs }>(); const layout = useMemo(() => { if (!currentPersonaDocStore) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx index c3c487288230..cb7d4ec94097 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx @@ -618,7 +618,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.CONTAINER} onUpdate={async () => noop()}> - + ); } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.CHARTS_TABLE)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index d543efcc7ba4..be87adbe35f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -33,11 +33,11 @@ import { FeedCounts } from '../../../interface/feed.interface'; import { restoreDashboard } from '../../../rest/dashboardAPI'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; -import dashboardDetailsClassBase from '../../../utils/DashboardDetailsClassBase'; import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import dashboardDetailsClassBase from '../../../utils/DashboardDetailsClassBase'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; @@ -228,7 +228,7 @@ const DashboardDetails = ({ ); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = dashboardDetailsClassBase.getDashboardDetailPageTabs({ editLineagePermission, @@ -245,7 +245,7 @@ const DashboardDetails = ({ labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.DETAILS diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index d1925e3b54d4..2015d640680b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -13,39 +13,37 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty, isUndefined, toString } from 'lodash'; -import { EntityTags } from 'Models'; +import { isUndefined, toString } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; +import { FQN_SEPARATOR_CHAR } from '../../../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, } from '../../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../../constants/ResizablePanel.constants'; import { EntityTabs, EntityType } from '../../../../enums/entity.enum'; import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; -import { TagLabel } from '../../../../generated/type/tagLabel'; +import { Page, PageType } from '../../../../generated/system/ui/page'; +import { useApplicationStore } from '../../../../hooks/useApplicationStore'; import { useFqn } from '../../../../hooks/useFqn'; import { FeedCounts } from '../../../../interface/feed.interface'; import { restoreDataModel } from '../../../../rest/dataModelsAPI'; +import { getDocumentByFQN } from '../../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../../utils/CustomizePage/CustomizePageUtils'; import { getDashboardDataModelDetailPageTabs } from '../../../../utils/DashboardDataModelUtils'; -import { getEntityName } from '../../../../utils/EntityUtils'; -import { getTagsWithoutTier } from '../../../../utils/TableUtils'; -import { createTagObject } from '../../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import { withActivityFeed } from '../../../AppRouter/withActivityFeed'; -import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import { GenericProvider } from '../../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import EntityRightPanel from '../../../Entity/EntityRightPanel/EntityRightPanel'; import { EntityName } from '../../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; import { DataModelDetailsProps } from './DataModelDetails.interface'; -import ModelTab from './ModelTab/ModelTab.component'; const DataModelDetails = ({ updateDataModelDetailsState, @@ -53,11 +51,8 @@ const DataModelDetails = ({ dataModelPermissions, fetchDataModel, handleFollowDataModel, - handleUpdateTags, handleUpdateOwner, handleUpdateTier, - handleUpdateDescription, - handleColumnUpdateDataModel, onUpdateDataModel, handleToggleDelete, onUpdateVote, @@ -66,22 +61,19 @@ const DataModelDetails = ({ const history = useHistory(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedDataModelFQN } = useFqn(); + const [customizedPage, setCustomizedPage] = useState(null); + const { selectedPersona } = useApplicationStore(); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const { deleted, owners, description, version, entityName, tags } = - useMemo(() => { - return { - deleted: dataModelData?.deleted, - owners: dataModelData?.owners, - description: dataModelData?.description, - version: dataModelData?.version, - entityName: getEntityName(dataModelData), - tags: getTagsWithoutTier(dataModelData.tags ?? []), - }; - }, [dataModelData]); + const { deleted, version } = useMemo(() => { + return { + deleted: dataModelData?.deleted, + version: dataModelData?.version, + }; + }, [dataModelData]); const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); @@ -134,11 +126,6 @@ const DataModelDetails = ({ } }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleUpdateTags(updatedTags); - }; - const handleRestoreDataModel = async () => { try { const { version: newVersion } = await restoreDataModel( @@ -167,24 +154,8 @@ const DataModelDetails = ({ [] ); - const { - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, - editLineagePermission, - } = useMemo(() => { + const { editLineagePermission } = useMemo(() => { return { - editDescriptionPermission: - (dataModelPermissions.EditAll || - dataModelPermissions.EditDescription) && - !deleted, - editGlossaryTermsPermission: - (dataModelPermissions.EditGlossaryTerms || - dataModelPermissions.EditAll) && - !deleted, - editTagsPermission: - (dataModelPermissions.EditAll || dataModelPermissions.EditTags) && - !deleted, editLineagePermission: (dataModelPermissions.EditAll || dataModelPermissions.EditLineage) && !deleted, @@ -204,75 +175,9 @@ const DataModelDetails = ({ [onUpdateDataModel, dataModelData] ); - const modelComponent = useMemo(() => { - return ( - -
- - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - (dataModelPermissions.EditAll || - dataModelPermissions.EditCustomFields) && - !deleted - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.DASHBOARD_DATA_MODEL} - selectedTags={tags} - viewAllPermission={dataModelPermissions.ViewAll} - onExtensionUpdate={handelExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ); - }, [ - decodedDataModelFQN, - dataModelData, - description, - decodedDataModelFQN, - editTagsPermission, - editGlossaryTermsPermission, - deleted, - editDescriptionPermission, - entityName, - handleTagSelection, - handleColumnUpdateDataModel, - handleUpdateDescription, - ]); - const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const allTabs = getDashboardDataModelDetailPageTabs({ - modelComponent, feedCount, activeTab, handleFeedCount, @@ -283,19 +188,43 @@ const DataModelDetails = ({ handelExtensionUpdate, getEntityFeedCount, fetchDataModel, + labelMap: tabLabelMap, }); - return allTabs; + return getDetailsTabWithNewLabel( + allTabs, + customizedPage?.tabs, + EntityTabs.TASKS + ); }, [ feedCount.conversationCount, feedCount.totalTasksCount, dataModelData?.sql, - modelComponent, deleted, handleFeedCount, editLineagePermission, ]); + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find( + (p: Page) => p.pageType === PageType.DashboardDataModel + ) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + return ( void; handleFollowDataModel: () => Promise; - handleUpdateTags: (selectedTags?: EntityTags[]) => Promise; handleUpdateOwner: (owner?: EntityReference[]) => Promise; handleUpdateTier: (tier?: Tag) => Promise; - handleUpdateDescription: (value: string) => Promise; - handleColumnUpdateDataModel: (updatedDataModel: Column[]) => Promise; onUpdateVote: (data: QueryVote, id: string) => Promise; onUpdateDataModel: ( updatedDataModel: DashboardDataModel, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index 68af1ca360b2..3b774fdff4fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -52,9 +52,13 @@ interface GenericEntity interface CommonWidgetsProps { widgetConfig: WidgetConfig; + entityType: EntityType; } -export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { +export const CommonWidgets = ({ + widgetConfig, + entityType, +}: CommonWidgetsProps) => { const { data, type, onUpdate, permissions } = useGenericContext(); @@ -215,7 +219,7 @@ export const CommonWidgets = ({ widgetConfig }: CommonWidgetsProps) => { return ( isRenderedInRightPanel - entityType={EntityType.TABLE} + entityType={entityType as EntityType.TABLE} handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={Boolean(viewAllPermission)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx deleted file mode 100644 index b51d8a279fe9..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureGenericTab/StoredProcedureGenericTab.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 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 React, { useMemo } from 'react'; -import RGL, { WidthProvider } from 'react-grid-layout'; -import { useParams } from 'react-router-dom'; -import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../../../enums/entity.enum'; -import { Page, PageType, Tab } from '../../../generated/system/ui/page'; -import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; -import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; -import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import { getDefaultWidgetForTab } from '../../../utils/CustomizePage/CustomizePageUtils'; -import storedProcedureClassBase from '../../../utils/StoredProcedureBase'; -import { CommonWidgets } from '../../DataAssets/CommonWidgets/CommonWidgets'; -import { StoredProcedureCodeCard } from '../StoredProcedureCodeCard/StoredProcedureCodeCard'; - -const ReactGridLayout = WidthProvider(RGL); - -export const StoredProcedureGenericTab = () => { - const { currentPersonaDocStore } = useCustomizeStore(); - const { tab = EntityTabs.CODE } = useParams<{ tab: EntityTabs }>(); - - const layout = useMemo(() => { - if (!currentPersonaDocStore) { - return storedProcedureClassBase.getDefaultLayout(tab); - } - - const page = currentPersonaDocStore?.data?.pages?.find( - (p: Page) => p.pageType === PageType.StoredProcedure - ); - - if (page) { - return page.tabs.find((t: Tab) => t.id === tab)?.layout; - } else { - return getDefaultWidgetForTab(PageType.StoredProcedure, tab); - } - }, [currentPersonaDocStore, tab]); - - const widgets = useMemo(() => { - const getWidgetFromKeyInternal = ( - widgetConfig: WidgetConfig - ): JSX.Element | null => { - if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) - ) { - return ; - } else { - return ; - } - }; - - return layout.map((widget: WidgetConfig) => ( -
- {getWidgetFromKeyInternal(widget)} -
- )); - }, [layout]); - - // call the hook to set the direction of the grid layout - useGridLayoutDirection(); - - return ( - <> - - {widgets} - - - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx index b92d83b097ad..dadf69217e22 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx @@ -79,6 +79,10 @@ import { searchData } from '../../../rest/miscAPI'; import { searchQuery } from '../../../rest/searchAPI'; import { formatDomainsResponse } from '../../../utils/APIUtils'; import { getIsErrorMatch } from '../../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; import domainClassBase from '../../../utils/Domain/DomainClassBase'; import { getQueryFilterForDomain, @@ -87,10 +91,6 @@ import { import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import Fqn from '../../../utils/Fqn'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getDomainDetailsPath, @@ -512,7 +512,7 @@ const DomainDetailsPage = ({ }, [domainFqn]); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = domainClassBase.getDomainDetailPageTabs({ domain, @@ -538,7 +538,7 @@ const DomainDetailsPage = ({ labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.DOCUMENTATION diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index db484b5ddde4..2cafb7054c62 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -37,16 +37,16 @@ import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStor import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import customizeGlossaryPageClassBase from '../../../utils/CustomizeGlossaryPage/CustomizeGlossaryPage'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityVersionByField, getEntityVersionTags, } from '../../../utils/EntityVersionUtils'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, - getWidgetFromKey, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; +import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; @@ -278,7 +278,7 @@ const GlossaryDetails = ({ }, [permissions, glossary, termsLoading, widgets]); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const items = [ { @@ -323,7 +323,7 @@ const GlossaryDetails = ({ : []), ]; - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( items, customizedPage?.tabs, EntityTabs.TERMS diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index ae64d9096b37..0d3c7970ca30 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -44,11 +44,11 @@ import { MOCK_GLOSSARY_NO_PERMISSIONS } from '../../../mocks/Glossary.mock'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { searchData } from '../../../rest/miscAPI'; import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils'; -import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils'; import { getGlossaryTermsVersionsPath } from '../../../utils/RouterUtils'; import { @@ -178,7 +178,7 @@ const GlossaryTermsV1 = ({ }; const tabItems = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const items = [ { @@ -303,7 +303,11 @@ const GlossaryTermsV1 = ({ : []), ]; - return getGlossaryTermDetailTabs(items, customizedPage?.tabs); + return getDetailsTabWithNewLabel( + items, + customizedPage?.tabs, + EntityTabs.TERMS + ); }, [ customizedPage?.tabs, glossaryTerm, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx index e62cf75d143b..f1347244792d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.component.tsx @@ -11,70 +11,43 @@ * limitations under the License. */ -import Icon from '@ant-design/icons/lib/components/Icon'; -import { Card, Col, Row, Tabs, Typography } from 'antd'; -import { ColumnsType } from 'antd/lib/table'; +import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { compare } from 'fast-json-patch'; -import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; -import { EntityTags, TagFilterOptions } from 'Models'; +import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, useHistory, useParams } from 'react-router-dom'; -import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg'; +import { useHistory, useParams } from 'react-router-dom'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; -import { - DATA_ASSET_ICON_DIMENSION, - getEntityDetailsPath, - NO_DATA_PLACEHOLDER, -} from '../../../constants/constants'; +import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { PIPELINE_TASK_TABS } from '../../../constants/pipeline.constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; -import { - Pipeline, - PipelineStatus, - TagLabel, - Task, -} from '../../../generated/entity/data/pipeline'; +import { Pipeline, TagLabel } from '../../../generated/entity/data/pipeline'; import { Page } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/uiCustomization'; -import { TagSource } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { FeedCounts } from '../../../interface/feed.interface'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restorePipeline } from '../../../rest/pipelineAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; -import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import pipelineClassBase from '../../../utils/PipelineClassBase'; -import { columnFilterIcon } from '../../../utils/TableColumn.util'; -import { - getAllTags, - searchTagInData, -} from '../../../utils/TableTags/TableTags.utils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; -import TableDescription from '../../Database/TableDescription/TableDescription.component'; -import TableTags from '../../Database/TableTags/TableTags.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; -import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import TasksDAGView from '../TasksDAGView/TasksDAGView'; import './pipeline-details.style.less'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; @@ -86,7 +59,6 @@ const PipelineDetails = ({ followPipelineHandler, unFollowPipelineHandler, settingsUpdateHandler, - taskUpdateHandler, versionHandler, pipelineFQN, onUpdateVote, @@ -98,61 +70,35 @@ const PipelineDetails = ({ const { t } = useTranslation(); const { currentUser, selectedPersona } = useApplicationStore(); const userID = currentUser?.id ?? ''; - const { - deleted, - owners, - description, - pipelineStatus, - entityName, - tier, - tags, - followers, - } = useMemo(() => { - return { - deleted: pipelineDetails.deleted, - owners: pipelineDetails.owners, - serviceType: pipelineDetails.serviceType, - description: pipelineDetails.description, - version: pipelineDetails.version, - pipelineStatus: pipelineDetails.pipelineStatus, - tier: getTierTags(pipelineDetails.tags ?? []), - tags: getTagsWithoutTier(pipelineDetails.tags ?? []), - entityName: getEntityName(pipelineDetails), - followers: pipelineDetails.followers ?? [], - }; - }, [pipelineDetails]); + const { deleted, owners, description, entityName, tier, followers } = + useMemo(() => { + return { + deleted: pipelineDetails.deleted, + owners: pipelineDetails.owners, + serviceType: pipelineDetails.serviceType, + description: pipelineDetails.description, + version: pipelineDetails.version, + pipelineStatus: pipelineDetails.pipelineStatus, + tier: getTierTags(pipelineDetails.tags ?? []), + tags: getTagsWithoutTier(pipelineDetails.tags ?? []), + entityName: getEntityName(pipelineDetails), + followers: pipelineDetails.followers ?? [], + }; + }, [pipelineDetails]); // local state variables const [customizedPage, setCustomizedPage] = useState(null); - const [editTask, setEditTask] = useState<{ - task: Task; - index: number; - }>(); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [selectedExecution] = useState( - pipelineStatus - ); - const [pipelinePermissions, setPipelinePermissions] = useState( DEFAULT_ENTITY_PERMISSION ); - const [activeTab, setActiveTab] = useState(PIPELINE_TASK_TABS.LIST_VIEW); - const { getEntityPermission } = usePermissionProvider(); - const tasksInternal = useMemo( - () => - pipelineDetails.tasks - ? pipelineDetails.tasks.map((t) => ({ ...t, tags: t.tags ?? [] })) - : [], - [pipelineDetails.tasks] - ); - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -187,29 +133,6 @@ const PipelineDetails = ({ [followers, userID] ); - const onTaskUpdate = async (taskDescription: string) => { - if (editTask) { - const updatedTasks = [...(pipelineDetails.tasks ?? [])]; - - const updatedTask = { - ...editTask.task, - description: taskDescription, - }; - updatedTasks[editTask.index] = updatedTask; - - const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks }; - const jsonPatch = compare(pipelineDetails, updatedPipeline); - await taskUpdateHandler(jsonPatch); - setEditTask(undefined); - } else { - setEditTask(undefined); - } - }; - - const closeEditTaskModal = (): void => { - setEditTask(undefined); - }; - const onOwnerUpdate = useCallback( async (newOwners?: Pipeline['owners']) => { const updatedPipelineDetails = { @@ -276,45 +199,6 @@ const PipelineDetails = ({ } }, [isFollowing, followPipelineHandler, unFollowPipelineHandler]); - const handleTableTagSelection = async ( - selectedTags: EntityTags[], - editColumnTag: Task - ) => { - const prevTags = editColumnTag.tags?.filter((tag) => - selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN) - ); - - const newTags = createTagObject( - selectedTags.filter( - (selectedTag) => - !editColumnTag.tags?.some((tag) => tag.tagFQN === selectedTag.tagFQN) - ) - ); - - const updatedTask = { - ...editColumnTag, - tags: [...(prevTags as TagLabel[]), ...newTags], - } as Task; - - const updatedTasks: Task[] = [...(pipelineDetails.tasks ?? [])].map( - (task) => (task.name === editColumnTag.name ? updatedTask : task) - ); - - const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks }; - const jsonPatch = compare(pipelineDetails, updatedPipeline); - - await taskUpdateHandler(jsonPatch); - }; - - const tagFilter = useMemo(() => { - const tags = getAllTags(tasksInternal); - - return groupBy(uniqBy(tags, 'value'), (tag) => tag.source) as Record< - TagSource, - TagFilterOptions[] - >; - }, [tasksInternal]); - const { editTagsPermission, editGlossaryTermsPermission, @@ -345,133 +229,6 @@ const PipelineDetails = ({ [pipelinePermissions, deleted] ); - const taskColumns: ColumnsType = useMemo( - () => [ - { - key: t('label.name'), - dataIndex: 'name', - title: t('label.name'), - width: 220, - fixed: 'left', - sorter: getColumnSorter('name'), - render: (_, record) => - isEmpty(record.sourceUrl) ? ( - {getEntityName(record)} - ) : ( - -
- {getEntityName(record)} - - -
- - ), - }, - { - key: t('label.type'), - dataIndex: 'taskType', - width: 180, - title: t('label.type'), - render: (text) => ( - {text || NO_DATA_PLACEHOLDER} - ), - }, - { - key: t('label.description'), - dataIndex: 'description', - width: 350, - title: t('label.description'), - render: (_, record, index) => ( - setEditTask({ task: record, index })} - /> - ), - }, - { - title: t('label.owner-plural'), - dataIndex: 'owners', - key: 'owners', - width: 120, - accessor: 'owner', - filterIcon: columnFilterIcon, - render: (owner) => , - }, - { - title: t('label.tag-plural'), - dataIndex: 'tags', - key: 'tags', - accessor: 'tags', - width: 300, - filterIcon: columnFilterIcon, - render: (tags, record, index) => ( - - entityFqn={pipelineFQN} - entityType={EntityType.PIPELINE} - handleTagSelection={handleTableTagSelection} - hasTagEditAccess={editTagsPermission} - index={index} - isReadOnly={deleted} - record={record} - tags={tags} - type={TagSource.Classification} - /> - ), - filters: tagFilter.Classification, - filterDropdown: ColumnFilter, - onFilter: searchTagInData, - }, - { - title: t('label.glossary-term-plural'), - dataIndex: 'tags', - key: 'glossary', - accessor: 'tags', - width: 300, - filterIcon: columnFilterIcon, - filters: tagFilter.Glossary, - filterDropdown: ColumnFilter, - onFilter: searchTagInData, - render: (tags, record, index) => ( - - entityFqn={pipelineFQN} - entityType={EntityType.PIPELINE} - handleTagSelection={handleTableTagSelection} - hasTagEditAccess={editGlossaryTermsPermission} - index={index} - isReadOnly={deleted} - record={record} - tags={tags} - type={TagSource.Glossary} - /> - ), - }, - ], - [ - deleted, - editTask, - editTagsPermission, - editGlossaryTermsPermission, - getEntityName, - handleTableTagSelection, - editDescriptionPermission, - ] - ); - const handleTabChange = (tabValue: string) => { if (tabValue !== tab) { history.push({ @@ -500,77 +257,40 @@ const PipelineDetails = ({ [] ); - const tasksDAGView = useMemo( - () => - !isEmpty(pipelineDetails.tasks) && !isUndefined(pipelineDetails.tasks) ? ( - -
- -
-
- ) : ( - - {t('server.no-task-available')} - - ), - [pipelineDetails, selectedExecution] - ); - const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = pipelineClassBase.getPipelineDetailPageTabs({ - description: description ?? '', - activeTab, feedCount: { totalCount: feedCount.totalCount, }, - deleted: deleted ?? false, - owners: owners ?? [], - entityName: entityName ?? '', - pipelineFQN: pipelineFQN ?? '', - pipelineDetails: pipelineDetails ?? {}, - tasksDAGView, - taskColumns: taskColumns ?? [], - tasksInternal: tasksInternal ?? [], + getEntityFeedCount, handleFeedCount, - handleTagSelection, onExtensionUpdate, - onDescriptionUpdate, - editDescriptionPermission: editDescriptionPermission ?? false, - editTagsPermission: editTagsPermission ?? false, - editGlossaryTermsPermission: editGlossaryTermsPermission ?? false, - editLineagePermission: editLineagePermission ?? false, - editCustomAttributePermission: editCustomAttributePermission ?? false, - viewAllPermission: viewAllPermission ?? false, - tab: tab ?? EntityTabs.TASKS, - getEntityFeedCount, - tags, - setActiveTab, + pipelineDetails, + pipelineFQN, + viewAllPermission, + editLineagePermission, + editCustomAttributePermission, + deleted: Boolean(pipelineDetails.deleted), fetchPipeline, + tab, labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.TASKS ); }, [ description, - activeTab, feedCount.totalCount, deleted, owners, entityName, pipelineFQN, pipelineDetails, - tasksDAGView, - taskColumns, - tasksInternal, handleFeedCount, handleTagSelection, onExtensionUpdate, @@ -648,21 +368,6 @@ const PipelineDetails = ({ - {editTask && ( - - )} - <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx new file mode 100644 index 000000000000..15b7d94130f0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx @@ -0,0 +1,343 @@ +/* + * Copyright 2025 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 { groupBy, isUndefined, uniqBy } from 'lodash'; + +import Icon from '@ant-design/icons/lib/components/Icon'; +import { Card, Col, Radio, Row, Table, Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { isEmpty } from 'lodash'; +import { EntityTags, TagFilterOptions } from 'Models'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { ReactComponent as ExternalLinkIcon } from '../../../assets/svg/external-links.svg'; +import { + DATA_ASSET_ICON_DIMENSION, + NO_DATA_PLACEHOLDER, +} from '../../../constants/constants'; +import { PIPELINE_TASK_TABS } from '../../../constants/pipeline.constants'; +import { EntityType } from '../../../enums/entity.enum'; +import { + Pipeline, + PipelineStatus, + Task, +} from '../../../generated/entity/data/pipeline'; +import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; +import { useFqn } from '../../../hooks/useFqn'; +import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; +import { columnFilterIcon } from '../../../utils/TableColumn.util'; +import { + getAllTags, + searchTagInData, +} from '../../../utils/TableTags/TableTags.utils'; +import { createTagObject } from '../../../utils/TagsUtils'; +import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; +import { ColumnFilter } from '../../Database/ColumnFilter/ColumnFilter.component'; +import TableDescription from '../../Database/TableDescription/TableDescription.component'; +import TableTags from '../../Database/TableTags/TableTags.component'; +import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import TasksDAGView from '../TasksDAGView/TasksDAGView'; + +export const PipelineTaskTab = () => { + const { + data: pipelineDetails, + permissions, + onUpdate, + } = useGenericContext(); + const { fqn: pipelineFQN } = useFqn(); + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(PIPELINE_TASK_TABS.LIST_VIEW); + const [selectedExecution] = useState( + pipelineDetails.pipelineStatus + ); + const [editTask, setEditTask] = useState<{ + task: Task; + index: number; + }>(); + + const { deleted } = pipelineDetails ?? {}; + + const { + editDescriptionPermission, + editTagsPermission, + editGlossaryTermsPermission, + } = useMemo( + () => ({ + editDescriptionPermission: + permissions?.EditAll || permissions?.EditDescription, + editTagsPermission: permissions?.EditAll || permissions?.EditTags, + editGlossaryTermsPermission: + permissions?.EditAll || permissions?.EditGlossaryTerms, + }), + [permissions] + ); + + const tasksInternal = useMemo( + () => + pipelineDetails.tasks + ? pipelineDetails.tasks.map((t) => ({ ...t, tags: t.tags ?? [] })) + : [], + [pipelineDetails.tasks] + ); + + const tagFilter = useMemo(() => { + const tags = getAllTags(tasksInternal); + + return groupBy(uniqBy(tags, 'value'), (tag) => tag.source) as Record< + TagSource, + TagFilterOptions[] + >; + }, [tasksInternal]); + + const tasksDAGView = useMemo( + () => + !isEmpty(pipelineDetails.tasks) && !isUndefined(pipelineDetails.tasks) ? ( + +
+ +
+
+ ) : ( + + {t('server.no-task-available')} + + ), + [pipelineDetails, selectedExecution] + ); + + const closeEditTaskModal = (): void => { + setEditTask(undefined); + }; + + const onTaskUpdate = async (taskDescription: string) => { + if (editTask) { + const updatedTasks = [...(pipelineDetails.tasks ?? [])]; + + const updatedTask = { + ...editTask.task, + description: taskDescription, + }; + updatedTasks[editTask.index] = updatedTask; + + const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks }; + await onUpdate(updatedPipeline); + setEditTask(undefined); + } else { + setEditTask(undefined); + } + }; + + const handleTableTagSelection = async ( + selectedTags: EntityTags[], + editColumnTag: Task + ) => { + const prevTags = editColumnTag.tags?.filter((tag) => + selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN) + ); + + const newTags = createTagObject( + selectedTags.filter( + (selectedTag) => + !editColumnTag.tags?.some((tag) => tag.tagFQN === selectedTag.tagFQN) + ) + ); + + const updatedTask = { + ...editColumnTag, + tags: [...(prevTags as TagLabel[]), ...newTags], + } as Task; + + const updatedTasks: Task[] = [...(pipelineDetails.tasks ?? [])].map( + (task) => (task.name === editColumnTag.name ? updatedTask : task) + ); + + const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks }; + await onUpdate(updatedPipeline); + }; + + const taskColumns: ColumnsType = useMemo( + () => [ + { + key: t('label.name'), + dataIndex: 'name', + title: t('label.name'), + width: 220, + fixed: 'left', + sorter: getColumnSorter('name'), + render: (_, record) => + isEmpty(record.sourceUrl) ? ( + {getEntityName(record)} + ) : ( + +
+ {getEntityName(record)} + + +
+ + ), + }, + { + key: t('label.type'), + dataIndex: 'taskType', + width: 180, + title: t('label.type'), + render: (text) => ( + {text || NO_DATA_PLACEHOLDER} + ), + }, + { + key: t('label.description'), + dataIndex: 'description', + width: 350, + title: t('label.description'), + render: (_, record, index) => ( + setEditTask({ task: record, index })} + /> + ), + }, + { + title: t('label.owner-plural'), + dataIndex: 'owners', + key: 'owners', + width: 120, + filterIcon: columnFilterIcon, + render: (owner) => , + }, + { + title: t('label.tag-plural'), + dataIndex: 'tags', + key: 'tags', + width: 300, + filterIcon: columnFilterIcon, + render: (tags, record, index) => ( + + entityFqn={pipelineFQN} + entityType={EntityType.PIPELINE} + handleTagSelection={handleTableTagSelection} + hasTagEditAccess={editTagsPermission} + index={index} + isReadOnly={deleted} + record={record} + tags={tags} + type={TagSource.Classification} + /> + ), + filters: tagFilter.Classification, + filterDropdown: ColumnFilter, + onFilter: searchTagInData, + }, + { + title: t('label.glossary-term-plural'), + dataIndex: 'tags', + key: 'glossary', + width: 300, + filterIcon: columnFilterIcon, + filters: tagFilter.Glossary, + filterDropdown: ColumnFilter, + onFilter: searchTagInData, + render: (tags, record, index) => ( + + entityFqn={pipelineFQN} + entityType={EntityType.PIPELINE} + handleTagSelection={handleTableTagSelection} + hasTagEditAccess={editGlossaryTermsPermission} + index={index} + isReadOnly={deleted} + record={record} + tags={tags} + type={TagSource.Glossary} + /> + ), + }, + ], + [ + deleted, + editTask, + editTagsPermission, + editGlossaryTermsPermission, + handleTableTagSelection, + editDescriptionPermission, + ] + ); + + return ( + +
+ setActiveTab(e.target.value)} + /> + + + + {activeTab === PIPELINE_TASK_TABS.LIST_VIEW ? ( +
+ ) : ( + tasksDAGView + )} + + + {editTask && ( + + )} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index 207d645e483c..4490f5d89cdc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -35,14 +35,14 @@ import { FeedCounts } from '../../../interface/feed.interface'; import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restoreTopic } from '../../../rest/topicsAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; import { getEntityName, getEntityReferenceFromEntity, } from '../../../utils/EntityUtils'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; @@ -297,7 +297,7 @@ const TopicDetails: React.FC = ({ }, [topicPermissions, decodedTopicFQN]); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = topicClassBase.getTopicDetailPageTabs({ schemaCount: topicDetails.messageSchema?.schemaFields?.length ?? 0, @@ -353,7 +353,7 @@ const TopicDetails: React.FC = ({ labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.SCHEMA diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 1bcecb0d8aa5..cfe8b23473ee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -14,7 +14,6 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined, omitBy, toString } from 'lodash'; -import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; @@ -50,7 +49,6 @@ import { Container } from '../../generated/entity/data/container'; import { Page } from '../../generated/system/ui/page'; import { PageType } from '../../generated/system/ui/uiCustomization'; import { Include } from '../../generated/type/include'; -import { TagLabel } from '../../generated/type/tagLabel'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; @@ -70,14 +68,13 @@ import { getFeedCounts, } from '../../utils/CommonUtils'; import containerDetailsClassBase from '../../utils/ContainerDetailsClassBase'; -import { getEntityName } from '../../utils/EntityUtils'; import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../utils/GlossaryTerm/GlossaryTermUtil'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const ContainerPage = () => { @@ -91,14 +88,9 @@ const ContainerPage = () => { // Local states const [isLoading, setIsLoading] = useState(true); - const [isChildrenLoading, setIsChildrenLoading] = useState(false); const [hasError, setHasError] = useState(false); - const [isEditDescription, setIsEditDescription] = useState(false); const [customizedPage, setCustomizedPage] = useState(null); const [containerData, setContainerData] = useState(); - const [containerChildrenData, setContainerChildrenData] = useState< - Container['children'] - >([]); const [containerPermissions, setContainerPermissions] = useState(DEFAULT_ENTITY_PERMISSION); @@ -143,20 +135,6 @@ const ContainerPage = () => { } }; - const fetchContainerChildren = async () => { - setIsChildrenLoading(true); - try { - const { children } = await getContainerByName(decodedContainerName, { - fields: TabSpecificField.CHILDREN, - }); - setContainerChildrenData(children); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsChildrenLoading(false); - } - }; - const handleFeedCount = useCallback( (data: FeedCounts) => setFeedCount(data), [] @@ -191,40 +169,17 @@ const ContainerPage = () => { } }; - const { - deleted, - owners, - description, - version, - entityName, - isUserFollowing, - tags, - tier, - } = useMemo(() => { + const { deleted, version, isUserFollowing } = useMemo(() => { return { deleted: containerData?.deleted, - owners: containerData?.owners, - description: containerData?.description, version: containerData?.version, - tier: getTierTags(containerData?.tags ?? []), - tags: getTagsWithoutTier(containerData?.tags ?? []), - entityId: containerData?.id, - entityName: getEntityName(containerData), isUserFollowing: containerData?.followers?.some( ({ id }: { id: string }) => id === currentUser?.id ), - followers: containerData?.followers ?? [], - size: containerData?.size ?? 0, - numberOfObjects: containerData?.numberOfObjects ?? 0, - partitioned: containerData?.dataModel?.isPartitioned, - entityFqn: containerData?.fullyQualifiedName ?? '', }; - }, [containerData, currentUser]); + }, [containerData]); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, editLineagePermission, viewBasicPermission, @@ -285,25 +240,6 @@ const ContainerPage = () => { [containerData] ); - const handleUpdateDescription = async (updatedDescription: string) => { - try { - const { description: newDescription, version } = - await handleUpdateContainerData({ - ...(containerData as Container), - description: updatedDescription, - }); - - setContainerData((prev) => ({ - ...(prev as Container), - description: newDescription, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsEditDescription(false); - } - }; const handleUpdateDisplayName = async (data: EntityName) => { if (isUndefined(containerData)) { return; @@ -447,25 +383,6 @@ const ContainerPage = () => { } }; - const handleTagUpdate = useCallback( - async (updatedContainer: Container) => { - if (isUndefined(containerData)) { - return; - } - - try { - const response = await handleUpdateContainerData({ - ...containerData, - tags: updatedContainer.tags, - }); - setContainerData(response); - } catch (error) { - showErrorToast(error as AxiosError); - } - }, - [containerData, handleUpdateContainerData, setContainerData] - ); - const handleExtensionUpdate = useCallback( async (updatedContainer: Container) => { if (isUndefined(containerData)) { @@ -485,26 +402,6 @@ const ContainerPage = () => { [containerData, handleUpdateContainerData, setContainerData] ); - const handleUpdateDataModel = async ( - updatedDataModel: Container['dataModel'] - ) => { - try { - const { dataModel: newDataModel, version } = - await handleUpdateContainerData({ - ...(containerData as Container), - dataModel: updatedDataModel, - }); - - setContainerData((prev) => ({ - ...(prev as Container), - dataModel: newDataModel, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const versionHandler = () => history.push( getVersionPath( @@ -514,23 +411,23 @@ const ContainerPage = () => { ) ); - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); + const handleContainerUpdate = async (updatedData: Container) => { + try { + const updatedContainer = await handleUpdateContainerData(updatedData); + setContainerData((prev) => { + if (!prev) { + return prev; + } - if (updatedTags && containerData) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedContainer = { ...containerData, tags: updatedTags }; - await handleTagUpdate(updatedContainer); + return { ...prev, ...updatedContainer }; + }); + } catch (error) { + showErrorToast(error as AxiosError); } }; - const handleContainerUpdate = async (updatedData: Container) => { - const updatedContainer = await handleUpdateContainerData(updatedData); - setContainerData(updatedContainer); - }; - const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); if (!containerData) { return []; @@ -538,35 +435,22 @@ const ContainerPage = () => { const tabs = containerDetailsClassBase.getContainerDetailPageTabs({ isDataModelEmpty, - description: description ?? '', decodedContainerName, - entityName, - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, - containerChildrenData: containerChildrenData ?? [], - fetchContainerChildren, - isChildrenLoading: isChildrenLoading ?? false, feedCount: feedCount ?? { totalCount: 0 }, getEntityFeedCount, handleFeedCount, tab, deleted: deleted ?? false, - owners: owners ?? [], containerData, - tags: tags ?? [], fetchContainerDetail, labelMap: tabLabelMap, - handleUpdateDescription, - handleUpdateDataModel, - handleTagSelection, handleExtensionUpdate, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.CHILDREN @@ -574,27 +458,13 @@ const ContainerPage = () => { }, [ isDataModelEmpty, containerData, - description, - decodedContainerName, decodedContainerName, - entityName, - editDescriptionPermission, - editTagsPermission, - editGlossaryTermsPermission, - isEditDescription, editLineagePermission, editCustomAttributePermission, viewAllPermission, deleted, - owners, - isChildrenLoading, - tags, feedCount.totalCount, - containerChildrenData, handleFeedCount, - handleUpdateDataModel, - handleUpdateDescription, - handleTagSelection, handleExtensionUpdate, customizedPage?.tabs, ]); @@ -692,7 +562,7 @@ const ContainerPage = () => { /> - data={{ ...containerData, children: containerChildrenData }} + data={containerData} permissions={containerPermissions} type={EntityType.CONTAINER} onUpdate={handleContainerUpdate}> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index e412437e9ad1..1db10c842e34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -14,7 +14,6 @@ import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isUndefined, omitBy } from 'lodash'; -import { EntityTags } from 'Models'; import { default as React, useCallback, @@ -83,7 +82,7 @@ const DataModelsPage = () => { }; }, [dataModelPermissions]); - const { tier, isUserFollowing } = useMemo(() => { + const { isUserFollowing } = useMemo(() => { return { tier: getTierTags(dataModelData?.tags ?? []), isUserFollowing: dataModelData?.followers?.some( @@ -146,24 +145,6 @@ const DataModelsPage = () => { return patchDataModelDetails(dataModelData?.id ?? '', jsonPatch); }; - const handleUpdateDescription = async (updatedDescription: string) => { - try { - const { description: newDescription, version } = - await handleUpdateDataModelData({ - ...(dataModelData as DashboardDataModel), - description: updatedDescription, - }); - - setDataModelData((prev) => ({ - ...(prev as DashboardDataModel), - description: newDescription, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const handleFollowDataModel = async () => { const followerId = currentUser?.id ?? ''; const dataModelId = dataModelData?.id ?? ''; @@ -192,23 +173,6 @@ const DataModelsPage = () => { } }; - const handleUpdateTags = async (selectedTags: Array = []) => { - try { - const { tags: newTags, version } = await handleUpdateDataModelData({ - ...(dataModelData as DashboardDataModel), - tags: [...(tier ? [tier] : []), ...selectedTags], - }); - - setDataModelData((prev) => ({ - ...(prev as DashboardDataModel), - tags: newTags, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const handleUpdateOwner = useCallback( async (updatedOwners?: DashboardDataModel['owners']) => { try { @@ -247,25 +211,6 @@ const DataModelsPage = () => { } }; - const handleColumnUpdateDataModel = async ( - updatedDataModel: DashboardDataModel['columns'] - ) => { - try { - const { columns: newColumns, version } = await handleUpdateDataModelData({ - ...(dataModelData as DashboardDataModel), - columns: updatedDataModel, - }); - - setDataModelData((prev) => ({ - ...(prev as DashboardDataModel), - columns: newColumns, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const handleUpdateDataModel = async ( updatedDataModel: DashboardDataModel, key?: keyof DashboardDataModel @@ -362,12 +307,9 @@ const DataModelsPage = () => { dataModelData={dataModelData} dataModelPermissions={dataModelPermissions} fetchDataModel={() => fetchDataModelDetails(dashboardDataModelFQN)} - handleColumnUpdateDataModel={handleColumnUpdateDataModel} handleFollowDataModel={handleFollowDataModel} handleToggleDelete={handleToggleDelete} - handleUpdateDescription={handleUpdateDescription} handleUpdateOwner={handleUpdateOwner} - handleUpdateTags={handleUpdateTags} handleUpdateTier={handleUpdateTier} updateDataModelDetailsState={updateDataModelDetailsState} onUpdateDataModel={handleUpdateDataModel} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index e1eb437e296e..fedd6bda089c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -72,14 +72,14 @@ import { } from '../../rest/databaseAPI'; import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; import { getQueryFilterForDatabase } from '../../utils/Database/Database.util'; import databaseClassBase from '../../utils/Database/DatabaseClassBase'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTierTags } from '../../utils/TableUtils'; import { updateTierTag } from '../../utils/TagsUtils'; @@ -388,7 +388,7 @@ const DatabaseDetails: FunctionComponent = () => { }, []); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = databaseClassBase.getDatabaseDetailPageTabs({ activeTab, @@ -405,7 +405,7 @@ const DatabaseDetails: FunctionComponent = () => { labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.CHILDREN diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index b1f5b0acd6a1..f9e1321b5d73 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -72,13 +72,13 @@ import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getStoredProceduresList } from '../../rest/storedProceduresAPI'; import { getTableList } from '../../rest/tableAPI'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; import databaseSchemaClassBase from '../../utils/DatabaseSchemaClassBase'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; @@ -424,7 +424,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { }; const tabs: TabsProps['items'] = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = databaseSchemaClassBase.getDatabaseSchemaPageTabs({ feedCount, @@ -441,7 +441,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { labelMap: tabLabelMap, }); - return getGlossaryTermDetailTabs( + return getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.TABLE diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index 6e0d47c9bb2c..b4c0bcdceb2a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -14,36 +14,24 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; -import { isEmpty, isEqual, isUndefined, omitBy } from 'lodash'; +import { isUndefined, omitBy } from 'lodash'; import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; -import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; -import QueryViewer from '../../components/common/QueryViewer/QueryViewer.component'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import SampleDataWithMessages from '../../components/Database/SampleDataWithMessages/SampleDataWithMessages'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../components/Lineage/Lineage.component'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../components/SearchedData/SearchedData.interface'; +import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, } from '../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; -import LineageProvider from '../../context/LineageProvider/LineageProvider'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -53,10 +41,12 @@ import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { SearchIndex, TagLabel } from '../../generated/entity/data/searchIndex'; +import { Page, PageType } from '../../generated/system/ui/page'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { addFollower, getSearchIndexDetailsByFQN, @@ -66,27 +56,33 @@ import { updateSearchIndexVotes, } from '../../rest/SearchIndexAPI'; import { addToRecentViewed, getFeedCounts } from '../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; +import searchIndexClassBase from '../../utils/SearchIndexDetailsClassBase'; import { defaultFields } from '../../utils/SearchIndexUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; -import SearchIndexFieldsTab from './SearchIndexFieldsTab/SearchIndexFieldsTab'; function SearchIndexDetailsPage() { const { getEntityPermissionByFqn } = usePermissionProvider(); - const { tab: activeTab = EntityTabs.FIELDS } = useParams<{ tab: string }>(); + const { tab: activeTab = EntityTabs.FIELDS } = + useParams<{ tab: EntityTabs }>(); const { fqn: decodedSearchIndexFQN } = useFqn(); const { t } = useTranslation(); const history = useHistory(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const USERId = currentUser?.id ?? ''; const [loading, setLoading] = useState(true); const [searchIndexDetails, setSearchIndexDetails] = useState(); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); + const [customizedPage, setCustomizedPage] = useState(null); const [searchIndexPermissions, setSearchIndexPermissions] = useState(DEFAULT_ENTITY_PERMISSION); @@ -121,7 +117,6 @@ function SearchIndexDetailsPage() { }; const { - tier, searchIndexTags, owners, version, @@ -285,19 +280,6 @@ function SearchIndexDetailsPage() { } }; - const onFieldsUpdate = async (updateFields: SearchIndex['fields']) => { - if ( - searchIndexDetails && - !isEqual(searchIndexDetails.fields, updateFields) - ) { - const updatedSearchIndexDetails = { - ...searchIndexDetails, - fields: updateFields, - }; - await onSearchIndexUpdate(updatedSearchIndexDetails, 'fields'); - } - }; - const handleDisplayNameUpdate = async (data: EntityName) => { if (!searchIndexDetails) { return; @@ -309,19 +291,6 @@ function SearchIndexDetailsPage() { await onSearchIndexUpdate(updatedSearchIndex, 'displayName'); }; - const handleTagsUpdate = async (selectedTags?: Array) => { - if (selectedTags && searchIndexDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedSearchIndex = { ...searchIndexDetails, tags: updatedTags }; - await onSearchIndexUpdate(updatedSearchIndex, 'tags'); - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleTagsUpdate(updatedTags); - }; - const onExtensionUpdate = useCallback( async (updatedData: SearchIndex) => { searchIndexDetails && @@ -337,167 +306,28 @@ function SearchIndexDetailsPage() { ); const tabs = useMemo(() => { - const allTabs = [ - { - label: ( - - ), - key: EntityTabs.FIELDS, - children: ( - - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityType={EntityType.SEARCH_INDEX} - selectedTags={searchIndexTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.SAMPLE_DATA, - children: !viewSampleDataPermission ? ( -
- -
- ) : ( - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.SEARCH_INDEX_SETTINGS, - children: ( - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: searchIndexDetails && ( -
- - entityType={EntityType.SEARCH_INDEX} - handleExtensionUpdate={onExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ]; + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); + const allTabs = searchIndexClassBase.getSearchIndexDetailPageTabs({ + searchIndexDetails: searchIndexDetails ?? ({} as SearchIndex), + viewAllPermission, + feedCount, + activeTab, + getEntityFeedCount, + fetchSearchIndexDetails, + handleFeedCount, + viewSampleDataPermission, + deleted: deleted ?? false, + editLineagePermission, + editCustomAttributePermission, + onExtensionUpdate, + labelMap: tabLabelMap, + }); - return allTabs; + return getDetailsTabWithNewLabel( + allTabs, + customizedPage?.tabs, + EntityTabs.FIELDS + ); }, [ activeTab, searchIndexDetails, @@ -673,6 +503,24 @@ function SearchIndexDetailsPage() { } }, [decodedSearchIndexFQN]); + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.SearchIndex) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + useEffect(() => { if (viewPermission) { fetchSearchIndexDetails(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts deleted file mode 100644 index 503d7693f06e..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2023 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 { SearchIndexField } from '../../../generated/entity/data/searchIndex'; - -export interface SearchIndexFieldsTabProps { - fields: Array; - hasDescriptionEditAccess: boolean; - hasTagEditAccess: boolean; - hasGlossaryTermEditAccess: boolean; - isReadOnly?: boolean; - entityFqn: string; - onUpdate: (fields: Array) => Promise; -} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx index 97af3eff43e8..cee7910b7af4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx @@ -18,19 +18,6 @@ import React from 'react'; import { SearchIndexField } from '../../../generated/entity/data/searchIndex'; import { MOCK_SEARCH_INDEX_FIELDS } from '../../../mocks/SearchIndex.mock'; import SearchIndexFieldsTab from './SearchIndexFieldsTab'; -import { SearchIndexFieldsTabProps } from './SearchIndexFieldsTab.interface'; - -const mockOnUpdate = jest.fn(); - -const mockProps: SearchIndexFieldsTabProps = { - fields: MOCK_SEARCH_INDEX_FIELDS, - onUpdate: mockOnUpdate, - hasDescriptionEditAccess: true, - hasTagEditAccess: true, - hasGlossaryTermEditAccess: true, - isReadOnly: false, - entityFqn: 'search_service.search_index_fqn', -}; jest.mock( '../../../components/common/SearchBarComponent/SearchBar.component', @@ -86,9 +73,30 @@ jest.mock('../../../utils/EntityUtils', () => ({ highlightSearchText: jest.fn((text) => text), })); +jest.mock( + '../../../components/Customization/GenericProvider/GenericProvider', + () => ({ + useGenericContext: jest.fn(() => ({ + data: { + fields: MOCK_SEARCH_INDEX_FIELDS, + }, + permissions: { + ViewAll: true, + }, + onUpdate: jest.fn(), + })), + }) +); + +jest.mock('../../../hooks/useFqn', () => ({ + useFqn: jest.fn(() => ({ + fqn: 'search_service.search_index_fqn', + })), +})); + describe('SearchIndexFieldsTab component', () => { it('SearchIndexFieldsTab should pass all the fields to SearchIndexFieldsTable when not searched anything', () => { - render(); + render(); expect(screen.getByText('testSearchBar')).toBeInTheDocument(); expect(screen.getByText('testToggleExpandButton')).toBeInTheDocument(); @@ -100,7 +108,7 @@ describe('SearchIndexFieldsTab component', () => { it('SearchIndexFieldsTab should pass only filtered fields according to the search text', async () => { await act(async () => { - render(); + render(); }); const searchBar = screen.getByText('testSearchBar'); @@ -117,7 +125,7 @@ describe('SearchIndexFieldsTab component', () => { it('expandedRowKeys should be updated with proper value on click of toggle button', async () => { await act(async () => { - render(); + render(); }); const toggleExpandButton = screen.getByText('testToggleExpandButton'); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx index 95cbff776261..f3b74ca58dfa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx @@ -18,30 +18,45 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Searchbar from '../../../components/common/SearchBarComponent/SearchBar.component'; import ToggleExpandButton from '../../../components/common/ToggleExpandButton/ToggleExpandButton'; -import { SearchIndexField } from '../../../generated/entity/data/searchIndex'; +import { useGenericContext } from '../../../components/Customization/GenericProvider/GenericProvider'; +import { + SearchIndex, + SearchIndexField, +} from '../../../generated/entity/data/searchIndex'; +import { useFqn } from '../../../hooks/useFqn'; import { getAllRowKeysByKeyName, getTableExpandableConfig, searchInFields, } from '../../../utils/TableUtils'; import SearchIndexFieldsTable from '../SearchIndexFieldsTable/SearchIndexFieldsTable'; -import { SearchIndexFieldsTabProps } from './SearchIndexFieldsTab.interface'; -function SearchIndexFieldsTab({ - fields, - onUpdate, - hasDescriptionEditAccess, - hasTagEditAccess, - hasGlossaryTermEditAccess, - isReadOnly = false, - entityFqn, -}: SearchIndexFieldsTabProps) { +function SearchIndexFieldsTab() { const { t } = useTranslation(); const [searchText, setSearchText] = useState(''); const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [searchedFields, setSearchedFields] = useState>( [] ); + const { fqn: entityFqn } = useFqn(); + const { data, permissions, onUpdate } = useGenericContext(); + + const { fields, deleted } = useMemo(() => data, [data.fields, data.deleted]); + + const { + hasDescriptionEditAccess, + hasGlossaryTermEditAccess, + hasTagEditAccess, + } = useMemo( + () => ({ + hasDescriptionEditAccess: + permissions.EditAll || permissions.EditDescription, + hasGlossaryTermEditAccess: + permissions.EditAll || permissions.EditGlossaryTerms, + hasTagEditAccess: permissions.EditAll || permissions.EditTags, + }), + [permissions] + ); const sortByOrdinalPosition = useMemo( () => sortBy(fields, 'ordinalPosition'), @@ -83,6 +98,16 @@ function SearchIndexFieldsTab({ [expandedRowKeys] ); + const handleSearchIndexFieldsUpdate = useCallback( + async (updatedFields: Array) => { + await onUpdate({ + ...data, + fields: updatedFields, + }); + }, + [data, onUpdate] + ); + useEffect(() => { if (!searchText) { setSearchedFields(sortByOrdinalPosition); @@ -99,7 +124,7 @@ function SearchIndexFieldsTab({ return ( <> - +
+ + + - - ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index 1e8ff45185fb..88934ace17bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -60,11 +60,11 @@ import { updateStoredProcedureVotes, } from '../../rest/storedProceduresAPI'; import { addToRecentViewed, getFeedCounts } from '../../utils/CommonUtils'; -import { getEntityName } from '../../utils/EntityUtils'; import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../utils/GlossaryTerm/GlossaryTermUtil'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getStoredProcedureDetailsPageTabs, @@ -441,7 +441,7 @@ const StoredProcedurePage = () => { ); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = getStoredProcedureDetailsPageTabs({ activeTab: activeTab as EntityTabs, @@ -462,7 +462,7 @@ const StoredProcedurePage = () => { labelMap: tabLabelMap, }); - const updatedTabs = getGlossaryTermDetailTabs( + const updatedTabs = getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.CODE diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 32ac829a9a4c..a7ba0ac9cd8d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -80,14 +80,14 @@ import { getFeedCounts, getPartialNameFromTableFQN, } from '../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; import { defaultFields } from '../../utils/DatasetDetailsUtils'; import EntityLink from '../../utils/EntityLink'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; -import { - getGlossaryTermDetailTabs, - getTabLabelMap, -} from '../../utils/GlossaryTerm/GlossaryTermUtil'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import tableClassBase from '../../utils/TableClassBase'; import { @@ -486,7 +486,7 @@ const TableDetailsPageV1: React.FC = () => { ); const tabs = useMemo(() => { - const tabLabelMap = getTabLabelMap(customizedPage?.tabs); + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const tabs = tableClassBase.getTableDetailPageTabs({ queryCount, @@ -511,7 +511,7 @@ const TableDetailsPageV1: React.FC = () => { labelMap: tabLabelMap, }); - const updatedTabs = getGlossaryTermDetailTabs( + const updatedTabs = getDetailsTabWithNewLabel( tabs, customizedPage?.tabs, EntityTabs.SCHEMA diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index 45dcd475d0c6..ec9aa391ef42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -17,22 +17,21 @@ import { EntityTags } from 'Models'; import React from 'react'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import ContainerChildren from '../components/Container/ContainerChildren/ContainerChildren'; -import ContainerDataModel from '../components/Container/ContainerDataModel/ContainerDataModel'; +import { ContainerWidget } from '../components/Container/ContainerWidget/ContainerWidget'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; -import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Column, ContainerDataModel as ContainerDataModelType, } from '../generated/entity/data/container'; +import { PageType } from '../generated/system/ui/uiCustomization'; import { LabelType, State, TagLabel } from '../generated/type/tagLabel'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { ContainerDetailPageTabProps } from './ContainerDetailsClassBase'; @@ -121,111 +120,45 @@ export const ContainerFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWN export const getContainerDetailPageTabs = ({ isDataModelEmpty, - description, decodedContainerName, - entityName, - editDescriptionPermission, - editGlossaryTermsPermission, - editTagsPermission, editLineagePermission, editCustomAttributePermission, viewAllPermission, - containerChildrenData, - fetchContainerChildren, - isChildrenLoading, - handleUpdateDescription, - handleUpdateDataModel, - handleTagSelection, handleExtensionUpdate, feedCount, getEntityFeedCount, handleFeedCount, tab, deleted, - owners, containerData, fetchContainerDetail, - tags, + labelMap, }: ContainerDetailPageTabProps) => { return [ - { - label: ( - - ), - key: isDataModelEmpty ? EntityTabs.CHILDREN : EntityTabs.SCHEMA, - children: ( - - - - - - {isDataModelEmpty ? ( - - ) : ( - - )} - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={ - editTagsPermission && !containerData?.deleted - } - entityType={EntityType.CONTAINER} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={handleExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, ...(isDataModelEmpty - ? [] + ? [ + { + label: ( + + ), + key: EntityTabs.CHILDREN, + children: , + }, + ] : [ + { + label: ( + + ), + key: EntityTabs.SCHEMA, + children: , + }, { label: ( @@ -234,10 +167,7 @@ export const getContainerDetailPageTabs = ({ children: (
- + ), @@ -304,5 +234,14 @@ export const getContainerDetailPageTabs = ({ }; export const getContainerWidgetsFromKey = (widgetConfig: WidgetConfig) => { - return ; + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.CONTAINER_CHILDREN)) { + return ; + } + + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index ebfa9c58d5e2..a91d04068b87 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -11,7 +11,6 @@ * limitations under the License. */ -import { EntityTags } from 'Models'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -25,14 +24,12 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { Container, - ContainerDataModel, FileFormat, StorageServiceType, } from '../generated/entity/data/container'; import { Tab } from '../generated/system/ui/uiCustomization'; import { getContainerDetailPageTabs } from './ContainerDetailUtils'; -import { EntityReference } from '../generated/entity/type'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getContainerWidgetsFromKey } from './ContainerDetailUtils'; @@ -41,30 +38,17 @@ import i18n from './i18next/LocalUtil'; export interface ContainerDetailPageTabProps { isDataModelEmpty: boolean; - description: string; decodedContainerName: string; - entityName: string; - editDescriptionPermission: boolean; - editGlossaryTermsPermission: boolean; - editTagsPermission: boolean; editLineagePermission: boolean; editCustomAttributePermission: boolean; viewAllPermission: boolean; - containerChildrenData: EntityReference[]; - fetchContainerChildren: () => Promise; - isChildrenLoading: boolean; - handleUpdateDescription: (description: string) => Promise; - handleUpdateDataModel: (dataModel?: ContainerDataModel) => Promise; - handleTagSelection: (tags: EntityTags[]) => Promise; handleExtensionUpdate: (updatedContainer: Container) => Promise; feedCount: { totalCount: number }; getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; tab: EntityTabs; - owners: EntityReference[]; deleted: boolean; containerData: Container; - tags: EntityTags[]; fetchContainerDetail: (containerFQN: string) => Promise; labelMap?: Record; } @@ -91,63 +75,61 @@ class ContainerDetailsClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.CHILDREN: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CONTAINER_CHILDREN, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && ![EntityTabs.CHILDREN, EntityTabs.SCHEMA].includes(tab)) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 3, + i: DetailPageWidgetKeys.CONTAINER_CHILDREN, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): Container { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts index 68465ed292a8..a21ff15f8097 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeDetailPage/CustomizeDetailPage.ts @@ -42,7 +42,7 @@ class CustomizeDetailPageClassBase { dataProduct: 3, tags: 3, glossaryTerms: 3, - customProperty: 3, + customProperty: 4, tabs: 1, announcements: 3, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 9fddcd369d19..3c1b90772e97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -23,6 +23,7 @@ import dashboardDetailsClassBase from '../DashboardDetailsClassBase'; import databaseClassBase from '../Database/DatabaseClassBase'; import databaseSchemaClassBase from '../DatabaseSchemaClassBase'; import domainClassBase from '../Domain/DomainClassBase'; +import { getEntityName } from '../EntityUtils'; import i18n from '../i18next/LocalUtil'; import pipelineClassBase from '../PipelineClassBase'; import searchIndexClassBase from '../SearchIndexDetailsClassBase'; @@ -400,3 +401,48 @@ export const getWidgetsFromKey = ( return null; } }; + +export const getDetailsTabWithNewLabel = ( + defaultTabs: Array< + NonNullable[number] & { isHidden?: boolean } + >, + customizedTabs?: Tab[], + defaultTabId: EntityTabs = EntityTabs.OVERVIEW +) => { + if (!customizedTabs) { + return defaultTabs.filter((data) => !data.isHidden); + } + const overviewTab = defaultTabs?.find((t) => t.key === defaultTabId); + + const newTabs = + customizedTabs?.map((t) => { + const tabItemDetails = defaultTabs?.find((i) => i.key === t.id); + + return ( + tabItemDetails ?? { + label: getEntityName(t), + key: t.id, + children: overviewTab?.children, + } + ); + }) ?? defaultTabs; + + return newTabs.filter((data) => !data.isHidden); +}; + +export const getTabLabelMapFromTabs = ( + tabs?: Tab[] +): Record => { + const labelMap = {} as Record; + + return ( + tabs?.reduce((acc: Record, item) => { + if (item.id && item.displayName) { + const tab = item.id as EntityTabs; + acc[tab] = item.displayName; + } + + return acc; + }, labelMap) ?? labelMap + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index bd06a71dfa8c..6e99aa563e7f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -34,7 +34,6 @@ import { import i18n from './i18next/LocalUtil'; export interface DashboardDataModelDetailPageTabProps { - modelComponent: JSX.Element; feedCount: { totalCount: number; }; @@ -49,6 +48,7 @@ export interface DashboardDataModelDetailPageTabProps { ) => Promise; getEntityFeedCount: () => void; fetchDataModel: () => void; + labelMap?: Record; } class DashboardDataModelBase { @@ -73,63 +73,61 @@ class DashboardDataModelBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.MODEL: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 6, - i: DetailPageWidgetKeys.DATA_MODEL, - w: 6, - x: 0, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.MODEL) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 6, + i: DetailPageWidgetKeys.DATA_MODEL, + w: 6, + x: 0, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): DashboardDataModel { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index a99cc8be908a..b534f05a91f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -17,19 +17,22 @@ import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/Acti import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import ModelTab from '../components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import SchemaEditor from '../components/Database/SchemaEditor/SchemaEditor'; import Lineage from '../components/Lineage/Lineage.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import LineageProvider from '../context/LineageProvider/LineageProvider'; import { CSMode } from '../enums/codemirror.enum'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum'; +import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; import i18n from './i18next/LocalUtil'; export const getDashboardDataModelDetailPageTabs = ({ - modelComponent, feedCount, activeTab, handleFeedCount, @@ -40,6 +43,7 @@ export const getDashboardDataModelDetailPageTabs = ({ handelExtensionUpdate, getEntityFeedCount, fetchDataModel, + labelMap, }: DashboardDataModelDetailPageTabProps): TabProps[] => { return [ { @@ -47,11 +51,11 @@ export const getDashboardDataModelDetailPageTabs = ({ ), key: EntityTabs.MODEL, - children: modelComponent, + children: , }, { label: ( @@ -59,7 +63,10 @@ export const getDashboardDataModelDetailPageTabs = ({ count={feedCount.totalCount} id={EntityTabs.ACTIVITY_FEED} isActive={activeTab === EntityTabs.ACTIVITY_FEED} - name={i18n.t('label.activity-feed-and-task-plural')} + name={ + labelMap?.[EntityTabs.ACTIVITY_FEED] ?? + i18n.t('label.activity-feed-and-task-plural') + } /> ), key: EntityTabs.ACTIVITY_FEED, @@ -81,7 +88,9 @@ export const getDashboardDataModelDetailPageTabs = ({ ), key: EntityTabs.SQL, @@ -106,7 +115,7 @@ export const getDashboardDataModelDetailPageTabs = ({ ), key: EntityTabs.LINEAGE, @@ -125,7 +134,10 @@ export const getDashboardDataModelDetailPageTabs = ({ label: ( ), key: EntityTabs.CUSTOM_PROPERTIES, @@ -150,5 +162,14 @@ export const getDashboardDataModelDetailPageTabs = ({ export const getDashboardDataModelWidgetsFromKey = ( widgetConfig: WidgetConfig ) => { - return ; + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DATA_MODEL)) { + return ; + } + + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 0831136c2f78..2b08c3dca968 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -72,61 +72,61 @@ class DashboardDetailsClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.DETAILS: - default: - return [ - { - h: 1, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 6, - i: DetailPageWidgetKeys.CHARTS_TABLE, - w: 6, - x: 0, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.DETAILS) { + return []; } + + return [ + { + h: 1, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 6, + i: DetailPageWidgetKeys.CHARTS_TABLE, + w: 6, + x: 0, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): Dashboard { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 1a61e5745bd1..65d01e7db8e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -142,5 +142,10 @@ export const getDashboardWidgetsFromKey = (widgetConfig: WidgetConfig) => { return ; } - return ; + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 7102f3bb64b6..2c2811758fe0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -206,5 +206,10 @@ export const getDatabaseWidgetsFromKey = (widgetConfig: WidgetConfig) => { return ; } - return ; + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index 80c8829f15fb..54c1fc85357e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -76,63 +76,61 @@ class DatabaseClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 11, - i: DetailPageWidgetKeys.DATABASE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.SCHEMA) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.DATABASE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): Database { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 45755c1329c6..517bc581954f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -75,131 +75,62 @@ class DatabaseSchemaClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.TABLE: - return [ - { - h: 1, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLES, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.TABLE) { + return []; } - } - public getDatabaseSchemaPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } - }; + return [ + { + h: 1, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.TABLES, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + } public getCommonWidgetList() { return [ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index 7360b9024cda..7b874e39ddc5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -120,5 +120,10 @@ export const getDatabaseSchemaWidgetsFromKey = (widgetConfig: WidgetConfig) => { return ; } - return ; + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 85fa83a0ad33..8563bff361c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -82,48 +82,46 @@ class DomainClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.DOCUMENTATION: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.DOCUMENTATION) { + return []; + } - { - h: 1, - i: DetailPageWidgetKeys.OWNERS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.EXPERTS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.DOMAIN_TYPE, - w: 2, - x: 6, - y: 3, - static: false, - }, - ]; + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, - default: - return []; - } + { + h: 1, + i: DetailPageWidgetKeys.OWNERS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.EXPERTS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.DOMAIN_TYPE, + w: 2, + x: 6, + y: 3, + static: false, + }, + ]; } public getDummyData(): Domain { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index 55264706a8c8..a28224b0f3ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -461,5 +461,7 @@ export const getDomainWidgetsFromKey = (widgetConfig: WidgetConfig) => { return ; } - return ; + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx index 263b4a7543d2..08962d22a506 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryTerm/GlossaryTermUtil.tsx @@ -10,16 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TabsProps } from 'antd'; import { isUndefined } from 'lodash'; import React from 'react'; import EmptyWidgetPlaceholder from '../../components/MyData/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder'; import { SIZE } from '../../enums/common.enum'; -import { EntityTabs } from '../../enums/entity.enum'; -import { Tab } from '../../generated/system/ui/uiCustomization'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; -import { getEntityName } from '../EntityUtils'; export const getWidgetFromKey = ({ widgetConfig, @@ -70,46 +66,3 @@ export const getWidgetFromKey = ({ /> ); }; - -export const getGlossaryTermDetailTabs = ( - defaultTabs: Array< - NonNullable[number] & { isHidden?: boolean } - >, - customizedTabs?: Tab[], - defaultTabId: EntityTabs = EntityTabs.OVERVIEW -) => { - if (!customizedTabs) { - return defaultTabs.filter((data) => !data.isHidden); - } - const overviewTab = defaultTabs?.find((t) => t.key === defaultTabId); - - const newTabs = - customizedTabs?.map((t) => { - const tabItemDetails = defaultTabs?.find((i) => i.key === t.id); - - return ( - tabItemDetails ?? { - label: getEntityName(t), - key: t.id, - children: overviewTab?.children, - } - ); - }) ?? defaultTabs; - - return newTabs.filter((data) => !data.isHidden); -}; - -export const getTabLabelMap = (tabs?: Tab[]): Record => { - const labelMap = {} as Record; - - return ( - tabs?.reduce((acc: Record, item) => { - if (item.id && item.displayName) { - const tab = item.id as EntityTabs; - acc[tab] = item.displayName; - } - - return acc; - }, labelMap) ?? labelMap - ); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index 6c71c5bbffa1..e239821e1a71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -11,7 +11,6 @@ * limitations under the License. */ -import { ColumnType } from 'antd/lib/table'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -21,20 +20,16 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; -import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { Tag } from '../generated/entity/classification/tag'; import { Pipeline, PipelineServiceType, State, StatusType, - Task, } from '../generated/entity/data/pipeline'; -import { EntityReference } from '../generated/entity/type'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { LabelType, TagLabel, TagSource } from '../generated/type/tagLabel'; +import { LabelType, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; @@ -45,33 +40,19 @@ import { } from './PipelineDetailsUtils'; export interface PipelineDetailPageTabProps { - description: string; - entityName: string; feedCount: { totalCount: number; }; - editDescriptionPermission: boolean; - editGlossaryTermsPermission: boolean; - editTagsPermission: boolean; getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; - handleTagSelection: (selectedTags: TagLabel[]) => Promise; - onDescriptionUpdate: (value: string) => Promise; onExtensionUpdate: (updatedPipeline: Pipeline) => Promise; pipelineDetails: Pipeline; pipelineFQN: string; - tasksInternal: Task[]; - tasksDAGView: JSX.Element; - tags: Tag[]; viewAllPermission: boolean; editLineagePermission: boolean; editCustomAttributePermission: boolean; deleted: boolean; - activeTab: PIPELINE_TASK_TABS; tab: EntityTabs; - setActiveTab: (tab: PIPELINE_TASK_TABS) => void; - taskColumns: ColumnType[]; - owners: EntityReference[]; fetchPipeline: () => void; labelMap?: Record; } @@ -85,85 +66,75 @@ class PipelineClassBase { public getPipelineDetailPageTabsIds(): Tab[] { return [ - EntityTabs.SCHEMA, + EntityTabs.TASKS, EntityTabs.ACTIVITY_FEED, - EntityTabs.SAMPLE_DATA, - EntityTabs.TABLE_QUERIES, - EntityTabs.PROFILER, - EntityTabs.INCIDENTS, + EntityTabs.EXECUTIONS, EntityTabs.LINEAGE, - EntityTabs.VIEW_DEFINITION, EntityTabs.CUSTOM_PROPERTIES, ].map((tab: EntityTabs) => ({ id: tab, name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [ - EntityTabs.SCHEMA, - EntityTabs.OVERVIEW, - EntityTabs.TERMS, - ].includes(tab), + editable: tab === EntityTabs.TASKS, })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.TASKS: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 11, - i: DetailPageWidgetKeys.PIPELINE_TASKS, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.TASKS) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.PIPELINE_TASKS, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): Pipeline { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index 979c3d2429a7..9727f32c6b8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -11,28 +11,25 @@ * limitations under the License. */ -import { Col, Radio, Row, Table } from 'antd'; import { t } from 'i18next'; -import { isEmpty } from 'lodash'; import React from 'react'; import { ReactComponent as IconFailBadge } from '../assets/svg/fail-badge.svg'; import { ReactComponent as IconSkippedBadge } from '../assets/svg/skipped-badge.svg'; import { ReactComponent as IconSuccessBadge } from '../assets/svg/success-badge.svg'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../components/common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../components/common/ResizablePanels/ResizablePanels'; import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; -import EntityRightPanel from '../components/Entity/EntityRightPanel/EntityRightPanel'; import Lineage from '../components/Lineage/Lineage.component'; import ExecutionsTab from '../components/Pipeline/Execution/Execution.component'; +import { PipelineTaskTab } from '../components/Pipeline/PipelineTaskTab/PipelineTaskTab'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; -import { PIPELINE_TASK_TABS } from '../constants/pipeline.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../constants/ResizablePanel.constants'; import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { StatusType, TaskStatus } from '../generated/entity/data/pipeline'; +import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { PipelineDetailPageTabProps } from './PipelineClassBase'; @@ -57,30 +54,16 @@ export const getStatusBadgeIcon = (status?: StatusType) => { }; export const getPipelineDetailPageTabs = ({ - description, - editDescriptionPermission, - editGlossaryTermsPermission, - editTagsPermission, - entityName, feedCount, getEntityFeedCount, handleFeedCount, - handleTagSelection, - onDescriptionUpdate, onExtensionUpdate, pipelineDetails, pipelineFQN, - tasksInternal, - tasksDAGView, - tags, viewAllPermission, editLineagePermission, editCustomAttributePermission, deleted, - activeTab, - setActiveTab, - taskColumns, - owners, fetchPipeline, tab, }: PipelineDetailPageTabProps) => { @@ -88,85 +71,7 @@ export const getPipelineDetailPageTabs = ({ { label: , key: EntityTabs.TASKS, - children: ( - - - - - - - - - setActiveTab(e.target.value)} - /> - - - {activeTab === PIPELINE_TASK_TABS.LIST_VIEW ? ( -
- ) : ( - tasksDAGView - )} - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.PIPELINE} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), + children: , }, { label: ( @@ -241,5 +146,14 @@ export const getPipelineDetailPageTabs = ({ }; export const getPipelineWidgetsFromKey = (widgetConfig: WidgetConfig) => { - return ; + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.PIPELINE_TASKS)) { + return ; + } + + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts index c40adca9265e..e7a2c6908e62 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -31,99 +31,112 @@ import { } from '../generated/entity/data/searchIndex'; import { Tab } from '../generated/system/ui/uiCustomization'; import { LabelType, TagSource } from '../generated/type/tagLabel'; +import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; -import { getSearchIndexWidgetsFromKey } from './SearchIndexUtils'; +import { + getSearchIndexDetailsTabs, + getSearchIndexWidgetsFromKey, +} from './SearchIndexUtils'; export interface SearchIndexDetailPageTabProps { + searchIndexDetails: SearchIndex; + viewAllPermission: boolean; + feedCount: { + totalCount: number; + }; + activeTab: EntityTabs; + getEntityFeedCount: () => Promise; + fetchSearchIndexDetails: () => Promise; + handleFeedCount: (feedCount: FeedCounts) => void; + viewSampleDataPermission: boolean; + deleted: boolean; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + onExtensionUpdate: (extension: SearchIndex['extension']) => Promise; labelMap?: Record; } class SearchIndexClassBase { public getSearchIndexDetailPageTabs( - _tabProps: SearchIndexDetailPageTabProps + tabProps: SearchIndexDetailPageTabProps ): TabProps[] { - return []; + return getSearchIndexDetailsTabs(tabProps); } public getSearchIndexDetailPageTabsIds(): Tab[] { return [ - EntityTabs.SCHEMA, + EntityTabs.FIELDS, EntityTabs.ACTIVITY_FEED, EntityTabs.SAMPLE_DATA, - EntityTabs.TABLE_QUERIES, - EntityTabs.PROFILER, - EntityTabs.INCIDENTS, EntityTabs.LINEAGE, - EntityTabs.VIEW_DEFINITION, + EntityTabs.SEARCH_INDEX_SETTINGS, EntityTabs.CUSTOM_PROPERTIES, ].map((tab: EntityTabs) => ({ id: tab, name: tab, displayName: getTabLabelFromId(tab), layout: this.getDefaultLayout(tab), - editable: [EntityTabs.SCHEMA].includes(tab), + editable: tab === EntityTabs.FIELDS, })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 11, - i: DetailPageWidgetKeys.DATABASE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.FIELDS) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.SEARCH_INDEX_FIELDS, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getDummyData(): SearchIndex { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx index fe3fcd513319..ae7cb1265f05 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx @@ -11,13 +11,29 @@ * limitations under the License. */ +import { t } from 'i18next'; import { uniqueId } from 'lodash'; import React from 'react'; +import ActivityFeedProvider from '../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import QueryViewer from '../components/common/QueryViewer/QueryViewer.component'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import SampleDataWithMessages from '../components/Database/SampleDataWithMessages/SampleDataWithMessages'; +import Lineage from '../components/Lineage/Lineage.component'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { SearchIndexField } from '../generated/entity/data/searchIndex'; +import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; +import SearchIndexFieldsTab from '../pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab'; +import { SearchIndexDetailPageTabProps } from './SearchIndexDetailsClassBase'; // eslint-disable-next-line max-len export const defaultFields = `${TabSpecificField.FIELDS},${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAIN},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; @@ -32,73 +48,149 @@ export const makeData = ( })); }; -export const getSearchIndexDetailsPageDefaultLayout = (tab: EntityTabs) => { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 3, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 4, - static: false, - }, - ]; - - default: - return []; - } +export const getSearchIndexDetailsTabs = ({ + searchIndexDetails, + viewAllPermission, + feedCount, + activeTab, + getEntityFeedCount, + fetchSearchIndexDetails, + handleFeedCount, + viewSampleDataPermission, + deleted, + editLineagePermission, + editCustomAttributePermission, + onExtensionUpdate, + labelMap, +}: SearchIndexDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.FIELDS, + children: , + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.SAMPLE_DATA, + children: !viewSampleDataPermission ? ( +
+ +
+ ) : ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.SEARCH_INDEX_SETTINGS, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: searchIndexDetails && ( +
+ + entityType={EntityType.SEARCH_INDEX} + handleExtensionUpdate={onExtensionUpdate} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; }; export const getSearchIndexWidgetsFromKey = (widgetConfig: WidgetConfig) => { - return ; + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.SEARCH_INDEX_FIELDS)) { + return ; + } + + return ( + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index 8b9290d8eb1b..40772b5bbe40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -82,63 +82,61 @@ class StoredProcedureClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.CODE: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 7, - i: DetailPageWidgetKeys.STORED_PROCEDURE_CODE, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.CODE) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 7, + i: DetailPageWidgetKeys.STORED_PROCEDURE_CODE, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getAlertEnableStatus() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index b06a0a5ad759..06424cbe680b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -117,6 +117,11 @@ export const getStoredProcedureWidgetsFromKey = ( if (widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE)) { return ; } else { - return ; + return ( + + ); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index ce888a2cfd92..2089007282e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -92,79 +92,77 @@ class TableClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 1, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 11, - i: DetailPageWidgetKeys.TABLE_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, - w: 2, - x: 6, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.TABLE_CONSTRAINTS, - w: 2, - x: 6, - y: 4, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.SCHEMA) { + return []; } + + return [ + { + h: 1, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 11, + i: DetailPageWidgetKeys.TABLE_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.FREQUENTLY_JOINED_TABLES, + w: 2, + x: 6, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.TABLE_CONSTRAINTS, + w: 2, + x: 6, + y: 4, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getAlertEnableStatus() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 59f5ea8b1f0a..a8b19d06108e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -160,7 +160,6 @@ import { ReactComponent as IconXML } from '../assets/svg/data-type-icon/xml.svg' import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; import SchemaTable from '../components/Database/SchemaTable/SchemaTable.component'; -import { StoredProcedureCodeCard } from '../components/Database/StoredProcedureCodeCard/StoredProcedureCodeCard'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { PageType } from '../generated/system/ui/uiCustomization'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; @@ -1082,11 +1081,12 @@ export const getTableWidgetFromKey = ( return ; } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.PARTITIONED_KEYS)) { return ; - } else if ( - widgetConfig.i.startsWith(DetailPageWidgetKeys.STORED_PROCEDURE_CODE) - ) { - return ; } else { - return ; + return ( + + ); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts index 5601bbd260a1..2d7d4e3d757b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -76,63 +76,61 @@ class TopicClassBase { })); } - public getDefaultLayout(tab: EntityTabs) { - switch (tab) { - case EntityTabs.SCHEMA: - return [ - { - h: 2, - i: DetailPageWidgetKeys.DESCRIPTION, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 8, - i: DetailPageWidgetKeys.TOPIC_SCHEMA, - w: 6, - x: 0, - y: 0, - static: false, - }, - { - h: 1, - i: DetailPageWidgetKeys.DATA_PRODUCTS, - w: 2, - x: 6, - y: 1, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.TAGS, - w: 2, - x: 6, - y: 2, - static: false, - }, - { - h: 2, - i: DetailPageWidgetKeys.GLOSSARY_TERMS, - w: 2, - x: 6, - y: 3, - static: false, - }, - { - h: 4, - i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, - w: 2, - x: 6, - y: 6, - static: false, - }, - ]; - - default: - return []; + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.SCHEMA) { + return []; } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.TOPIC_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; } public getAlertEnableStatus() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx index 6b2c742c506d..2d84b6863aeb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.tsx @@ -19,7 +19,7 @@ import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidg import TopicSchemaFields from '../components/Topic/TopicSchema/TopicSchema'; import { ERROR_PLACEHOLDER_TYPE } from '../enums/common.enum'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; -import { EntityTabs } from '../enums/entity.enum'; +import { EntityTabs, EntityType } from '../enums/entity.enum'; import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import i18n from './i18next/LocalUtil'; @@ -106,6 +106,11 @@ export const getTopicWidgetsFromKey = (widgetConfig: WidgetConfig) => { if (widgetConfig.i.startsWith(DetailPageWidgetKeys.TOPIC_SCHEMA)) { return ; } else { - return ; + return ( + + ); } }; From bb8082e01b47cd6006804217484f37cb839337a7 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:11:34 +0530 Subject: [PATCH 31/63] fix tests --- .../ContainerChildren.test.tsx | 68 ++++----- .../Database/SchemaTable/SchemaTable.test.tsx | 2 +- .../DocumentationTab.test.tsx | 10 ++ .../EntityRightPanel.test.tsx | 2 +- .../GlossaryHeader/GlossaryHeader.test.tsx | 2 +- .../tabs/GlossaryTermReferences.test.tsx | 2 +- .../tabs/GlossaryTermSynonyms.test.tsx | 2 +- .../GlossaryTerms/tabs/RelatedTerms.test.tsx | 2 +- .../PipelineDetails/PipelineDetails.test.tsx | 34 +++-- .../Tag/TagsContainerV2/TagsContainerV2.tsx | 4 +- .../Topic/TopicSchema/TopicSchema.test.tsx | 2 +- .../pages/ContainerPage/ContainerPage.mock.ts | 6 +- .../ContainerPage/ContainerPage.test.tsx | 65 ++++++--- .../DataModelPage/DataModelPage.component.tsx | 1 - .../DataModelPage/DataModelPage.test.tsx | 138 ++++++++++-------- .../DatabaseSchemaVersionPage.test.tsx | 21 +-- .../StoredProcedurePage.test.tsx | 17 +-- .../TableDetailsPageV1.test.tsx | 8 +- .../utils/mocks/PipelineDetailsUtils.mock.ts | 1 + 19 files changed, 221 insertions(+), 166 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx index 41a57ab49257..7679756f0ef3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx @@ -12,10 +12,11 @@ */ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { BrowserRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; +import { TabSpecificField } from '../../../enums/entity.enum'; +import { getContainerByName } from '../../../rest/storageAPI'; import ContainerChildren from './ContainerChildren'; -const mockFetchChildren = jest.fn(); const mockChildrenList = [ { id: '1', @@ -33,52 +34,48 @@ const mockChildrenList = [ }, ]; -jest.mock('../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../Customization/GenericTab/GenericTab', () => ({ useGenericContext: jest.fn().mockImplementation(() => ({ data: { children: mockChildrenList }, })), })); -describe('ContainerChildren', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - }); +jest.mock('../../../rest/storageAPI'); +describe('ContainerChildren', () => { it('Should call fetch container function on load', () => { - render( - - - - ); + render(, { wrapper: MemoryRouter }); - expect(mockFetchChildren).toHaveBeenCalled(); + expect(getContainerByName).toHaveBeenCalledWith( + '', + expect.objectContaining({ + fields: TabSpecificField.CHILDREN, + }) + ); }); it('Should render table with correct columns', () => { - render( - - - - ); + render(, { wrapper: MemoryRouter }); expect(screen.getByTestId('container-list-table')).toBeInTheDocument(); expect(screen.getByText('label.name')).toBeInTheDocument(); expect(screen.getByText('label.description')).toBeInTheDocument(); }); - it('Should render container names as links', () => { - render( - - - + it('Should render container names as links', async () => { + (getContainerByName as jest.Mock).mockResolvedValue({ + children: mockChildrenList, + }); + render(, { wrapper: MemoryRouter }); + + expect(getContainerByName).toHaveBeenCalledWith( + '', + expect.objectContaining({ + fields: TabSpecificField.CHILDREN, + }) ); - const containerNameLinks = screen.getAllByTestId('container-name'); + const containerNameLinks = await screen.findAllByTestId('container-name'); expect(containerNameLinks).toHaveLength(2); @@ -91,17 +88,10 @@ describe('ContainerChildren', () => { }); }); - it('Should render container descriptions as rich text', () => { - render( - - - - ); - - // Fast-forward until all timers have been executed - jest.runAllTimers(); + it('Should render container descriptions as rich text', async () => { + render(, { wrapper: MemoryRouter }); - const richTextPreviewers = screen.getAllByTestId('viewer-container'); + const richTextPreviewers = await screen.findAllByTestId('viewer-container'); expect(richTextPreviewers).toHaveLength(2); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx index 68c74d60d1d6..fbb6c7242fde 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx @@ -73,7 +73,7 @@ const mockGenericContextProps = { permissions: DEFAULT_ENTITY_PERMISSION, }; -jest.mock('../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest .fn() .mockImplementation(() => mockGenericContextProps), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.test.tsx index dbdd8f3222ba..8291d6af7082 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.test.tsx @@ -35,6 +35,16 @@ jest.mock('../../../common/ProfilePicture/ProfilePicture', () => jest.fn().mockReturnValue(<>ProfilePicture) ); +jest.mock('../../../Customization/GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockReturnValue({ + data: MOCK_DOMAIN, + permissions: { + ViewAll: true, + EditAll: true, + }, + }), +})); + describe('DocumentationTab', () => { it('should render the initial content', () => { const { getByTestId } = render(, { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx index a1c8def048bd..76ac6d89fade 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.test.tsx @@ -55,7 +55,7 @@ jest.mock( }) ); -jest.mock('../../../components/GenericProvider/GenericProvider', () => ({ +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => ({ data: { tableDetails: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.test.tsx index f32c50462e0b..56ab7f6e34c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.test.tsx @@ -179,7 +179,7 @@ const mockContext = { permissions: DEFAULT_ENTITY_PERMISSION, }; -jest.mock('../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => mockContext), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.test.tsx index 96517ebdfbd1..e14e0568528a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermReferences.test.tsx @@ -27,7 +27,7 @@ const mockContext = { permissions: MOCK_PERMISSIONS, }; -jest.mock('../../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => mockContext), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.test.tsx index 9a785f8c7ab2..cc44f1a854a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryTermSynonyms.test.tsx @@ -27,7 +27,7 @@ const mockContext = { permissions: MOCK_PERMISSIONS, }; -jest.mock('../../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => mockContext), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx index 02efd7c68c5e..c06a595291dd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx @@ -25,7 +25,7 @@ const mockContext = { permissions: MOCK_PERMISSIONS, }; -jest.mock('../../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => mockContext), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx index 6794a5953b21..0fc35e9c0f6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx @@ -26,7 +26,9 @@ import { MemoryRouter, useParams } from 'react-router-dom'; import { EntityTabs } from '../../../enums/entity.enum'; import { Pipeline } from '../../../generated/entity/data/pipeline'; import { Paging } from '../../../generated/type/paging'; +import { mockPipelineDetails } from '../../../utils/mocks/PipelineDetailsUtils.mock'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import PipelineDetails from './PipelineDetails.component'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; @@ -168,6 +170,7 @@ jest.mock('../../PageLayoutV1/PageLayoutV1', () => { jest.mock('../../../utils/TableTags/TableTags.utils', () => ({ getAllTags: jest.fn().mockReturnValue([]), searchTagInData: jest.fn().mockReturnValue([]), + getFilterTags: jest.fn().mockReturnValue([]), })); jest.mock('../../../utils/EntityUtils', () => ({ @@ -190,6 +193,7 @@ jest.mock('../../../utils/CommonUtils', () => ({ jest.mock('../../../utils/TagsUtils', () => ({ createTagObject: jest.fn().mockReturnValue([]), updateTierTag: jest.fn().mockReturnValue([]), + getTagPlaceholder: jest.fn().mockReturnValue(''), })); jest.mock('../../../utils/ToastUtils', () => ({ @@ -241,6 +245,20 @@ jest.mock('../../../hoc/LimitWrapper', () => { return jest.fn().mockImplementation(({ children }) =>
{children}
); }); +// jest.mock('../../../utils/PipelineDetailsUtils', () => ({ +// getPipelineDetailPageTabs: jest.fn().mockReturnValue([]), +// })); + +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ + GenericProvider: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), + useGenericContext: jest.fn().mockReturnValue({ + data: mockPipelineDetails, + permissions: DEFAULT_ENTITY_PERMISSION, + }), +})); + describe('Test PipelineDetails component', () => { it('Checks if the PipelineDetails component has all the proper components rendered', async () => { const { container } = render( @@ -284,15 +302,13 @@ describe('Test PipelineDetails component', () => { }); it('Should render no tasks data placeholder is tasks list is empty', async () => { - render( - , - { - wrapper: MemoryRouter, - } - ); + (useGenericContext as jest.Mock).mockReturnValue({ + data: { tasks: [] } as unknown as Pipeline, + permissions: DEFAULT_ENTITY_PERMISSION, + }); + render(, { + wrapper: MemoryRouter, + }); const switchContainer = screen.getByTestId('pipeline-task-switch'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx index 1a62cd93d73e..8f78b49ec56b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx @@ -82,8 +82,8 @@ const TagsContainerV2 = ({ () => ({ isGlossaryType: tagType === TagSource.Glossary, showAddTagButton: permission && isEmpty(tags?.[tagType]), - selectedTagsInternal: tags?.[tagType].map(({ tagFQN }) => tagFQN), - initialOptions: tags?.[tagType].map((data) => ({ + selectedTagsInternal: tags?.[tagType]?.map(({ tagFQN }) => tagFQN), + initialOptions: tags?.[tagType]?.map((data) => ({ label: data.tagFQN, value: data.tagFQN, data, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx index 9af08133c0ff..601aa208f01f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.test.tsx @@ -120,7 +120,7 @@ const mockTopicDetails = { messageSchema: MESSAGE_SCHEMA as Topic['messageSchema'], }; -jest.mock('../../GenericProvider/GenericProvider', () => ({ +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => ({ data: mockTopicDetails, isVersionView: false, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts index c5c64217bcc5..4286026f5e4d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const CONTAINER_DATA = { +export const MOCK_CONTAINER_DATA = { id: '5d11e32a-8673-4a84-a9be-ccd9651ba9fc', name: 'transactions', fullyQualifiedName: 's3_storage_sample.transactions', @@ -91,7 +91,7 @@ export const CONTAINER_DATA = { deleted: false, }; -export const CONTAINER_DATA_1 = { - ...CONTAINER_DATA, +export const MOCK_CONTAINER_DATA_1 = { + ...MOCK_CONTAINER_DATA, dataModel: {}, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx index fa9468dec711..4ff220263fac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx @@ -21,14 +21,15 @@ import { getContainerByName, } from '../../rest/storageAPI'; import ContainerPage from './ContainerPage'; -import { CONTAINER_DATA, CONTAINER_DATA_1 } from './ContainerPage.mock'; +import { + MOCK_CONTAINER_DATA, + MOCK_CONTAINER_DATA_1, +} from './ContainerPage.mock'; const mockGetEntityPermissionByFqn = jest.fn().mockResolvedValue({ ViewBasic: true, }); -const mockGetContainerByName = jest.fn().mockResolvedValue(CONTAINER_DATA); - jest.mock( '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider', () => ({ @@ -102,7 +103,7 @@ jest.mock( '../../components/Container/ContainerChildren/ContainerChildren', () => jest.fn().mockImplementation(({ isLoading }) => { - getContainerByName(CONTAINER_DATA_1.fullyQualifiedName, { + getContainerByName(MOCK_CONTAINER_DATA_1.fullyQualifiedName, { fields: 'children', }); @@ -173,17 +174,7 @@ jest.mock('../../rest/feedsAPI', () => ({ postThread: jest.fn().mockImplementation(() => Promise.resolve()), })); -jest.mock('../../rest/storageAPI', () => ({ - addContainerFollower: jest.fn(), - getContainerByName: jest - .fn() - .mockImplementation((...params) => mockGetContainerByName(params)), - patchContainerDetails: jest.fn().mockImplementation(() => Promise.resolve()), - removeContainerFollower: jest - .fn() - .mockImplementation(() => Promise.resolve()), - restoreContainer: jest.fn().mockImplementation(() => Promise.resolve()), -})); +jest.mock('../../rest/storageAPI'); jest.mock('../../utils/CommonUtils', () => ({ addToRecentViewed: jest.fn(), @@ -214,6 +205,7 @@ jest.mock('../../utils/TableUtils', () => ({ jest.mock('../../utils/TagsUtils', () => ({ createTagObject: jest.fn().mockImplementation((tagObject) => tagObject), updateTierTag: jest.fn().mockImplementation((tagObject) => tagObject), + getTagPlaceholder: jest.fn().mockReturnValue(''), })); jest.mock('../../utils/ToastUtils', () => ({ @@ -222,7 +214,7 @@ jest.mock('../../utils/ToastUtils', () => ({ })); const mockUseParams = jest.fn().mockReturnValue({ - fqn: CONTAINER_DATA.fullyQualifiedName, + fqn: MOCK_CONTAINER_DATA.fullyQualifiedName, tab: 'schema', }); @@ -269,7 +261,7 @@ describe('Container Page Component', () => { expect(mockGetEntityPermissionByFqn).toHaveBeenCalled(); expect(getContainerByName).toHaveBeenCalledWith( - CONTAINER_DATA.fullyQualifiedName, + MOCK_CONTAINER_DATA.fullyQualifiedName, { fields: [ 'parent', @@ -305,6 +297,9 @@ describe('Container Page Component', () => { }); it('should render the page container data, with the schema tab selected', async () => { + (getContainerByName as jest.Mock).mockResolvedValueOnce( + MOCK_CONTAINER_DATA + ); await act(async () => { render(); @@ -312,7 +307,23 @@ describe('Container Page Component', () => { }); expect(mockGetEntityPermissionByFqn).toHaveBeenCalled(); - expect(getContainerByName).toHaveBeenCalled(); + expect(getContainerByName).toHaveBeenCalledWith( + 's3_storage_sample.transactions', + { + fields: [ + 'parent', + 'dataModel', + 'owners', + 'tags', + 'followers', + 'extension', + 'domain', + 'dataProducts', + 'votes', + ], + include: 'all', + } + ); expect(screen.getByTestId('data-asset-header')).toBeInTheDocument(); @@ -322,10 +333,16 @@ describe('Container Page Component', () => { expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); expect(screen.getByText('DescriptionV1')).toBeVisible(); expect(screen.getByText('ContainerDataModel')).toBeVisible(); - expect(screen.getByText('EntityRightPanel')).toBeVisible(); + expect(screen.getByText('CustomPropertyTable')).toBeVisible(); + expect(screen.getByText('label.glossary-term')).toBeVisible(); + expect(screen.getByText('label.tag-plural')).toBeVisible(); + expect(screen.getByText('label.data-product-plural')).toBeVisible(); }); it('onClick of follow container should call addContainerFollower', async () => { + (getContainerByName as jest.Mock).mockResolvedValueOnce( + MOCK_CONTAINER_DATA + ); await act(async () => { render(); @@ -342,6 +359,9 @@ describe('Container Page Component', () => { }); it('tab switch should work', async () => { + (getContainerByName as jest.Mock).mockResolvedValueOnce( + MOCK_CONTAINER_DATA + ); await act(async () => { render(); @@ -358,8 +378,11 @@ describe('Container Page Component', () => { }); it('children should render on children tab', async () => { + (getContainerByName as jest.Mock).mockResolvedValueOnce( + MOCK_CONTAINER_DATA_1 + ); mockUseParams.mockReturnValue({ - fqn: CONTAINER_DATA_1.fullyQualifiedName, + fqn: MOCK_CONTAINER_DATA_1.fullyQualifiedName, tab: EntityTabs.CHILDREN, }); @@ -376,7 +399,7 @@ describe('Container Page Component', () => { expect(screen.getByText('ContainerChildren')).toBeVisible(); expect(getContainerByName).toHaveBeenCalledWith( - CONTAINER_DATA_1.fullyQualifiedName, + MOCK_CONTAINER_DATA_1.fullyQualifiedName, { fields: 'children', } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index 1db10c842e34..18184fe60371 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -22,7 +22,6 @@ import { useState, } from 'react'; import { useTranslation } from 'react-i18next'; - import { useHistory } from 'react-router-dom'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx index 5f12345e9a6c..91944b9ad5e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.test.tsx @@ -10,15 +10,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - act, - render, - screen, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { mockUserData } from '../../components/Settings/Users/mocks/User.mocks'; +import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface'; +import { useFqn } from '../../hooks/useFqn'; +import { + addDataModelFollower, + getDataModelByFqn, + patchDataModelDetails, + removeDataModelFollower, + updateDataModelVotes, +} from '../../rest/dataModelsAPI'; +import { showErrorToast } from '../../utils/ToastUtils'; import DataModelsPage from './DataModelPage.component'; import { CREATE_THREAD, @@ -32,17 +38,11 @@ import { UPDATE_VOTE, } from './mocks/DataModelPage.mock'; -const mockAddDataModelFollower = jest.fn().mockResolvedValue({}); -const mockGetDataModelByFqn = jest.fn().mockResolvedValue({}); -const mockPatchDataModelDetails = jest.fn().mockResolvedValue({}); -const mockRemoveDataModelFollower = jest.fn().mockResolvedValue({}); -const mockUpdateDataModelVotes = jest.fn().mockResolvedValue({}); -const mockGetEntityPermissionByFqn = jest.fn().mockResolvedValue({ +const mockGetEntityPermissionByFqn = jest.fn().mockImplementation(() => ({ ViewAll: true, ViewBasic: true, -}); +})); const mockUpdateTierTag = jest.fn(); -const mockShowErrorToast = jest.fn(); const ENTITY_MISSING_ERROR = 'Entity missing error.'; jest.mock('../../hooks/useApplicationStore', () => ({ @@ -64,12 +64,9 @@ jest.mock( ({ createThread, dataModelData, - handleColumnUpdateDataModel, handleFollowDataModel, handleToggleDelete, - handleUpdateDescription, handleUpdateOwner, - handleUpdateTags, handleUpdateTier, onUpdateDataModel, onUpdateVote, @@ -80,10 +77,7 @@ jest.mock(
+
{ + const { + data: domain, + permissions, + onUpdate, + isVersionView, + } = useGenericContext(); + + const { editOwnerPermission, editAllPermission } = useMemo( + () => ({ + editOwnerPermission: permissions.EditAll || permissions.EditOwners, + editAllPermission: permissions.EditAll, + }), + [permissions] + ); + + const handleExpertsUpdate = async (data: Array) => { + if (!isEqual(data, domain.experts)) { + let updatedDomain = cloneDeep(domain); + const oldExperts = data.filter((d) => includes(domain.experts, d)); + const newExperts = data + .filter((d) => !includes(domain.experts, d)) + .map((d) => ({ + id: d.id, + type: d.type, + name: d.name, + displayName: d.displayName, + })); + updatedDomain = { + ...updatedDomain, + experts: [...oldExperts, ...newExperts], + }; + await onUpdate(updatedDomain); + } + }; + + return ( +
+
0 ? 'm-b-xss' : '' + }`}> + + {t('label.expert-plural')} + + {editOwnerPermission && domain.experts && domain.experts.length > 0 && ( + + +
+
+ {getOwnerVersionLabel( + domain, + isVersionView ?? false, + TabSpecificField.EXPERTS, + editAllPermission + )} +
+ +
+ {editOwnerPermission && domain.experts && domain.experts.length === 0 && ( + + } + label={t('label.add')} + tooltip="" + /> + + )} +
+
+ ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx index 00229776305b..3b0d4a1a8cd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx @@ -11,41 +11,32 @@ * limitations under the License. */ import { Button, Col, Row, Space, Tooltip, Typography } from 'antd'; -import { cloneDeep, includes, isEqual } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; import { ReactComponent as PlusIcon } from '../../../../assets/svg/plus-primary.svg'; import DescriptionV1 from '../../../../components/common/EntityDescription/DescriptionV1'; -import { UserSelectableList } from '../../../../components/common/UserSelectableList/UserSelectableList.component'; import { UserTeamSelectableList } from '../../../../components/common/UserTeamSelectableList/UserTeamSelectableList.component'; -import DomainTypeSelectForm from '../../../../components/Domain/DomainTypeSelectForm/DomainTypeSelectForm.component'; import { DE_ACTIVE_COLOR } from '../../../../constants/constants'; import { EntityField } from '../../../../constants/Feeds.constants'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../../constants/ResizablePanel.constants'; import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType, TabSpecificField } from '../../../../enums/entity.enum'; import { DataProduct } from '../../../../generated/entity/domains/dataProduct'; -import { - Domain, - DomainType, -} from '../../../../generated/entity/domains/domain'; -import { - ChangeDescription, - EntityReference, -} from '../../../../generated/entity/type'; -import { domainTypeTooltipDataRender } from '../../../../utils/DomainUtils'; +import { Domain } from '../../../../generated/entity/domains/domain'; +import { ChangeDescription } from '../../../../generated/entity/type'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityVersionByField, getOwnerVersionLabel, } from '../../../../utils/EntityVersionUtils'; import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; -import FormItemLabel from '../../../common/Form/FormItemLabel'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import TagButton from '../../../common/TagButton/TagButton.component'; import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import '../../domain.less'; +import { DomainExpertWidget } from '../../DomainExpertsWidget/DomainExpertWidget'; +import { DomainTypeWidget } from '../../DomainTypeWidget/DomainTypeWidget'; import { DocumentationEntity, DocumentationTabProps, @@ -56,7 +47,6 @@ const DocumentationTab = ({ type = DocumentationEntity.DOMAIN, }: DocumentationTabProps) => { const { t } = useTranslation(); - const [editDomainType, setEditDomainType] = useState(false); const resourceType = type === DocumentationEntity.DOMAIN ? ResourceEntity.DOMAIN @@ -133,36 +123,6 @@ const DocumentationTab = ({ await onUpdate(updatedData as Domain | DataProduct); }; - const handleExpertsUpdate = async (data: Array) => { - if (!isEqual(data, domain.experts)) { - let updatedDomain = cloneDeep(domain); - const oldExperts = data.filter((d) => includes(domain.experts, d)); - const newExperts = data - .filter((d) => !includes(domain.experts, d)) - .map((d) => ({ - id: d.id, - type: d.type, - name: d.name, - displayName: d.displayName, - })); - updatedDomain = { - ...updatedDomain, - experts: [...oldExperts, ...newExperts], - }; - await onUpdate(updatedDomain); - } - }; - - const handleDomainTypeUpdate = async (domainType: string) => { - let updatedDomain = cloneDeep(domain); - updatedDomain = { - ...updatedDomain, - domainType: domainType as DomainType, - }; - await onUpdate(updatedDomain); - setEditDomainType(false); - }; - return ( )} -
-
0 ? 'm-b-xss' : '' - }`}> - - {t('label.expert-plural')} - - {editOwnerPermission && - domain.experts && - domain.experts.length > 0 && ( - - -
-
- {getOwnerVersionLabel( - domain, - isVersionsView ?? false, - TabSpecificField.EXPERTS, - editAllPermission - )} -
+ -
- {editOwnerPermission && - domain.experts && - domain.experts.length === 0 && ( - - } - label={t('label.add')} - tooltip="" - /> - - )} -
- - - {type === DocumentationEntity.DOMAIN && ( -
-
- - - - - {editAllPermission && (domain as Domain).domainType && ( - -
- {!editDomainType && ( - - {(domain as Domain).domainType} - - )} - - {editDomainType && ( - setEditDomainType(false)} - onSubmit={handleDomainTypeUpdate} - /> - )} - - )} + {type === DocumentationEntity.DOMAIN && } {domain && type === DocumentationEntity.DATA_PRODUCT && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx new file mode 100644 index 000000000000..b6eeabe98133 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2025 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 { Button, Col, Space, Tooltip, Typography } from 'antd'; +import { t } from 'i18next'; +import { cloneDeep } from 'lodash'; +import React, { useMemo, useState } from 'react'; +import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; +import { DE_ACTIVE_COLOR } from '../../../constants/constants'; +import { Domain, DomainType } from '../../../generated/entity/domains/domain'; +import { domainTypeTooltipDataRender } from '../../../utils/DomainUtils'; +import FormItemLabel from '../../common/Form/FormItemLabel'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; +import DomainTypeSelectForm from '../DomainTypeSelectForm/DomainTypeSelectForm.component'; + +export const DomainTypeWidget = () => { + const { data: domain, permissions, onUpdate } = useGenericContext(); + const [editDomainType, setEditDomainType] = useState(false); + + const { editAllPermission } = useMemo( + () => ({ + editAllPermission: permissions.EditAll, + }), + [permissions] + ); + + const handleDomainTypeUpdate = async (domainType: string) => { + let updatedDomain = cloneDeep(domain); + updatedDomain = { + ...updatedDomain, + domainType: domainType as DomainType, + }; + await onUpdate(updatedDomain); + setEditDomainType(false); + }; + + return ( + +
+ + + + + {editAllPermission && (domain as Domain).domainType && ( + +
+ {!editDomainType && ( + + {domain?.domainType} + + )} + + {editDomainType && ( + setEditDomainType(false)} + onSubmit={handleDomainTypeUpdate} + /> + )} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 3390a64a9484..ddb87f978ab2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -39,7 +39,6 @@ export enum DetailPageWidgetKeys { CONTAINER_CHILDREN = 'KnowledgePanel.ContainerChildren', PIPELINE_TASKS = 'KnowledgePanel.PipelineTasks', SEARCH_INDEX_FIELDS = 'KnowledgePanel.SearchIndexFields', - DOCUMENTATION = 'KnowledgePanel.Documentation', OWNERS = 'KnowledgePanel.Owners', EXPERTS = 'KnowledgePanel.Experts', DOMAIN_TYPE = 'KnowledgePanel.DomainType', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index f9e1321b5d73..c5111eda8472 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -87,7 +87,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const { setFilters } = useTableFilters(INITIAL_TABLE_FILTERS); + const { setFilters, filters } = useTableFilters(INITIAL_TABLE_FILTERS); const { tab: activeTab = EntityTabs.TABLE } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedDatabaseSchemaFQN } = useFqn(); @@ -360,12 +360,15 @@ const DatabaseSchemaPage: FunctionComponent = () => { const { paging } = await getTableList({ databaseSchema: decodedDatabaseSchemaFQN, limit: 0, + include: filters.showDeletedTables + ? Include.Deleted + : Include.NonDeleted, }); setTableCount(paging.total); } catch (error) { showErrorToast(error as AxiosError); } - }, [decodedDatabaseSchemaFQN]); + }, [decodedDatabaseSchemaFQN, filters.showDeletedTables]); useEffect(() => { fetchDatabaseSchemaPermission(); @@ -375,11 +378,15 @@ const DatabaseSchemaPage: FunctionComponent = () => { if (viewDatabaseSchemaPermission) { fetchDatabaseSchemaDetails(); fetchStoreProcedureCount(); - fetchTableCount(); + getEntityFeedCount(); } }, [viewDatabaseSchemaPermission]); + useEffect(() => { + fetchTableCount(); + }, [filters.showDeletedTables]); + const { editCustomAttributePermission, viewAllPermission } = useMemo( () => ({ editCustomAttributePermission: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 8563bff361c7..36ee44dc800d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -96,7 +96,6 @@ class DomainClassBase { y: 0, static: false, }, - { h: 1, i: DetailPageWidgetKeys.OWNERS, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index a28224b0f3ac..5084ddc21850 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -24,9 +24,9 @@ import ResizablePanels from '../components/common/ResizablePanels/ResizablePanel import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import { DomainExpertWidget } from '../components/Domain/DomainExpertsWidget/DomainExpertWidget'; import DataProductsTab from '../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.component'; -import DocumentationTab from '../components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component'; -import { DocumentationEntity } from '../components/Domain/DomainTabs/DocumentationTab/DocumentationTab.interface'; +import { DomainTypeWidget } from '../components/Domain/DomainTypeWidget/DomainTypeWidget'; import SubDomainsTable from '../components/Domain/SubDomainsTable/SubDomainsTable.component'; import EntitySummaryPanel from '../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import AssetsTabs from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; @@ -457,8 +457,10 @@ export const getDomainDetailTabs = ({ }; export const getDomainWidgetsFromKey = (widgetConfig: WidgetConfig) => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DOCUMENTATION)) { - return ; + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.EXPERTS)) { + return ; + } else if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DOMAIN_TYPE)) { + return ; } return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 2089007282e9..9b8c2a31679c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -107,7 +107,7 @@ class TableClassBase { static: false, }, { - h: 11, + h: 8, i: DetailPageWidgetKeys.TABLE_SCHEMA, w: 6, x: 0, From 9da8664f7ffa2feca66dae75b9a1fe9fde808b5e Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:51:16 +0530 Subject: [PATCH 33/63] fix data test id for tags and glossary terms --- .../src/main/resources/ui/playwright/utils/entity.ts | 2 +- .../src/main/resources/ui/playwright/utils/glossary.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 14c7b59b1b50..9aac8d32fa48 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -359,7 +359,7 @@ export const assignTag = async ( page: Page, tag: string, action: 'Add' | 'Edit' = 'Add', - parentId = 'entity-right-panel' + parentId = 'KnowledgePanel.Tags' ) => { await page .getByTestId(parentId) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 2f195d73b43b..5f05855a3ae5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -901,7 +901,7 @@ export const assignTagToGlossaryTerm = async ( page: Page, tag: string, action: 'Add' | 'Edit' = 'Add', - parentTestId = 'entity-right-panel' + parentTestId = 'KnowledgePanel.GlossaryTerms' ) => { await page .getByTestId(parentTestId) From 466e5cf1edba4687afa742c7a30356b0f87b089c Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:14:59 +0530 Subject: [PATCH 34/63] fix version pages --- .../APIEndpointVersion/APIEndpointVersion.tsx | 23 +++++++++++----- .../ContainerVersion.component.tsx | 23 +++++++++++----- .../DashboardVersion.component.tsx | 25 +++++++++++------ .../DataModelVersion.component.tsx | 23 +++++++++++----- .../DataProductsDetailsPage.component.tsx | 2 ++ .../StoredProcedureVersion.component.tsx | 23 +++++++++++----- .../TableVersion/TableVersion.component.tsx | 23 +++++++++++----- .../Metric/MetricVersion/MetricVersion.tsx | 23 +++++++++++----- .../MlModelVersion.component.tsx | 23 +++++++++++----- .../PipelineVersion.component.tsx | 23 +++++++++++----- .../SearchIndexVersion/SearchIndexVersion.tsx | 23 +++++++++++----- .../TopicVersion/TopicVersion.component.tsx | 23 +++++++++++----- .../APICollectionVersionPage.tsx | 27 ++++++++++++------- .../DatabaseSchemaVersionPage.tsx | 27 ++++++++++++------- .../DatabaseVersionPage.tsx | 27 ++++++++++++------- 15 files changed, 233 insertions(+), 105 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx index 514c5094df55..868943a07b81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx @@ -30,6 +30,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -201,13 +202,21 @@ const APIEndpointVersion: FC = ({ onVersionClick={backHandler} /> -
- - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerVersion/ContainerVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerVersion/ContainerVersion.component.tsx index 004428aabfcb..2f1eee755364 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerVersion/ContainerVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerVersion/ContainerVersion.component.tsx @@ -38,6 +38,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -247,13 +248,21 @@ const ContainerVersion: React.FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardVersion/DashboardVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardVersion/DashboardVersion.component.tsx index 600e23cc4ef3..9c49d2bcce33 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardVersion/DashboardVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardVersion/DashboardVersion.component.tsx @@ -41,6 +41,7 @@ import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -271,14 +272,22 @@ const DashboardVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx index ec26e995c383..b470373755cf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx @@ -38,6 +38,7 @@ import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomP import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import Loader from '../../../common/Loader/Loader'; import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -231,13 +232,21 @@ const DataModelVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx index 88fa75d70edd..3c3f9cc7c7de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx @@ -617,7 +617,9 @@ const DataProductsDetailsPage = ({ + currentVersionData={dataProduct} data={dataProduct} + isVersionView={isVersionsView} permissions={dataProductPermission} type={EntityType.DATA_PRODUCT} onUpdate={onUpdate}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx index d29008d79748..dee9174d290a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx @@ -31,6 +31,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -191,13 +192,21 @@ const StoredProcedureVersion = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx index dbe172b98d4d..a876efdb8fdf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx @@ -39,6 +39,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -252,13 +253,21 @@ const TableVersion: React.FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx index 154b2302f48a..90676844b647 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricVersion/MetricVersion.tsx @@ -30,6 +30,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -198,13 +199,21 @@ const MetricVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx index 64ee78840f2b..504fc108e3a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelVersion/MlModelVersion.component.tsx @@ -44,6 +44,7 @@ import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder import Loader from '../../common/Loader/Loader'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -349,13 +350,21 @@ const MlModelVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx index b1af6806c229..8b38be1d5683 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineVersion/PipelineVersion.component.tsx @@ -39,6 +39,7 @@ import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -285,13 +286,21 @@ const PipelineVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchIndexVersion/SearchIndexVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchIndexVersion/SearchIndexVersion.tsx index 186bbe837ffb..eef45e43a3b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchIndexVersion/SearchIndexVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchIndexVersion/SearchIndexVersion.tsx @@ -37,6 +37,7 @@ import { import { getUpdatedSearchIndexFields } from '../../utils/SearchIndexVersionUtils'; import Loader from '../common/Loader/Loader'; import TabsLabel from '../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../Customization/GenericProvider/GenericProvider'; import DataProductsContainer from '../DataProducts/DataProductsContainer/DataProductsContainer.component'; import VersionTable from '../Entity/VersionTable/VersionTable.component'; import { SearchIndexVersionProps } from './SearchIndexVersion.interface'; @@ -220,13 +221,21 @@ const SearchIndexVersion: React.FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx index 489091d3e6c9..8d7286e16ee1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicVersion/TopicVersion.component.tsx @@ -32,6 +32,7 @@ import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomProp import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -213,13 +214,21 @@ const TopicVersion: FC = ({ onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx index 8d6bbff49200..7fd90b48838d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx @@ -23,6 +23,7 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error import Loader from '../../components/common/Loader/Loader'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../components/DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../components/Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -386,15 +387,23 @@ const APICollectionVersionPage = () => { onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx index 48364230f97c..1f37c496d1b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx @@ -22,6 +22,7 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error import Loader from '../../components/common/Loader/Loader'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import DataProductsContainer from '../../components/DataProducts/DataProductsContainer/DataProductsContainer.component'; import EntityVersionTimeLine from '../../components/Entity/EntityVersionTimeLine/EntityVersionTimeLine'; @@ -339,15 +340,23 @@ function DatabaseSchemaVersionPage() { onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx index c1937c04daf5..084173e43545 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseVersionPage/DatabaseVersionPage.tsx @@ -22,6 +22,7 @@ import DescriptionV1 from '../../components/common/EntityDescription/Description import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import { DatabaseSchemaTable } from '../../components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable'; import DataProductsContainer from '../../components/DataProducts/DataProductsContainer/DataProductsContainer.component'; @@ -292,15 +293,23 @@ function DatabaseVersionPage() { onVersionClick={backHandler} /> - - - + Promise.resolve()}> + + + + )} From 223a05a8971617ee11c46b4c5146841f5cc01a6d Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:55:58 +0530 Subject: [PATCH 35/63] fix unit test --- .../DatabaseSchemaVersionPage.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx index 0a62e87fc452..ddc9367e12d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx @@ -162,6 +162,18 @@ jest.mock('react-router-dom', () => ({ })), })); +jest.mock( + '../../components/Customization/GenericProvider/GenericProvider', + () => ({ + GenericProvider: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), + useGenericContext: jest.fn().mockImplementation(() => ({ + data: {}, + })), + }) +); + describe('DatabaseSchemaVersionPage', () => { it('should render all necessary components', async () => { await act(async () => { From cf0b7fc9fc64b1ee515293f14446b28a0ba51cb7 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:49:26 +0530 Subject: [PATCH 36/63] update page enum with api collection --- .../src/main/resources/json/schema/system/ui/page.json | 4 +++- .../src/main/resources/ui/src/generated/system/ui/page.ts | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json index c2f2f247fe28..9b708d74556c 100644 --- a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json +++ b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json @@ -24,7 +24,9 @@ "SearchIndex", "Glossary", "GlossaryTerm", - "Domain" + "Domain", + "APICollection", + "APIEndpoint" ] } }, diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts index 534654586188..3b3612fad4b2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 Collate. + * Copyright 2025 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 @@ -10,9 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - - /** +/** * This schema defines the Page entity. A Page is a landing page, schema page to customize * in OpenMetadata. */ @@ -115,6 +113,8 @@ export enum EntityType { * This schema defines the type used for describing different types of pages. */ export enum PageType { + APICollection = "APICollection", + APIEndpoint = "APIEndpoint", Container = "Container", Dashboard = "Dashboard", DashboardDataModel = "DashboardDataModel", From d2629927749238c10888713258ccb8ef69629934 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:13:13 +0530 Subject: [PATCH 37/63] update custom property to use generic provider --- .../APIEndpointDetails/APIEndpointDetails.tsx | 1 - .../DashboardDetails.component.tsx | 1 - .../DataModels/DataModelDetails.component.tsx | 14 -------------- .../DataAssets/CommonWidgets/CommonWidgets.tsx | 5 ----- .../DataProductsDetailsPage.component.tsx | 1 - .../DocumentationTab.component.tsx | 1 - .../Entity/EntityRightPanel/EntityRightPanel.tsx | 2 -- .../GlossaryTerms/GlossaryTermsV1.component.tsx | 2 -- .../tabs/GlossaryOverviewTab.component.tsx | 9 +-------- .../Metric/MetricDetails/MetricDetails.tsx | 1 - .../MlModelDetail/MlModelDetail.component.tsx | 1 - .../ModalWithCustomPropertyEditor.component.tsx | 7 ------- .../Topic/TopicDetails/TopicDetails.component.tsx | 1 - .../CustomPropertyTable.interface.ts | 1 - .../CustomPropertyTable/CustomPropertyTable.tsx | 10 +++++----- .../ui/src/pages/ContainerPage/ContainerPage.tsx | 1 - .../DatabaseDetailsPage/DatabaseDetailsPage.tsx | 1 - .../DatabaseSchemaPage.component.tsx | 1 - .../TableDetailsPageV1/TableDetailsPageV1.tsx | 1 - .../ui/src/utils/ContainerDetailUtils.tsx | 2 -- .../ui/src/utils/ContainerDetailsClassBase.ts | 1 - .../ui/src/utils/DashboardDataModelBase.ts | 3 --- .../ui/src/utils/DashboardDataModelUtils.tsx | 2 -- .../ui/src/utils/DashboardDetailsClassBase.ts | 1 - .../ui/src/utils/DashboardDetailsUtils.tsx | 2 -- .../ui/src/utils/Database/Database.util.tsx | 2 -- .../ui/src/utils/Database/DatabaseClassBase.ts | 4 ---- .../ui/src/utils/DatabaseSchemaClassBase.ts | 1 - .../ui/src/utils/DatabaseSchemaDetailsUtils.tsx | 2 -- .../ui/src/utils/PipelineDetailsUtils.tsx | 2 -- .../resources/ui/src/utils/SearchIndexUtils.tsx | 2 -- .../ui/src/utils/StoredProceduresUtils.tsx | 2 -- .../main/resources/ui/src/utils/TableClassBase.ts | 1 - .../src/main/resources/ui/src/utils/TableUtils.tsx | 2 -- 34 files changed, 6 insertions(+), 84 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index 8fd26aafd0b9..e89901320af7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -378,7 +378,6 @@ const APIEndpointDetails: React.FC = ({
entityType={EntityType.API_ENDPOINT} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index be87adbe35f8..9db5fba7b96e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -237,7 +237,6 @@ const DashboardDetails = ({ dashboardDetails, deleted: deleted ?? false, handleFeedCount, - onExtensionUpdate, feedCount, activeTab, getEntityFeedCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index 2015d640680b..8a2b86e5adfa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -162,19 +162,6 @@ const DataModelDetails = ({ }; }, [dataModelPermissions, deleted]); - const handelExtensionUpdate = useCallback( - async (updatedDataModel: DashboardDataModel) => { - await onUpdateDataModel( - { - ...dataModelData, - extension: updatedDataModel.extension, - }, - 'extension' - ); - }, - [onUpdateDataModel, dataModelData] - ); - const tabs = useMemo(() => { const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); const allTabs = getDashboardDataModelDetailPageTabs({ @@ -185,7 +172,6 @@ const DataModelDetails = ({ dataModelData, dataModelPermissions, deleted: deleted ?? false, - handelExtensionUpdate, getEntityFeedCount, fetchDataModel, labelMap: tabLabelMap, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index a8a83456582a..5fa5ecec14a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -197,10 +197,6 @@ export const CommonWidgets = ({ widgetConfig.h, ]); - const handleExtensionUpdate = async (updatedTable: Table) => { - await onUpdate(updatedTable as unknown as GenericEntity); - }; - const widget = useMemo(() => { if (widgetConfig.i.startsWith(DetailPageWidgetKeys.DESCRIPTION)) { return descriptionWidget; @@ -223,7 +219,6 @@ export const CommonWidgets = ({ isRenderedInRightPanel entityType={entityType as EntityType.TABLE} - handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={Boolean(viewAllPermission)} maxDataCap={5} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx index 3c3f9cc7c7de..1f5f07d91b6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx @@ -480,7 +480,6 @@ const DataProductsDetailsPage = ({
entityType={EntityType.DATA_PRODUCT} - handleExtensionUpdate={handelExtensionUpdate} hasEditAccess={ (dataProductPermission.EditAll || dataProductPermission.EditCustomFields) && diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx index 3b0d4a1a8cd4..cc8fe7942a8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTabs/DocumentationTab/DocumentationTab.component.tsx @@ -212,7 +212,6 @@ const DocumentationTab = ({ isRenderedInRightPanel entityType={EntityType.DATA_PRODUCT} - handleExtensionUpdate={onUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={Boolean(viewAllPermission)} maxDataCap={5} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index 6034071c5950..aa81da425f5b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -58,7 +58,6 @@ const EntityRightPanel = ({ viewAllPermission, customProperties, editCustomAttributePermission, - onExtensionUpdate, }: EntityRightPanelProps) => { const KnowledgeArticles = entityRightPanelClassBase.getKnowLedgeArticlesWidget(); @@ -111,7 +110,6 @@ const EntityRightPanel = ({ isRenderedInRightPanel entityType={entityType as T} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={Boolean(viewAllPermission)} maxDataCap={5} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index 0d3c7970ca30..f074d676d4e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -194,7 +194,6 @@ const GlossaryTermsV1 = ({ !isVersionView && (permissions.EditAll || permissions.EditCustomFields) } - onExtensionUpdate={onExtensionUpdate} /> ), }, @@ -288,7 +287,6 @@ const GlossaryTermsV1 = ({
entityType={EntityType.GLOSSARY_TERM} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={ !isVersionView && (permissions.EditAll || permissions.EditCustomFields) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx index 949a74474dc8..63b85481451c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/GlossaryOverviewTab.component.tsx @@ -49,13 +49,9 @@ const ReactGridLayout = WidthProvider(RGL); type Props = { editCustomAttributePermission: boolean; - onExtensionUpdate: (updatedTable: GlossaryTerm) => Promise; }; -const GlossaryOverviewTab = ({ - editCustomAttributePermission, - onExtensionUpdate, -}: Props) => { +const GlossaryOverviewTab = ({ editCustomAttributePermission }: Props) => { const [tagsUpdating, setTagsUpdating] = useState(); const { currentPersonaDocStore } = useCustomizeStore(); // Since we are rendering this component for all customized tabs we need tab ID to get layout form store @@ -189,9 +185,6 @@ const GlossaryOverviewTab = ({ { - await onExtensionUpdate?.(updatedTable); - }} hasEditAccess={Boolean(editCustomAttributePermission)} hasPermission={hasViewAllPermission} maxDataCap={5} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index 440612e99ca2..e51e972ccd72 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -384,7 +384,6 @@ const MetricDetails: React.FC = ({
entityType={EntityType.METRIC} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index f908c418f6ad..5f21a84706bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -486,7 +486,6 @@ const MlModelDetail: FC = ({
entityType={EntityType.MLMODEL} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx index 2f4bdea7dba8..9b9d2d181b14 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx @@ -74,12 +74,6 @@ export const ModalWithCustomPropertyEditor = ({ setIsSaveLoading(false); }; - const onExtensionUpdate = async ( - data: ExtentionEntities[keyof ExtentionEntities] - ) => { - setExtensionObject(data.extension); - }; - useEffect(() => { fetchTypeDetail(); }, []); @@ -122,7 +116,6 @@ export const ModalWithCustomPropertyEditor = ({ hasPermission isRenderedInRightPanel entityType={entityType as keyof ExtentionEntities} - handleExtensionUpdate={onExtensionUpdate} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index 4490f5d89cdc..9f5f273e7e83 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -341,7 +341,6 @@ const TopicDetails: React.FC = ({
entityType={EntityType.TOPIC} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts index fead1780695e..f818b0f0c246 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts @@ -55,7 +55,6 @@ export type ExtentionEntitiesKeys = keyof ExtentionEntities; export interface CustomPropertyProps { isVersionView?: boolean; entityType: T; - handleExtensionUpdate?: (updatedTable: ExtentionEntities[T]) => Promise; hasEditAccess: boolean; className?: string; hasPermission: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx index df39f1dfdcd7..b068007fdd17 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx @@ -55,7 +55,6 @@ import { ExtensionTable } from './ExtensionTable'; import { PropertyValue } from './PropertyValue'; export const CustomPropertyTable = ({ - handleExtensionUpdate, entityType, hasEditAccess, className, @@ -66,7 +65,8 @@ export const CustomPropertyTable = ({ }: CustomPropertyProps) => { const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const { data: entityDetails } = useGenericContext(); + const { data: entityDetails, onUpdate } = + useGenericContext(); const [entityTypeDetail, setEntityTypeDetail] = useState({} as Type); const [entityTypeDetailLoading, setEntityTypeDetailLoading] = @@ -109,15 +109,15 @@ export const CustomPropertyTable = ({ const onExtensionUpdate = useCallback( async (updatedExtension: ExtentionEntities[T]) => { - if (!isUndefined(handleExtensionUpdate) && entityDetails) { + if (!isUndefined(onUpdate) && entityDetails) { const updatedData = { ...entityDetails, extension: updatedExtension, }; - await handleExtensionUpdate(updatedData); + await onUpdate(updatedData); } }, - [entityDetails, handleExtensionUpdate] + [entityDetails, onUpdate] ); const extensionObject: { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index cfe8b23473ee..684664d1a3e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -447,7 +447,6 @@ const ContainerPage = () => { containerData, fetchContainerDetail, labelMap: tabLabelMap, - handleExtensionUpdate, }); return getDetailsTabWithNewLabel( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index fedd6bda089c..41743f24e0ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -398,7 +398,6 @@ const DatabaseDetails: FunctionComponent = () => { feedCount, handleFeedCount, getEntityFeedCount, - settingsUpdateHandler, deleted: database.deleted ?? false, editCustomAttributePermission, getDetailsByFQN, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index c5111eda8472..c1def2b913c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -440,7 +440,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { viewAllPermission, databaseSchemaPermission, storedProcedureCount, - handleExtensionUpdate, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index a7ba0ac9cd8d..3e98d54e3f85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -496,7 +496,6 @@ const TableDetailsPageV1: React.FC = () => { deleted, tableDetails, totalFeedCount: feedCount.totalCount, - onExtensionUpdate, getEntityFeedCount, handleFeedCount, viewAllPermission, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx index ec9aa391ef42..602f20a970a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailUtils.tsx @@ -124,7 +124,6 @@ export const getContainerDetailPageTabs = ({ editLineagePermission, editCustomAttributePermission, viewAllPermission, - handleExtensionUpdate, feedCount, getEntityFeedCount, handleFeedCount, @@ -223,7 +222,6 @@ export const getContainerDetailPageTabs = ({
entityType={EntityType.CONTAINER} - handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index a91d04068b87..afced67b9e66 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -42,7 +42,6 @@ export interface ContainerDetailPageTabProps { editLineagePermission: boolean; editCustomAttributePermission: boolean; viewAllPermission: boolean; - handleExtensionUpdate: (updatedContainer: Container) => Promise; feedCount: { totalCount: number }; getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index 6e99aa563e7f..77b718a9f289 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -43,9 +43,6 @@ export interface DashboardDataModelDetailPageTabProps { dataModelData: DashboardDataModel; dataModelPermissions: OperationPermission; deleted: boolean; - handelExtensionUpdate: ( - dashboardDataModel: DashboardDataModel - ) => Promise; getEntityFeedCount: () => void; fetchDataModel: () => void; labelMap?: Record; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index b534f05a91f3..63e51faae84d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -40,7 +40,6 @@ export const getDashboardDataModelDetailPageTabs = ({ dataModelData, dataModelPermissions, deleted, - handelExtensionUpdate, getEntityFeedCount, fetchDataModel, labelMap, @@ -145,7 +144,6 @@ export const getDashboardDataModelDetailPageTabs = ({
entityType={EntityType.DASHBOARD_DATA_MODEL} - handleExtensionUpdate={handelExtensionUpdate} hasEditAccess={ dataModelPermissions.EditAll || dataModelPermissions.EditCustomFields diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 2b08c3dca968..2d0150f7ce5a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -41,7 +41,6 @@ export interface DashboardDetailsTabsProps { editCustomAttributePermission: boolean; viewAllPermission: boolean; handleFeedCount: (data: FeedCounts) => void; - onExtensionUpdate: (data: Dashboard) => Promise; feedCount: FeedCounts; activeTab: EntityTabs; deleted: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 65d01e7db8e7..f24cd53eb02c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -65,7 +65,6 @@ export const getDashboardDetailPageTabs = ({ editCustomAttributePermission, viewAllPermission, handleFeedCount, - onExtensionUpdate, feedCount, activeTab, deleted, @@ -127,7 +126,6 @@ export const getDashboardDetailPageTabs = ({
entityType={EntityType.DASHBOARD} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx index 2c2811758fe0..1c62a9fd10c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/Database.util.tsx @@ -139,7 +139,6 @@ export const getDatabasePageBaseTabs = ({ feedCount, handleFeedCount, getEntityFeedCount, - settingsUpdateHandler, editCustomAttributePermission, getDetailsByFQN, }: DatabaseDetailPageTabProps): TabProps[] => { @@ -190,7 +189,6 @@ export const getDatabasePageBaseTabs = ({
entityType={EntityType.DATABASE} - handleExtensionUpdate={settingsUpdateHandler} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} isVersionView={false} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index 54c1fc85357e..c88b768c5b31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -45,10 +45,6 @@ export interface DatabaseDetailPageTabProps { feedCount: FeedCounts; handleFeedCount: (data: FeedCounts) => void; getEntityFeedCount: () => void; - settingsUpdateHandler: ( - data: Database, - key?: keyof Database - ) => Promise; deleted: boolean; editCustomAttributePermission: boolean; getDetailsByFQN: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 517bc581954f..c98c5635713b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -45,7 +45,6 @@ export interface DatabaseSchemaPageTabProps { viewAllPermission: boolean; databaseSchemaPermission: OperationPermission; storedProcedureCount: number; - handleExtensionUpdate: (schema: DatabaseSchema) => Promise; getEntityFeedCount: () => void; fetchDatabaseSchemaDetails: () => Promise; handleFeedCount: (data: FeedCounts) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx index 7b874e39ddc5..e3da24027b32 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaDetailsUtils.tsx @@ -37,7 +37,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ editCustomAttributePermission, viewAllPermission, storedProcedureCount, - handleExtensionUpdate, getEntityFeedCount, fetchDatabaseSchemaDetails, handleFeedCount, @@ -104,7 +103,6 @@ export const getDataBaseSchemaPageBaseTabs = ({ className="" entityType={EntityType.DATABASE_SCHEMA} - handleExtensionUpdate={handleExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} isVersionView={false} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index 9727f32c6b8c..f1c4ed63463f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -57,7 +57,6 @@ export const getPipelineDetailPageTabs = ({ feedCount, getEntityFeedCount, handleFeedCount, - onExtensionUpdate, pipelineDetails, pipelineFQN, viewAllPermission, @@ -135,7 +134,6 @@ export const getPipelineDetailPageTabs = ({
entityType={EntityType.PIPELINE} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx index ae7cb1265f05..03f0c928ea69 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx @@ -60,7 +60,6 @@ export const getSearchIndexDetailsTabs = ({ deleted, editLineagePermission, editCustomAttributePermission, - onExtensionUpdate, labelMap, }: SearchIndexDetailPageTabProps) => { return [ @@ -172,7 +171,6 @@ export const getSearchIndexDetailsTabs = ({
entityType={EntityType.SEARCH_INDEX} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 06424cbe680b..6a8cb538527b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -38,7 +38,6 @@ export const getStoredProcedureDetailsPageTabs = ({ editLineagePermission, editCustomAttributePermission, viewAllPermission, - onExtensionUpdate, getEntityFeedCount, fetchStoredProcedureDetails, handleFeedCount, @@ -102,7 +101,6 @@ export const getStoredProcedureDetailsPageTabs = ({ children: storedProcedure && ( entityType={EntityType.STORED_PROCEDURE} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 9b8c2a31679c..5996009c7f8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -60,7 +60,6 @@ export interface TableDetailPageTabProps { testCaseSummary?: TestSummary; getEntityFeedCount: () => void; fetchTableDetails: () => Promise; - onExtensionUpdate: (updatedData: Table) => Promise; handleFeedCount: (data: FeedCounts) => void; labelMap?: Record; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index a8b19d06108e..789c93bb3292 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -745,7 +745,6 @@ export const getTableDetailPageBaseTabs = ({ deleted, tableDetails, totalFeedCount, - onExtensionUpdate, getEntityFeedCount, handleFeedCount, viewAllPermission, @@ -950,7 +949,6 @@ export const getTableDetailPageBaseTabs = ({
entityType={EntityType.TABLE} - handleExtensionUpdate={onExtensionUpdate} hasEditAccess={editCustomAttributePermission} hasPermission={viewAllPermission} /> From 238e1fb2653ff83b1c6acb1ee790eea9c89047af Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:54:42 +0530 Subject: [PATCH 38/63] fix api collection & endpoint --- .../APIEndpointDetails/APIEndpointDetails.tsx | 274 +++----------- .../APIEndpointSchema/APIEndpointSchema.tsx | 14 +- .../APIEndpointVersion/APIEndpointVersion.tsx | 6 +- .../GenericProvider/GenericProvider.tsx | 4 +- .../ui/src/enums/CustomizeDetailPage.enum.ts | 2 + .../APICollectionPage/APICollectionPage.tsx | 354 +++++------------- .../APICollectionVersionPage.tsx | 11 +- .../APICollectionPage/APIEndpointsTab.tsx | 149 +++++--- .../resources/ui/src/rest/apiEndpointsAPI.ts | 2 +- .../APICollection/APICollectionClassBase.ts | 195 ++++++++++ .../APICollection/APICollectionUtils.tsx | 119 ++++++ .../APIEndpoints/APIEndpointClassBase.ts | 341 +++++++++++++++++ .../utils/APIEndpoints/APIEndpointUtils.tsx | 131 +++++++ 13 files changed, 1033 insertions(+), 569 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index e89901320af7..ca2eaad83d6a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -13,52 +13,42 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { APIEndpoint } from '../../../generated/entity/data/apiEndpoint'; -import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { TagLabel } from '../../../generated/type/schema'; +import { Page, PageType } from '../../../generated/system/ui/page'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { restoreApiEndPoint } from '../../../rest/apiEndpointsAPI'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; +import apiEndpointClassBase from '../../../utils/APIEndpoints/APIEndpointClassBase'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { - getEntityName, - getEntityReferenceFromEntity, -} from '../../../utils/EntityUtils'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import { getEntityName } from '../../../utils/EntityUtils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; +import { updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../SearchedData/SearchedData.interface'; -import APIEndpointSchema from '../APIEndpointSchema/APIEndpointSchema'; import { APIEndpointDetailsProps } from './APIEndpointDetails.interface'; const APIEndpointDetails: React.FC = ({ apiEndpointDetails, apiEndpointPermissions, - fetchAPIEndpointDetails, onFollowApiEndPoint, onApiEndpointUpdate, @@ -69,7 +59,7 @@ const APIEndpointDetails: React.FC = ({ onUpdateVote, }: APIEndpointDetailsProps) => { const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedApiEndpointFqn } = useFqn(); @@ -77,15 +67,12 @@ const APIEndpointDetails: React.FC = ({ const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); + const [customizedPage, setCustomizedPage] = useState(null); const { owners, deleted, - description, followers = [], - entityName, - apiEndpointTags, - tier, } = useMemo( () => ({ ...apiEndpointDetails, @@ -114,12 +101,6 @@ const APIEndpointDetails: React.FC = ({ }; await onApiEndpointUpdate(updatedData, 'displayName'); }; - const onExtensionUpdate = async (updatedData: APIEndpoint) => { - await onApiEndpointUpdate( - { ...apiEndpointDetails, extension: updatedData.extension }, - 'extension' - ); - }; const handleRestoreApiEndpoint = async () => { try { @@ -155,19 +136,6 @@ const APIEndpointDetails: React.FC = ({ } }; - const onDescriptionUpdate = async (updatedHTML: string) => { - if (description !== updatedHTML) { - const updatedApiEndpointDetails = { - ...apiEndpointDetails, - description: updatedHTML, - }; - try { - await onApiEndpointUpdate(updatedApiEndpointDetails, 'description'); - } catch (error) { - showErrorToast(error as AxiosError); - } - } - }; const onOwnerUpdate = useCallback( async (newOwners?: APIEndpoint['owners']) => { const updatedApiEndpointDetails = { @@ -189,29 +157,6 @@ const APIEndpointDetails: React.FC = ({ return onApiEndpointUpdate(updatedApiEndpointDetails, 'tags'); }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - - if (updatedTags && apiEndpointDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedApiEndpoint = { ...apiEndpointDetails, tags: updatedTags }; - await onApiEndpointUpdate(updatedApiEndpoint, 'tags'); - } - }; - - const onDataProductsUpdate = async (updatedData: DataProduct[]) => { - const dataProductsEntity = updatedData?.map((item) => { - return getEntityReferenceFromEntity(item, EntityType.DATA_PRODUCT); - }); - - const updatedApiEndpointDetails = { - ...apiEndpointDetails, - dataProducts: dataProductsEntity, - }; - - await onApiEndpointUpdate(updatedApiEndpointDetails, 'dataProducts'); - }; - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -230,38 +175,19 @@ const APIEndpointDetails: React.FC = ({ ); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, - editAllPermission, editLineagePermission, - viewSampleDataPermission, viewAllPermission, } = useMemo( () => ({ - editTagsPermission: - (apiEndpointPermissions.EditTags || apiEndpointPermissions.EditAll) && - !deleted, - editGlossaryTermsPermission: - (apiEndpointPermissions.EditGlossaryTerms || - apiEndpointPermissions.EditAll) && - !deleted, - editDescriptionPermission: - (apiEndpointPermissions.EditDescription || - apiEndpointPermissions.EditAll) && - !deleted, editCustomAttributePermission: (apiEndpointPermissions.EditAll || apiEndpointPermissions.EditCustomFields) && !deleted, - editAllPermission: apiEndpointPermissions.EditAll && !deleted, editLineagePermission: (apiEndpointPermissions.EditAll || apiEndpointPermissions.EditLineage) && !deleted, - viewSampleDataPermission: - apiEndpointPermissions.ViewAll || apiEndpointPermissions.ViewSampleData, viewAllPermission: apiEndpointPermissions.ViewAll, }), [apiEndpointPermissions, deleted] @@ -271,145 +197,55 @@ const APIEndpointDetails: React.FC = ({ getEntityFeedCount(); }, [apiEndpointPermissions, decodedApiEndpointFqn]); - const tabs = useMemo( - () => [ - { - label: , - key: EntityTabs.SCHEMA, - children: ( - -
- - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityType={EntityType.API_ENDPOINT} - selectedTags={apiEndpointTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: apiEndpointDetails && ( -
- - entityType={EntityType.API_ENDPOINT} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - - [ + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); + const tabs = apiEndpointClassBase.getAPIEndpointDetailPageTabs({ activeTab, - feedCount.totalCount, - apiEndpointTags, - entityName, - apiEndpointDetails, - decodedApiEndpointFqn, + feedCount, + apiEndpoint: apiEndpointDetails, fetchAPIEndpointDetails, - deleted, + getEntityFeedCount, + labelMap: tabLabelMap, handleFeedCount, - onExtensionUpdate, - handleTagSelection, - onDescriptionUpdate, - onDataProductsUpdate, - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, - editLineagePermission, - editAllPermission, - viewSampleDataPermission, viewAllPermission, - ] - ); + editLineagePermission, + }); + + return getDetailsTabWithNewLabel( + tabs, + customizedPage?.tabs, + EntityTabs.SCHEMA + ); + }, [ + activeTab, + feedCount, + apiEndpointDetails, + fetchAPIEndpointDetails, + getEntityFeedCount, + handleFeedCount, + editCustomAttributePermission, + viewAllPermission, + editLineagePermission, + ]); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.APIEndpoint) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); return ( = ({ - apiEndpointDetails, - permissions, - onApiEndpointUpdate, isVersionView = false, }) => { const { theme } = useApplicationStore(); @@ -83,6 +76,11 @@ const APIEndpointSchema: FC = ({ const [viewType, setViewType] = useState( SchemaViewType.REQUEST_SCHEMA ); + const { + data: apiEndpointDetails, + permissions, + onUpdate: onApiEndpointUpdate, + } = useGenericContext(); const { requestSchemaAllRowKeys, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx index 868943a07b81..4a7266b3d55f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointVersion/APIEndpointVersion.tsx @@ -124,11 +124,7 @@ const APIEndpointVersion: FC = ({ />
- + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx index c0ff62af7be5..69472ff40c77 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericProvider/GenericProvider.tsx @@ -28,7 +28,7 @@ interface GenericProviderProps> { children?: React.ReactNode; data: T; type: EntityType; - onUpdate: (updatedData: T) => Promise; + onUpdate: (updatedData: T, key?: keyof T) => Promise; isVersionView?: boolean; permissions: OperationPermission; currentVersionData?: T; @@ -37,7 +37,7 @@ interface GenericProviderProps> { interface GenericContextType> { data: T; type: EntityType; - onUpdate: (updatedData: T) => Promise; + onUpdate: (updatedData: T, key?: keyof T) => Promise; isVersionView?: boolean; permissions: OperationPermission; currentVersionData?: T; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index ddb87f978ab2..58235b7717d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -43,6 +43,8 @@ export enum DetailPageWidgetKeys { EXPERTS = 'KnowledgePanel.Experts', DOMAIN_TYPE = 'KnowledgePanel.DomainType', TABLES = 'KnowledgePanel.Tables', + API_ENDPOINTS = 'KnowledgePanel.APIEndpoints', + API_SCHEMA = 'KnowledgePanel.APISchema', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index 31a95eff2059..20d249b76fcd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -15,7 +15,6 @@ import { Col, Row, Skeleton, Tabs, TabsProps } from 'antd'; import { AxiosError } from 'axios'; import { compare, Operation } from 'fast-json-patch'; import { isEmpty, isUndefined } from 'lodash'; -import { EntityTags } from 'Models'; import React, { FunctionComponent, useCallback, @@ -25,30 +24,21 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; -import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; -import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; -import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; -import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; +import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, - INITIAL_PAGING_VALUE, - PAGE_SIZE, ROUTES, } from '../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, @@ -63,11 +53,11 @@ import { } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { APICollection } from '../../generated/entity/data/apiCollection'; -import { APIEndpoint } from '../../generated/entity/data/apiEndpoint'; +import { Page, PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; -import { TagLabel } from '../../generated/type/tagLabel'; -import { usePaging } from '../../hooks/paging/usePaging'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useTableFilters } from '../../hooks/useTableFilters'; import { FeedCounts } from '../../interface/feed.interface'; import { getApiCollectionByFQN, @@ -75,32 +65,25 @@ import { restoreApiCollection, updateApiCollectionVote, } from '../../rest/apiCollectionsAPI'; -import { - getApiEndPoints, - GetApiEndPointsType, -} from '../../rest/apiEndpointsAPI'; +import { getApiEndPoints } from '../../rest/apiEndpointsAPI'; +import { getDocumentByFQN } from '../../rest/DocStoreAPI'; +import apiCollectionClassBase from '../../utils/APICollection/APICollectionClassBase'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../utils/CustomizePage/CustomizePageUtils'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../utils/TagsUtils'; +import { updateTierTag } from '../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; -import APIEndpointsTab from './APIEndpointsTab'; const APICollectionPage: FunctionComponent = () => { const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const pagingInfo = usePaging(PAGE_SIZE); - - const { - paging, - pageSize, - handlePagingChange, - handlePageChange, - currentPage, - } = pagingInfo; - + const [customizedPage, setCustomizedPage] = useState(null); + const { selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.API_ENDPOINT } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedAPICollectionFQN } = useFqn(); @@ -110,19 +93,19 @@ const APICollectionPage: FunctionComponent = () => { const [apiCollection, setAPICollection] = useState( {} as APICollection ); - const [apiEndpoints, setAPIEndpoints] = useState>([]); - const [apiEndpointsLoading, setAPIEndpointsLoading] = useState(true); const [isAPICollectionLoading, setIsAPICollectionLoading] = useState(true); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); + const [apiEndpointCount, setApiEndpointCount] = useState(0); const [apiCollectionPermission, setAPICollectionPermission] = useState(DEFAULT_ENTITY_PERMISSION); - const [showDeletedEndpoints, setShowDeletedEndpoints] = - useState(false); + const { filters, setFilters } = useTableFilters({ + showDeletedEndpoints: false, + }); const extraDropdownContent = useMemo( () => @@ -134,16 +117,9 @@ const APICollectionPage: FunctionComponent = () => { [apiCollectionPermission, decodedAPICollectionFQN] ); - const handleShowDeletedEndPoints = (value: boolean) => { - setShowDeletedEndpoints(value); - handlePageChange(INITIAL_PAGING_VALUE); - }; - - const { currentVersion, tags, tier, apiCollectionId } = useMemo( + const { currentVersion, apiCollectionId } = useMemo( () => ({ currentVersion: apiCollection?.version, - tier: getTierTags(apiCollection.tags ?? []), - tags: getTagsWithoutTier(apiCollection.tags ?? []), apiCollectionId: apiCollection?.id ?? '', }), [apiCollection] @@ -190,7 +166,9 @@ const APICollectionPage: FunctionComponent = () => { include: Include.All, }); setAPICollection(response); - setShowDeletedEndpoints(response.deleted ?? false); + setFilters({ + showDeletedEndpoints: response.deleted ?? false, + }); } catch (err) { // Error if ((err as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) { @@ -201,31 +179,22 @@ const APICollectionPage: FunctionComponent = () => { } }, [decodedAPICollectionFQN]); - const getAPICollectionEndpoints = useCallback( - async (params?: Pick) => { - if (!apiCollection) { - return; - } - - setAPIEndpointsLoading(true); - try { - const res = await getApiEndPoints({ - ...params, - fields: TabSpecificField.OWNERS, - apiCollection: decodedAPICollectionFQN, - service: apiCollection?.service?.fullyQualifiedName ?? '', - include: showDeletedEndpoints ? Include.Deleted : Include.NonDeleted, - }); - setAPIEndpoints(res.data); - handlePagingChange(res.paging); - } catch (err) { - showErrorToast(err as AxiosError); - } finally { - setAPIEndpointsLoading(false); - } - }, - [decodedAPICollectionFQN, showDeletedEndpoints, apiCollection] - ); + const getApiEndpointCount = useCallback(async () => { + const res = await getApiEndPoints({ + apiCollection: decodedAPICollectionFQN, + service: apiCollection?.service?.fullyQualifiedName ?? '', + paging: { limit: 0 }, + include: filters.showDeletedEndpoints + ? Include.Deleted + : Include.NonDeleted, + }); + setApiEndpointCount(res.paging.total); + }, [ + decodedAPICollectionFQN, + filters.showDeletedEndpoints, + apiCollection, + apiCollection?.service?.fullyQualifiedName, + ]); const saveUpdatedAPICollectionData = useCallback( (updatedData: APICollection) => { @@ -239,31 +208,6 @@ const APICollectionPage: FunctionComponent = () => { [apiCollectionId, apiCollection] ); - const onDescriptionUpdate = useCallback( - async (updatedHTML: string) => { - if (apiCollection?.description !== updatedHTML && apiCollection) { - const updatedAPICollectionDetails = { - ...apiCollection, - description: updatedHTML, - }; - - try { - const response = await saveUpdatedAPICollectionData( - updatedAPICollectionDetails - ); - if (response) { - setAPICollection(response); - } else { - throw t('server.unexpected-response'); - } - } catch (error) { - showErrorToast(error as AxiosError); - } - } - }, - [apiCollection] - ); - const activeTabHandler = useCallback( (activeKey: string) => { if (activeKey !== activeTab) { @@ -304,27 +248,6 @@ const APICollectionPage: FunctionComponent = () => { [apiCollection, apiCollection?.owners] ); - const handleTagsUpdate = async (selectedTags?: Array) => { - if (selectedTags) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedData = { ...apiCollection, tags: updatedTags }; - - try { - const res = await saveUpdatedAPICollectionData( - updatedData as APICollection - ); - setAPICollection(res); - } catch (error) { - showErrorToast(error as AxiosError, t('server.api-error')); - } - } - }; - - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - await handleTagsUpdate(updatedTags); - }; - const handleUpdateTier = useCallback( async (newTier?: Tag) => { const tierTag = updateTierTag(apiCollection?.tags ?? [], newTier); @@ -364,14 +287,16 @@ const APICollectionPage: FunctionComponent = () => { return prev; } + setFilters({ + showDeletedEndpoints: !prev.deleted, + }); + return { ...prev, deleted: !prev?.deleted, ...(version ? { version } : {}), }; }); - - setShowDeletedEndpoints((prev) => !prev); }; const handleRestoreAPICollection = useCallback(async () => { @@ -396,20 +321,6 @@ const APICollectionPage: FunctionComponent = () => { } }, [apiCollectionId]); - const endpointPaginationHandler = useCallback( - ({ cursorType, currentPage }: PagingHandlerParams) => { - if (cursorType) { - getAPICollectionEndpoints({ - paging: { - [cursorType]: paging[cursorType], - }, - }); - } - handlePageChange(currentPage); - }, - [paging, getAPICollectionEndpoints] - ); - const versionHandler = useCallback(() => { currentVersion && history.push( @@ -450,25 +361,16 @@ const APICollectionPage: FunctionComponent = () => { useEffect(() => { if (viewAPICollectionPermission && decodedAPICollectionFQN) { - getAPICollectionEndpoints({ - paging: { limit: pageSize }, - }); + getApiEndpointCount(); } }, [ - showDeletedEndpoints, + filters.showDeletedEndpoints, decodedAPICollectionFQN, viewAPICollectionPermission, apiCollection, - pageSize, ]); - const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, - viewAllPermission, - } = useMemo( + const { editCustomAttributePermission, viewAllPermission } = useMemo( () => ({ editTagsPermission: (apiCollectionPermission.EditTags || apiCollectionPermission.EditAll) && @@ -490,23 +392,6 @@ const APICollectionPage: FunctionComponent = () => { [apiCollectionPermission, apiCollection] ); - const handleExtensionUpdate = async (apiCollectionData: APICollection) => { - const response = await saveUpdatedAPICollectionData({ - ...apiCollection, - extension: apiCollectionData.extension, - }); - setAPICollection((prev) => { - if (!prev) { - return prev; - } - - return { - ...prev, - extension: response.extension, - }; - }); - }; - const handleAPICollectionUpdate = async (updatedData: APICollection) => { const response = await saveUpdatedAPICollectionData({ ...apiCollection, @@ -515,113 +400,28 @@ const APICollectionPage: FunctionComponent = () => { setAPICollection(response); }; - const tabs: TabsProps['items'] = [ - { - label: ( - - ), - key: EntityTabs.API_ENDPOINT, - children: ( - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={editGlossaryTermsPermission} - editTagPermission={editTagsPermission} - entityType={EntityType.API_COLLECTION} - selectedTags={tags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={handleExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: apiCollection && ( -
- - className="" - entityType={EntityType.API_COLLECTION} - handleExtensionUpdate={handleExtensionUpdate} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - isVersionView={false} - /> -
- ), - }, - ]; + const tabs: TabsProps['items'] = useMemo(() => { + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); + + const tabs = apiCollectionClassBase.getAPICollectionDetailPageTabs({ + activeTab, + feedCount, + apiCollection, + fetchAPICollectionDetails, + getEntityFeedCount, + handleFeedCount, + editCustomAttributePermission, + viewAllPermission, + apiEndpointCount, + labelMap: tabLabelMap, + }); + + return getDetailsTabWithNewLabel( + tabs, + customizedPage?.tabs, + EntityTabs.API_ENDPOINT + ); + }, [activeTab]); const updateVote = async (data: QueryVote, id: string) => { try { @@ -636,6 +436,26 @@ const APICollectionPage: FunctionComponent = () => { } }; + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find( + (p: Page) => p.pageType === PageType.APICollection + ) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); + if (isPermissionsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx index 7fd90b48838d..71f157d6f6ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx @@ -284,16 +284,7 @@ const APICollectionVersionPage = () => { children: (
- + Promise; - onShowDeletedEndpointsChange?: (value: boolean) => void; isVersionView?: boolean; - pagingInfo: UsePagingInterface; } function APIEndpointsTab({ - apiCollectionDetails, - apiEndpointsLoading, - description, - editDescriptionPermission = false, - apiEndpoints, - currentEndpointsPage, - endpointPaginationHandler, - onDescriptionUpdate, - showDeletedEndpoints = false, - onShowDeletedEndpointsChange, isVersionView = false, - pagingInfo, }: Readonly) { const { t } = useTranslation(); + const { fqn: decodedAPICollectionFQN } = useFqn(); + const { data: apiCollection } = useGenericContext(); + const [apiEndpoints, setAPIEndpoints] = useState([]); + const [apiEndpointsLoading, setAPIEndpointsLoading] = + useState(false); + const { + paging, + handlePageChange, + currentPage, + showPagination, + pageSize, + handlePagingChange, + handlePageSizeChange, + } = usePaging(PAGE_SIZE); + const { filters, setFilters } = useTableFilters({ + showDeletedEndpoints: false, + }); + + const getAPICollectionEndpoints = useCallback( + async (params?: Pick) => { + if (!apiCollection) { + return; + } + + setAPIEndpointsLoading(true); + try { + const res = await getApiEndPoints({ + ...params, + fields: TabSpecificField.OWNERS, + apiCollection: decodedAPICollectionFQN, + service: apiCollection?.service?.fullyQualifiedName ?? '', + include: filters.showDeletedEndpoints + ? Include.Deleted + : Include.NonDeleted, + }); + setAPIEndpoints(res.data); + handlePagingChange(res.paging); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setAPIEndpointsLoading(false); + } + }, + [decodedAPICollectionFQN, filters.showDeletedEndpoints, apiCollection] + ); const tableColumn: ColumnsType = useMemo( () => [ @@ -109,36 +141,39 @@ function APIEndpointsTab({ [] ); + const handleEndpointsPagination = useCallback( + ({ cursorType, currentPage }: PagingHandlerParams) => { + if (cursorType) { + getAPICollectionEndpoints({ + paging: { + [cursorType]: paging[cursorType], + }, + }); + } + handlePageChange(currentPage); + }, + [paging, getAPICollectionEndpoints] + ); + + useEffect(() => { + getAPICollectionEndpoints(); + }, []); + return ( - - {isVersionView ? ( - - ) : ( - - )} - {!isVersionView && ( + setFilters({ + ...filters, + showDeletedEndpoints: !filters.showDeletedEndpoints, + }) + } /> {t('label.deleted')} @@ -168,15 +203,15 @@ function APIEndpointsTab({ size="small" /> - {!isUndefined(pagingInfo) && pagingInfo.showPagination && ( + {showPagination && ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/apiEndpointsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/apiEndpointsAPI.ts index f8907aad8ee5..91ba3d3af9ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/apiEndpointsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/apiEndpointsAPI.ts @@ -26,7 +26,7 @@ import APIClient from './index'; export type GetApiEndPointsType = { service: string; apiCollection: string; - fields: string; + fields?: string; paging?: PagingWithoutTotal; include?: Include; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts new file mode 100644 index 000000000000..0e4db3dd5156 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts @@ -0,0 +1,195 @@ +/* + * Copyright 2024 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 { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { + APICollection, + APIServiceType, +} from '../../generated/entity/data/apiCollection'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { FeedCounts } from '../../interface/feed.interface'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; +import { + getApiCollectionDetailsPageTabs, + getApiCollectionWidgetsFromKey, +} from './APICollectionUtils'; + +export interface APICollectionDetailPageTabProps { + activeTab: EntityTabs; + feedCount: { + totalCount: number; + }; + apiCollection: APICollection; + fetchAPICollectionDetails: () => Promise; + getEntityFeedCount: () => void; + handleFeedCount: (data: FeedCounts) => void; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + apiEndpointCount: number; + labelMap: Record; +} + +class APICollectionClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getAPICollectionDetailPageTabs( + apiCollectionDetailsPageProps: APICollectionDetailPageTabProps + ): TabProps[] { + return getApiCollectionDetailsPageTabs(apiCollectionDetailsPageProps); + } + + public getAPICollectionDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.API_ENDPOINT, + EntityTabs.ACTIVITY_FEED, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.API_ENDPOINT, + })); + } + + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.API_ENDPOINT) { + return []; + } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.API_ENDPOINTS, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + } + public getDummyData(): APICollection { + return { + id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', + name: 'pet', + fullyQualifiedName: 'sample_api_service.pet', + version: 0.1, + updatedAt: 1722588116104, + updatedBy: 'ingestion-bot', + endpointURL: 'https://petstore3.swagger.io/#/pet', + href: 'http://sandbox-beta.open-metadata.org/api/v1/apiCollections/db03ef8f-82f9-4a23-a940-3ba5af5bba29', + owners: [], + tags: [], + service: { + id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', + type: 'apiService', + name: 'sample_api_service', + fullyQualifiedName: 'sample_api_service', + displayName: 'sample_api_service', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/apiServices/449b7937-c4ca-4dce-866c-5f6d0acc45c1', + }, + serviceType: APIServiceType.REST, + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.API_ENDPOINTS, + name: i18n.t('label.api-endpoint'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getApiCollectionWidgetsFromKey(widgetConfig); + } +} + +const apiCollectionClassBase = new APICollectionClassBase(); + +export default apiCollectionClassBase; +export { APICollectionClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx new file mode 100644 index 000000000000..cfec89511e70 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2025 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 React from 'react'; +import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../../components/DataAssets/CommonWidgets/CommonWidgets'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs, EntityType } from '../../enums/entity.enum'; +import { PageType } from '../../generated/system/ui/page'; +import APIEndpointsTab from '../../pages/APICollectionPage/APIEndpointsTab'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import i18n from '../i18next/LocalUtil'; +import { APICollectionDetailPageTabProps } from './APICollectionClassBase'; + +export const getApiCollectionDetailsPageTabs = ({ + activeTab, + feedCount, + apiCollection, + fetchAPICollectionDetails, + getEntityFeedCount, + handleFeedCount, + editCustomAttributePermission, + viewAllPermission, + apiEndpointCount, + labelMap, +}: APICollectionDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.API_ENDPOINT, + children: , + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: apiCollection && ( +
+ + className="" + entityType={EntityType.API_COLLECTION} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + isVersionView={false} + /> +
+ ), + }, + ]; +}; + +export const getApiCollectionWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.API_ENDPOINTS)) { + return ; + } + + return ( + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts new file mode 100644 index 000000000000..50a3ed0a4e93 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts @@ -0,0 +1,341 @@ +/* + * Copyright 2024 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 { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { + APIEndpoint, + APIRequestMethod, + APIServiceType, + DataTypeTopic, + SchemaType, +} from '../../generated/entity/data/apiEndpoint'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { FeedCounts } from '../../interface/feed.interface'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; +import { + getApiEndpointDetailsPageTabs, + getApiEndpointWidgetsFromKey, +} from './APIEndpointUtils'; + +export interface APIEndpointDetailPageTabProps { + activeTab: EntityTabs; + feedCount: { + totalCount: number; + }; + apiEndpoint: APIEndpoint; + fetchAPIEndpointDetails: () => void; + getEntityFeedCount: () => void; + handleFeedCount: (data: FeedCounts) => void; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + editLineagePermission: boolean; + labelMap: Record; +} + +class APIEndpointClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getAPIEndpointDetailPageTabs( + apiEndpointDetailsPageProps: APIEndpointDetailPageTabProps + ): TabProps[] { + return getApiEndpointDetailsPageTabs(apiEndpointDetailsPageProps); + } + + public getEndpointDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.SCHEMA, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.SCHEMA, + })); + } + + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.SCHEMA) { + return []; + } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.API_SCHEMA, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + } + public getDummyData(): APIEndpoint { + return { + id: 'b41d3506-09ac-4e02-ae40-8d6933f6a77f', + name: 'addPet', + displayName: 'Add Pet', + fullyQualifiedName: 'sample_api_service.pet.addPet', + description: 'add a new pet', + version: 0.5, + updatedAt: 1723268606694, + updatedBy: 'sachin', + endpointURL: 'https://petstore3.swagger.io/#/pet/addPet', + requestMethod: APIRequestMethod.Post, + requestSchema: { + schemaType: SchemaType.JSON, + schemaFields: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of pet that needs to be updated', + fullyQualifiedName: 'sample_api_service.pet.addPet.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.name', + tags: [], + }, + { + name: 'category', + dataType: DataTypeTopic.Record, + description: 'Category of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.category', + tags: [], + children: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of category', + fullyQualifiedName: + 'sample_api_service.pet.addPet.category.name', + tags: [], + }, + ], + }, + { + name: 'photoUrls', + dataType: DataTypeTopic.Array, + description: "URLs of pet's photos", + fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', + tags: [], + }, + { + name: 'tags', + dataType: DataTypeTopic.Array, + description: 'Tags of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.tags', + tags: [], + }, + { + name: 'status', + dataType: DataTypeTopic.String, + description: 'Status of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.status', + tags: [], + }, + ], + }, + responseSchema: { + schemaType: SchemaType.JSON, + schemaFields: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of pet that needs to be updated', + fullyQualifiedName: 'sample_api_service.pet.addPet.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.name', + tags: [], + }, + { + name: 'category', + dataType: DataTypeTopic.Record, + description: 'Category of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.category', + tags: [], + children: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of category', + fullyQualifiedName: + 'sample_api_service.pet.addPet.category.name', + tags: [], + }, + ], + }, + { + name: 'photoUrls', + dataType: DataTypeTopic.Array, + description: "URLs of pet's photos", + fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', + tags: [], + }, + { + name: 'tags', + dataType: DataTypeTopic.Array, + description: 'Tags of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.tags', + tags: [], + }, + { + name: 'status', + dataType: DataTypeTopic.String, + description: 'Status of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.status', + tags: [], + }, + ], + }, + apiCollection: { + id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', + type: 'apiCollection', + name: 'pet', + fullyQualifiedName: 'sample_api_service.pet', + displayName: 'pet', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/apiCollections/db03ef8f-82f9-4a23-a940-3ba5af5bba29', + }, + href: 'http://sandbox-beta.open-metadata.org/api/v1/apiEndpoints/b41d3506-09ac-4e02-ae40-8d6933f6a77f', + owners: [], + followers: [], + tags: [], + service: { + id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', + type: 'apiService', + name: 'sample_api_service', + fullyQualifiedName: 'sample_api_service', + displayName: 'sample_api_service', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/apiServices/449b7937-c4ca-4dce-866c-5f6d0acc45c1', + }, + serviceType: APIServiceType.REST, + + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.API_ENDPOINTS, + name: i18n.t('label.api-endpoint'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + CUSTOM_PROPERTIES_WIDGET, + ]; + } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getApiEndpointWidgetsFromKey(widgetConfig); + } +} + +const apiEndpointClassBase = new APIEndpointClassBase(); + +export default apiEndpointClassBase; +export { APIEndpointClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointUtils.tsx new file mode 100644 index 000000000000..97c068722a53 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointUtils.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2025 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 React from 'react'; +import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import APIEndpointSchema from '../../components/APIEndpoint/APIEndpointSchema/APIEndpointSchema'; +import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../../components/DataAssets/CommonWidgets/CommonWidgets'; +import Lineage from '../../components/Lineage/Lineage.component'; +import { SourceType } from '../../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../../context/LineageProvider/LineageProvider'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs, EntityType } from '../../enums/entity.enum'; +import { PageType } from '../../generated/system/ui/page'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import i18n from '../i18next/LocalUtil'; +import { APIEndpointDetailPageTabProps } from './APIEndpointClassBase'; + +export const getApiEndpointDetailsPageTabs = ({ + activeTab, + feedCount, + apiEndpoint, + fetchAPIEndpointDetails, + getEntityFeedCount, + handleFeedCount, + editCustomAttributePermission, + viewAllPermission, + editLineagePermission, + labelMap, +}: APIEndpointDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.SCHEMA, + children: , + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: ( +
+ + entityType={EntityType.API_ENDPOINT} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; +}; + +export const getApiEndpointWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.API_SCHEMA)) { + return ; + } + + return ( + + ); +}; From c1349fd3ecf393af86bf489b3997acb60501cd22 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 00:26:31 +0530 Subject: [PATCH 39/63] fix apiCollection and apiEndpoint customization --- .../utils/CustomizePage/CustomizePageUtils.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 3c1b90772e97..680232a0f130 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -15,6 +15,8 @@ import { CommonWidgetType } from '../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../enums/entity.enum'; import { PageType, Tab } from '../../generated/system/ui/page'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import apiCollectionClassBase from '../APICollection/APICollectionClassBase'; +import apiEndpointClassBase from '../APIEndpoints/APIEndpointClassBase'; import containerDetailsClassBase from '../ContainerDetailsClassBase'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; @@ -203,6 +205,14 @@ export const getDomainDefaultTabs = () => { return domainClassBase.getDomainDetailPageTabsIds(); }; +export const getAPICollectionDefaultTabs = () => { + return apiCollectionClassBase.getAPICollectionDetailPageTabsIds(); +}; + +export const getAPIEndpointDefaultTabs = () => { + return apiEndpointClassBase.getEndpointDetailPageTabsIds(); +}; + export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: @@ -231,6 +241,10 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { return getDashboardDefaultTabs(); case PageType.Domain: return getDomainDefaultTabs(); + case PageType.APICollection: + return getAPICollectionDefaultTabs(); + case PageType.APIEndpoint: + return getAPIEndpointDefaultTabs(); default: return [ { @@ -272,6 +286,10 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return domainClassBase.getDefaultLayout(tab); case PageType.Dashboard: return dashboardDetailsClassBase.getDefaultLayout(tab); + case PageType.APICollection: + return apiCollectionClassBase.getDefaultLayout(tab); + case PageType.APIEndpoint: + return apiEndpointClassBase.getDefaultLayout(tab); default: return []; } @@ -327,6 +345,10 @@ export const getCustomizableWidgetByPage = ( return searchIndexClassBase.getCommonWidgetList(); case PageType.Domain: return domainClassBase.getCommonWidgetList(); + case PageType.APICollection: + return apiCollectionClassBase.getCommonWidgetList(); + case PageType.APIEndpoint: + return apiEndpointClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; @@ -357,6 +379,10 @@ export const getDummyDataByPage = (pageType: PageType) => { return dashboardDetailsClassBase.getDummyData(); case PageType.Domain: return domainClassBase.getDummyData(); + case PageType.APICollection: + return apiCollectionClassBase.getDummyData(); + case PageType.APIEndpoint: + return apiEndpointClassBase.getDummyData(); case PageType.LandingPage: default: @@ -397,6 +423,10 @@ export const getWidgetsFromKey = ( return dashboardDetailsClassBase.getWidgetsFromKey(widgetConfig); case PageType.Domain: return domainClassBase.getWidgetsFromKey(widgetConfig); + case PageType.APICollection: + return apiCollectionClassBase.getWidgetsFromKey(widgetConfig); + case PageType.APIEndpoint: + return apiEndpointClassBase.getWidgetsFromKey(widgetConfig); default: return null; } From 41c0da324bcecbfaaf9afa1dc2b4e5d5d76e238b Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:16:51 +0530 Subject: [PATCH 40/63] fix container test and fix sonar --- .../ContainerChildren.test.tsx | 11 ++-- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 7 +- .../Customization/GenericTab/GenericTab.tsx | 20 +++--- .../ReviewerLabelV2/ReviewerLabelV2.tsx | 2 +- .../DomainTypeWidget/DomainTypeWidget.tsx | 4 +- .../PipelineTaskTab/PipelineTaskTab.tsx | 6 +- .../EntityDescription/DescriptionV1.tsx | 8 +-- .../CustomizableDomainPage.tsx | 2 +- .../DashboardDetailsPage.component.tsx | 28 ++------ .../SearchIndexFieldsTab.tsx | 66 +++++++++---------- .../APIEndpoints/APIEndpointClassBase.ts | 2 +- .../ui/src/utils/ContainerDetailsClassBase.ts | 7 +- 12 files changed, 70 insertions(+), 93 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx index 7d1f4e4de1d9..d6740af7c8c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx @@ -28,9 +28,6 @@ jest.mock('../../common/NextPrevious/NextPrevious', () => { )); }); -const mockHandleChildrenPageChange = jest.fn(); -const mockHandlePageSizeChange = jest.fn(); - const mockChildrenList = [ { id: '1', @@ -55,7 +52,13 @@ jest.mock('../../Customization/GenericTab/GenericTab', () => ({ })); jest.mock('../../../rest/storageAPI'); -jest.mock('../../../hooks/paging/usePaging'); +jest.mock('../../../hooks/paging/usePaging', () => ({ + usePaging: jest.fn().mockImplementation(() => ({ + paging: { + total: 2, + }, + })), +})); describe('ContainerChildren', () => { it('Should call fetch container function on load', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx index 196ab0373bd6..527efcccf396 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -11,7 +11,7 @@ * limitations under the License. */ import Icon from '@ant-design/icons'; -import { Input, Modal, Tooltip } from 'antd'; +import { Button, Input, Modal, Tooltip } from 'antd'; import { isEmpty, isNil, toString, uniqueId } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import RGL, { Layout, WidthProvider } from 'react-grid-layout'; @@ -273,7 +273,8 @@ export const CustomizeTabWidget = () => { items={items.map((item) => ({ key: item.id, label: ( - { event.stopPropagation(); item.editable && onChange(item.id); @@ -292,7 +293,7 @@ export const CustomizeTabWidget = () => { }} /> - + ), closable: true, }))} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index e56fed197e15..bd0f0a2b010d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -69,16 +69,14 @@ export const GenericTab = ({ type }: GenericTabProps) => { useGridLayoutDirection(); return ( - <> - - {widgets} - - + + {widgets} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx index 78214d9fd6f3..84f2411f7ce7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/ReviewerLabelV2/ReviewerLabelV2.tsx @@ -98,7 +98,7 @@ export const ReviewerLabelV2 = <
{getOwnerVersionLabel( - data as T, + data, isVersionView ?? false, TabSpecificField.REVIEWERS, hasEditReviewerAccess diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx index b6eeabe98133..2d098b67ffeb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainTypeWidget/DomainTypeWidget.tsx @@ -58,7 +58,7 @@ export const DomainTypeWidget = () => { /> - {editAllPermission && (domain as Domain).domainType && ( + {editAllPermission && domain.domainType && ( { {editDomainType && ( setEditDomainType(false)} onSubmit={handleDomainTypeUpdate} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx index 15b7d94130f0..80e957d9b4aa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineTaskTab/PipelineTaskTab.tsx @@ -10,12 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { groupBy, isUndefined, uniqBy } from 'lodash'; - -import Icon from '@ant-design/icons/lib/components/Icon'; +import Icon from '@ant-design/icons'; import { Card, Col, Radio, Row, Table, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; -import { isEmpty } from 'lodash'; +import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash'; import { EntityTags, TagFilterOptions } from 'Models'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index 51afd745bedf..abd598c0ec24 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -65,15 +65,11 @@ const DescriptionV1 = ({ const { onThreadLinkSelect } = useGenericContext(); const handleRequestDescription = useCallback(() => { - history.push( - getRequestDescriptionPath(entityType as string, entityFqn as string) - ); + history.push(getRequestDescriptionPath(entityType, entityFqn)); }, [entityType, entityFqn]); const handleUpdateDescription = useCallback(() => { - history.push( - getUpdateDescriptionPath(entityType as string, entityFqn as string) - ); + history.push(getUpdateDescriptionPath(entityType, entityFqn)); }, [entityType, entityFqn]); // Callback to handle the edit button from description diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx index b4760e2e6986..f16706d7be52 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDomainPage/CustomizableDomainPage.tsx @@ -94,7 +94,7 @@ const CustomizableDomainPage = ({
{ } }; - const handleDashboardUpdate = async (updatedDashboard: Dashboard) => { - try { - const response = await saveUpdatedDashboardData(updatedDashboard); - setDashboardDetails((previous) => { - return { - ...previous, - ...response, - }; - }); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - - /* const onDashboardUpdate = async ( + const onDashboardUpdate = async ( updatedDashboard: Dashboard, - key: keyof Dashboard + key?: keyof Dashboard ) => { try { const response = await saveUpdatedDashboardData(updatedDashboard); @@ -172,16 +157,13 @@ const DashboardDetailsPage = () => { return { ...previous, version: response.version, - [key]: - key === 'tags' - ? sortTagsCaseInsensitive(response[key] ?? []) - : response[key], + ...(key ? { [key]: response[key] } : response), }; }); } catch (error) { showErrorToast(error as AxiosError); } - }; */ + }; const followDashboard = async () => { try { @@ -299,7 +281,7 @@ const DashboardDetailsPage = () => { unFollowDashboardHandler={unFollowDashboard} updateDashboardDetailsState={updateDashboardDetailsState} versionHandler={versionHandler} - onDashboardUpdate={handleDashboardUpdate} + onDashboardUpdate={onDashboardUpdate} onUpdateVote={updateVote} /> ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx index f3b74ca58dfa..e71b1886d847 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx @@ -123,41 +123,39 @@ function SearchIndexFieldsTab() { }, [searchText, sortByOrdinalPosition]); return ( - <> - -
- - + + + + - - - - - - - - + + + + + + + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts index 50a3ed0a4e93..8b865ed3230c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts @@ -46,7 +46,7 @@ export interface APIEndpointDetailPageTabProps { }; apiEndpoint: APIEndpoint; fetchAPIEndpointDetails: () => void; - getEntityFeedCount: () => void; + getEntityFeedCount: () => Promise; handleFeedCount: (data: FeedCounts) => void; editCustomAttributePermission: boolean; viewAllPermission: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index afced67b9e66..36dcab59e606 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -28,11 +28,12 @@ import { StorageServiceType, } from '../generated/entity/data/container'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { getContainerDetailPageTabs } from './ContainerDetailUtils'; - import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; -import { getContainerWidgetsFromKey } from './ContainerDetailUtils'; +import { + getContainerDetailPageTabs, + getContainerWidgetsFromKey, +} from './ContainerDetailUtils'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; import i18n from './i18next/LocalUtil'; From 4b34f882c7cb262fd101c204e7e486370e5e7b27 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:38:20 +0530 Subject: [PATCH 41/63] fix tests --- .../ui/playwright/support/entity/EntityDataClass.ts | 9 ++++++--- .../ui/src/pages/APICollectionPage/APIEndpointsTab.tsx | 2 +- .../ui/src/utils/APICollection/APICollectionUtils.tsx | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts index 83765b9c4a2b..0bb86ec9db2f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts @@ -97,8 +97,7 @@ export class EntityDataClass { this.domain2.create(apiContext), this.glossary1.create(apiContext), this.glossary2.create(apiContext), - this.glossaryTerm1.create(apiContext), - this.glossaryTerm2.create(apiContext), + this.user1.create(apiContext), this.user2.create(apiContext), this.user3.create(apiContext), @@ -184,6 +183,10 @@ export class EntityDataClass { } await Promise.allSettled(promises); + await Promise.allSettled([ + this.glossaryTerm1.create(apiContext), + this.glossaryTerm2.create(apiContext), + ]); } static async postRequisitesForTests( @@ -280,6 +283,6 @@ export class EntityDataClass { promises.push(this.storageService.delete(apiContext)); } - await Promise.allSettled(promises); + return await Promise.allSettled(promises); } } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx index 19c16c5f2498..1123e81e6b2e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APIEndpointsTab.tsx @@ -157,7 +157,7 @@ function APIEndpointsTab({ useEffect(() => { getAPICollectionEndpoints(); - }, []); + }, [apiCollection]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx index cfec89511e70..8ef960dc2f53 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionUtils.tsx @@ -51,7 +51,7 @@ export const getApiCollectionDetailsPageTabs = ({ /> ), key: EntityTabs.API_ENDPOINT, - children: , + children: , }, { label: ( From 25f68cdefffc5a7d52d3c2348f1c1d51ff3d8261 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:56:18 +0530 Subject: [PATCH 42/63] fix delete playwright --- .../src/main/resources/ui/playwright/constant/delete.ts | 4 ++-- .../resources/ui/playwright/e2e/Pages/Glossary.spec.ts | 8 ++++---- .../src/main/resources/ui/playwright/utils/user.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/delete.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/delete.ts index d1efb6b599ae..bba7f016af49 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/delete.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/delete.ts @@ -29,12 +29,12 @@ export const LIST_OF_FIELDS_TO_EDIT_NOT_TO_BE_PRESENT = [ }, { containerSelector: - '[data-testid="entity-right-panel"] [data-testid="tags-container"]', + '[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"]', elementSelector: '[data-testid="add-tag"]', }, { containerSelector: - '[data-testid="entity-right-panel"] [data-testid="glossary-container"]', + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"]', elementSelector: '[data-testid="add-tag"]', }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 02dd2138321a..9c1f11742aa0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -416,7 +416,7 @@ test.describe('Glossary tests', () => { // Dashboard Entity Right Panel await page.click( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="add-tag"]' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]' ); // Select 1st term @@ -472,7 +472,7 @@ test.describe('Glossary tests', () => { // Add non mutually exclusive tags await page.click( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="add-tag"]' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]' ); // Select 1st term @@ -523,7 +523,7 @@ test.describe('Glossary tests', () => { // Check if the terms are present const glossaryContainer = page.locator( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"]' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"]' ); const glossaryContainerText = await glossaryContainer.innerText(); @@ -533,7 +533,7 @@ test.describe('Glossary tests', () => { // Check if the icons are present const icons = page.locator( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="glossary-icon"]' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="glossary-icon"]' ); expect(await icons.count()).toBe(2); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 972ce6ec6dcf..c8525be9f839 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -567,14 +567,14 @@ export const checkDataConsumerPermissions = async (page: Page) => { // Check right panel add tags button await expect( page.locator( - '[data-testid="entity-right-panel"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button' + '[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button' ) ).toBeVisible(); // Check right panel add glossary term button await expect( page.locator( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button' ) ).toBeVisible(); @@ -663,14 +663,14 @@ export const checkStewardPermissions = async (page: Page) => { // Check right panel add tags button await expect( page.locator( - '[data-testid="entity-right-panel"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button' + '[data-testid="KnowledgePanel.Tags"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button' ) ).toBeVisible(); // Check right panel add glossary term button await expect( page.locator( - '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button' + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button' ) ).toBeVisible(); From cc992a2a67f92e767b57c8f44dc2315c226c3a38 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:32:16 +0530 Subject: [PATCH 43/63] support metric entity --- .../main/resources/ui/playwright.config.ts | 2 +- .../ContainerChildren/ContainerChildren.tsx | 27 +- .../EntityRightPanel/EntityRightPanel.tsx | 44 +-- .../MetricSummary/MetricSummary.tsx | 40 ++- .../Metric/MetricDetails/MetricDetails.tsx | 296 ++++-------------- .../MetricExpression/MetricExpression.tsx | 15 +- .../Metric/MetricVersion/MetricVersion.tsx | 2 +- .../ui/src/enums/CustomizeDetailPage.enum.ts | 1 + .../MetricDetailsClassBase.ts | 199 ++++++++++++ .../utils/MetricEntityUtils/MetricUtils.ts | 65 ---- .../utils/MetricEntityUtils/MetricUtils.tsx | 191 +++++++++++ 11 files changed, 508 insertions(+), 374 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/playwright.config.ts b/openmetadata-ui/src/main/resources/ui/playwright.config.ts index c47a3699213b..35a86f6f8e40 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright.config.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 4 : undefined, - maxFailures: 30, + // maxFailures: 30, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx index f4576dea3809..fc72bf25981c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.tsx @@ -16,15 +16,12 @@ import { AxiosError } from 'axios'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { getEntityDetailsPath, PAGE_SIZE } from '../../../constants/constants'; -import { EntityType, TabSpecificField } from '../../../enums/entity.enum'; +import { getEntityDetailsPath } from '../../../constants/constants'; +import { EntityType } from '../../../enums/entity.enum'; import { EntityReference } from '../../../generated/type/entityReference'; import { usePaging } from '../../../hooks/paging/usePaging'; import { useFqn } from '../../../hooks/useFqn'; -import { - getContainerByName, - getContainerChildrenByName, -} from '../../../rest/storageAPI'; +import { getContainerChildrenByName } from '../../../rest/storageAPI'; import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -43,7 +40,7 @@ const ContainerChildren: FC = () => { handlePageChange, handlePageSizeChange, handlePagingChange, - } = usePaging(PAGE_SIZE); + } = usePaging(); const { fqn: decodedContainerName } = useFqn(); const [isChildrenLoading, setIsChildrenLoading] = useState(false); @@ -95,20 +92,6 @@ const ContainerChildren: FC = () => { [] ); - const fetchChildren = async () => { - setIsChildrenLoading(true); - try { - const { children } = await getContainerByName(decodedContainerName, { - fields: TabSpecificField.CHILDREN, - }); - setContainerChildrenData(children ?? []); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsChildrenLoading(false); - } - }; - const fetchContainerChildren = async (pagingOffset?: number) => { setIsChildrenLoading(true); try { @@ -134,7 +117,7 @@ const ContainerChildren: FC = () => { }; useEffect(() => { - fetchChildren(); + fetchContainerChildren(); }, [pageSize]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx index aa81da425f5b..d7d79bdd87e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityRightPanel/EntityRightPanel.tsx @@ -82,27 +82,31 @@ const EntityRightPanel = ({ /> )} - +
+ +
- +
+ +
{KnowledgeArticles && ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx index ee307c1071c1..00efa6a1c136 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx @@ -12,9 +12,10 @@ */ import { Col, Divider, Row } from 'antd'; -import { get, isEmpty } from 'lodash'; +import { get, isEmpty, noop } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { TabSpecificField } from '../../../../enums/entity.enum'; +import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; +import { EntityType, TabSpecificField } from '../../../../enums/entity.enum'; import { ExplorePageTabs } from '../../../../enums/Explore.enum'; import { Metric } from '../../../../generated/entity/data/metric'; import { getMetricByFqn } from '../../../../rest/metricsAPI'; @@ -26,6 +27,7 @@ import { import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import SummaryPanelSkeleton from '../../../common/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component'; import SummaryTagsDescription from '../../../common/SummaryTagsDescription/SummaryTagsDescription.component'; +import { GenericProvider } from '../../../Customization/GenericProvider/GenericProvider'; import MetricExpression from '../../../Metric/MetricExpression/MetricExpression'; import RelatedMetrics from '../../../Metric/RelatedMetrics/RelatedMetrics'; import { SearchedDataProps } from '../../../SearchedData/SearchedData.interface'; @@ -103,19 +105,27 @@ const MetricSummary = ({ /> - -
- - - - - - - + + data={entityDetails} + permissions={{} as OperationPermission} + type={EntityType.METRIC} + onUpdate={async () => { + noop(); + }}> + + + + + + + + + + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index e51e972ccd72..f8933c672f7d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -13,47 +13,35 @@ import { Col, Row, Tabs } from 'antd'; import { AxiosError } from 'axios'; -import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath, ROUTES } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Metric } from '../../../generated/entity/data/metric'; -import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { TagLabel } from '../../../generated/type/schema'; +import { Page, PageType } from '../../../generated/system/ui/page'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restoreMetric } from '../../../rest/metricsAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { - getEntityName, - getEntityReferenceFromEntity, -} from '../../../utils/EntityUtils'; -import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; +import metricDetailsClassBase from '../../../utils/MetricEntityUtils/MetricDetailsClassBase'; +import { updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../SearchedData/SearchedData.interface'; -import MetricExpression from '../MetricExpression/MetricExpression'; -import RelatedMetrics from '../RelatedMetrics/RelatedMetrics'; import { MetricDetailsProps } from './MetricDetails.interface'; const MetricDetails: React.FC = ({ @@ -69,7 +57,7 @@ const MetricDetails: React.FC = ({ onUpdateVote, }: MetricDetailsProps) => { const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.OVERVIEW } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedMetricFqn } = useFqn(); @@ -81,20 +69,9 @@ const MetricDetails: React.FC = ({ const { owners, deleted, - description, followers = [], - entityName, - metricTags, - tier, - } = useMemo( - () => ({ - ...metricDetails, - tier: getTierTags(metricDetails.tags ?? []), - metricTags: getTagsWithoutTier(metricDetails.tags ?? []), - entityName: getEntityName(metricDetails), - }), - [metricDetails] - ); + } = useMemo(() => metricDetails, [metricDetails]); + const [customizedPage, setCustomizedPage] = useState(null); const { isFollowing } = useMemo( () => ({ @@ -114,12 +91,6 @@ const MetricDetails: React.FC = ({ }; await onMetricUpdate(updatedData, 'displayName'); }; - const onExtensionUpdate = async (updatedData: Metric) => { - await onMetricUpdate( - { ...metricDetails, extension: updatedData.extension }, - 'extension' - ); - }; const handleRestoreMetric = async () => { try { @@ -149,19 +120,6 @@ const MetricDetails: React.FC = ({ } }; - const onDescriptionUpdate = async (updatedHTML: string) => { - if (description !== updatedHTML) { - const updatedMetricDetails = { - ...metricDetails, - description: updatedHTML, - }; - try { - await onMetricUpdate(updatedMetricDetails, 'description'); - } catch (error) { - showErrorToast(error as AxiosError); - } - } - }; const onOwnerUpdate = useCallback( async (newOwners?: Metric['owners']) => { const updatedMetricDetails = { @@ -183,29 +141,6 @@ const MetricDetails: React.FC = ({ return onMetricUpdate(updatedMetricDetails, 'tags'); }; - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - - if (updatedTags && metricDetails) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedMetric = { ...metricDetails, tags: updatedTags }; - await onMetricUpdate(updatedMetric, 'tags'); - } - }; - - const onDataProductsUpdate = async (updatedData: DataProduct[]) => { - const dataProductsEntity = updatedData?.map((item) => { - return getEntityReferenceFromEntity(item, EntityType.DATA_PRODUCT); - }); - - const updatedMetricDetails = { - ...metricDetails, - dataProducts: dataProductsEntity, - }; - - await onMetricUpdate(updatedMetricDetails, 'dataProducts'); - }; - const handleFeedCount = useCallback((data: FeedCounts) => { setFeedCount(data); }, []); @@ -220,9 +155,6 @@ const MetricDetails: React.FC = ({ ); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, editAllPermission, editLineagePermission, @@ -230,14 +162,6 @@ const MetricDetails: React.FC = ({ viewAllPermission, } = useMemo( () => ({ - editTagsPermission: - (metricPermissions.EditTags || metricPermissions.EditAll) && !deleted, - editGlossaryTermsPermission: - (metricPermissions.EditGlossaryTerms || metricPermissions.EditAll) && - !deleted, - editDescriptionPermission: - (metricPermissions.EditDescription || metricPermissions.EditAll) && - !deleted, editCustomAttributePermission: (metricPermissions.EditAll || metricPermissions.EditCustomFields) && !deleted, @@ -256,165 +180,57 @@ const MetricDetails: React.FC = ({ getEntityFeedCount(); }, [metricPermissions, decodedMetricFqn]); - const tabs = useMemo( - () => [ - { - label: ( - - ), - key: EntityTabs.OVERVIEW, - children: ( - - - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - afterSlot={ -
- -
- } - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityType={EntityType.METRIC} - selectedTags={metricTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.EXPRESSION, - children: ( -
- -
- ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: metricDetails && ( -
- - entityType={EntityType.METRIC} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); + const tabs = metricDetailsClassBase.getMetricDetailPageTabs({ activeTab, - feedCount.totalCount, - metricTags, - entityName, + feedCount, metricDetails, - decodedMetricFqn, fetchMetricDetails, - deleted, handleFeedCount, - onExtensionUpdate, - handleTagSelection, - onDescriptionUpdate, - onDataProductsUpdate, - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, editLineagePermission, - editAllPermission, - viewSampleDataPermission, + editCustomAttributePermission, viewAllPermission, - ] - ); + getEntityFeedCount, + labelMap: tabLabelMap, + }); + + return getDetailsTabWithNewLabel( + tabs, + customizedPage?.tabs, + EntityTabs.OVERVIEW + ); + }, [ + activeTab, + feedCount.totalCount, + metricDetails, + fetchMetricDetails, + deleted, + handleFeedCount, + editCustomAttributePermission, + editLineagePermission, + editAllPermission, + viewSampleDataPermission, + viewAllPermission, + ]); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.Metric) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); return ( Promise; -} - -const MetricExpression: FC = ({ - metricDetails, - onMetricUpdate, -}: MetricExpressionProps) => { +const MetricExpression: FC = () => { const [form] = Form.useForm(); const { t } = useTranslation(); + const { data: metricDetails, onUpdate: onMetricUpdate } = + useGenericContext(); const [isUpdating, setIsUpdating] = React.useState(false); const [isEditing, setIsEditing] = React.useState(false); @@ -99,7 +94,7 @@ const MetricExpression: FC = ({ ? t('label.edit-entity', { entity: t('label.expression') }) : metricDetails?.metricExpression?.language ?? t('label.expression')} - {!isEditing && onMetricUpdate && !metricDetails.deleted && ( + {!isEditing && !metricDetails.deleted && ( = ({ />
- + diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index 58235b7717d1..ec2c0432ee90 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -45,6 +45,7 @@ export enum DetailPageWidgetKeys { TABLES = 'KnowledgePanel.Tables', API_ENDPOINTS = 'KnowledgePanel.APIEndpoints', API_SCHEMA = 'KnowledgePanel.APISchema', + RELATED_METRICS = 'KnowledgePanel.RelatedMetrics', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts new file mode 100644 index 000000000000..1f7b857a2d45 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts @@ -0,0 +1,199 @@ +/* + * Copyright 2024 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 { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { + Language, + Metric, + MetricGranularity, + MetricType, + UnitOfMeasurement, +} from '../../generated/entity/data/metric'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { FeedCounts } from '../../interface/feed.interface'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; +import { + getMetricDetailsPageTabs, + getMetricWidgetsFromKey, +} from './MetricUtils'; + +export interface MetricDetailPageTabProps { + feedCount: { + totalCount: number; + }; + activeTab: EntityTabs; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + getEntityFeedCount: () => void; + fetchMetricDetails: () => void; + metricDetails: Metric; + handleFeedCount: (data: FeedCounts) => void; + labelMap: Record; +} + +class MetricDetailsClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getMetricDetailPageTabs( + metricDetailsPageProps: MetricDetailPageTabProps + ): TabProps[] { + return getMetricDetailsPageTabs(metricDetailsPageProps); + } + + public getMetricDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.OVERVIEW, + EntityTabs.EXPRESSION, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.OVERVIEW, + })); + } + + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.OVERVIEW) { + return []; + } + + return [ + { + h: 2, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 8, + i: DetailPageWidgetKeys.API_ENDPOINTS, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 6, + static: false, + }, + ]; + } + public getDummyData(): Metric { + return { + id: '2be869f2-a8c6-4781-89fa-df45d671e449', + name: 'LTV', + fullyQualifiedName: 'LTV', + displayName: 'Lifetime Value', + metricExpression: { + language: Language.Python, + code: 'def ltv(customer: Customer, orders: List[Order]):\n\tlifetime = customer.lifetime\n ltv: float = sum([o.amount for o in orders if o.customer_id == customer.id])\n \n return ltv', + }, + metricType: MetricType.Other, + unitOfMeasurement: UnitOfMeasurement.Dollars, + granularity: MetricGranularity.Quarter, + relatedMetrics: [], + version: 0.1, + updatedAt: 1727366423816, + updatedBy: 'teddy', + href: 'http://sandbox-beta.open-metadata.org/api/v1/metrics/2be869f2-a8c6-4781-89fa-df45d671e449', + owners: [], + followers: [], + tags: [], + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.RELATED_METRICS, + name: i18n.t('label.related-metrics'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getMetricWidgetsFromKey(widgetConfig); + } +} + +const metricDetailsClassBase = new MetricDetailsClassBase(); + +export default metricDetailsClassBase; +export { MetricDetailsClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.ts deleted file mode 100644 index 0bdb4e0de192..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2024 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 { CSMode } from '../../enums/codemirror.enum'; -import { - Language, - Metric, - MetricGranularity, -} from '../../generated/entity/data/metric'; - -const granularityOrder = [ - MetricGranularity.Second, - MetricGranularity.Minute, - MetricGranularity.Hour, - MetricGranularity.Day, - MetricGranularity.Week, - MetricGranularity.Month, - MetricGranularity.Quarter, - MetricGranularity.Year, -]; - -export const getSortedOptions = ( - options: { - label: string; - value: string; - key: string; - }[], - value: string | undefined, - valueKey: keyof Metric -) => { - return options.sort((a, b) => { - if (a.value === value) { - return -1; - } - if (b.value === value) { - return 1; - } - - return valueKey === 'granularity' - ? granularityOrder.indexOf(a.value as MetricGranularity) - - granularityOrder.indexOf(b.value as MetricGranularity) - : 0; - }); -}; - -export const getMetricExpressionLanguageName = (language?: Language) => { - if (!language) { - return CSMode.SQL; - } - - if (language === Language.Java) { - return CSMode.CLIKE; - } - - return language.toLowerCase() as CSMode; -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx new file mode 100644 index 000000000000..27d256ab602a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx @@ -0,0 +1,191 @@ +/* + * Copyright 2024 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 React from 'react'; +import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../../components/DataAssets/CommonWidgets/CommonWidgets'; +import Lineage from '../../components/Lineage/Lineage.component'; +import MetricExpression from '../../components/Metric/MetricExpression/MetricExpression'; +import { SourceType } from '../../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../../context/LineageProvider/LineageProvider'; +import { CSMode } from '../../enums/codemirror.enum'; +import { EntityTabs, EntityType } from '../../enums/entity.enum'; +import { + Language, + Metric, + MetricGranularity, +} from '../../generated/entity/data/metric'; +import { PageType } from '../../generated/system/ui/page'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import i18n from '../i18next/LocalUtil'; +import { MetricDetailPageTabProps } from './MetricDetailsClassBase'; + +const granularityOrder = [ + MetricGranularity.Second, + MetricGranularity.Minute, + MetricGranularity.Hour, + MetricGranularity.Day, + MetricGranularity.Week, + MetricGranularity.Month, + MetricGranularity.Quarter, + MetricGranularity.Year, +]; + +export const getSortedOptions = ( + options: { + label: string; + value: string; + key: string; + }[], + value: string | undefined, + valueKey: keyof Metric +) => { + return options.sort((a, b) => { + if (a.value === value) { + return -1; + } + if (b.value === value) { + return 1; + } + + return valueKey === 'granularity' + ? granularityOrder.indexOf(a.value as MetricGranularity) - + granularityOrder.indexOf(b.value as MetricGranularity) + : 0; + }); +}; + +export const getMetricExpressionLanguageName = (language?: Language) => { + if (!language) { + return CSMode.SQL; + } + + if (language === Language.Java) { + return CSMode.CLIKE; + } + + return language.toLowerCase() as CSMode; +}; + +export const getMetricDetailsPageTabs = ({ + feedCount, + activeTab, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + getEntityFeedCount, + fetchMetricDetails, + metricDetails, + handleFeedCount, + labelMap, +}: MetricDetailPageTabProps) => { + return [ + { + label: ( + + ), + key: EntityTabs.OVERVIEW, + children: , + }, + { + label: ( + + ), + key: EntityTabs.EXPRESSION, + children: ( +
+ +
+ ), + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: metricDetails && ( +
+ + entityType={EntityType.METRIC} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; +}; + +export const getMetricWidgetsFromKey = (widgetConfig: WidgetConfig) => { + return ( + + ); +}; From f228a79245173fc59419aaa805cf09966e0d9fdd Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:35:18 +0530 Subject: [PATCH 44/63] add metric as page type to customization --- .../src/main/resources/json/schema/system/ui/page.json | 3 ++- .../src/main/resources/ui/src/generated/system/ui/page.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json index 9b708d74556c..0937c753558b 100644 --- a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json +++ b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json @@ -26,7 +26,8 @@ "GlossaryTerm", "Domain", "APICollection", - "APIEndpoint" + "APIEndpoint", + "Metric" ] } }, diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts index 3b3612fad4b2..9785aa844435 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts @@ -124,6 +124,7 @@ export enum PageType { Glossary = "Glossary", GlossaryTerm = "GlossaryTerm", LandingPage = "LandingPage", + Metric = "Metric", Pipeline = "Pipeline", SearchIndex = "SearchIndex", StoredProcedure = "StoredProcedure", From c89d3a059e11ab3662eab9be418e5ffdb630ff2d Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 11:48:56 +0530 Subject: [PATCH 45/63] fix container unit tests --- .../ContainerChildren.test.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx index d6740af7c8c5..10a3ebaa9392 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerChildren/ContainerChildren.test.tsx @@ -13,9 +13,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { TabSpecificField } from '../../../enums/entity.enum'; import { usePaging } from '../../../hooks/paging/usePaging'; -import { getContainerByName } from '../../../rest/storageAPI'; +import { getContainerChildrenByName } from '../../../rest/storageAPI'; import ContainerChildren from './ContainerChildren'; jest.mock('../../common/NextPrevious/NextPrevious', () => { @@ -54,6 +53,7 @@ jest.mock('../../Customization/GenericTab/GenericTab', () => ({ jest.mock('../../../rest/storageAPI'); jest.mock('../../../hooks/paging/usePaging', () => ({ usePaging: jest.fn().mockImplementation(() => ({ + pageSize: 15, paging: { total: 2, }, @@ -64,12 +64,10 @@ describe('ContainerChildren', () => { it('Should call fetch container function on load', () => { render(, { wrapper: MemoryRouter }); - expect(getContainerByName).toHaveBeenCalledWith( - '', - expect.objectContaining({ - fields: TabSpecificField.CHILDREN, - }) - ); + expect(getContainerChildrenByName).toHaveBeenCalledWith('', { + limit: 15, + offset: 0, + }); }); it('Should render table with correct columns', () => { @@ -87,8 +85,11 @@ describe('ContainerChildren', () => { }); it('Should render container names as links', async () => { - (getContainerByName as jest.Mock).mockResolvedValue({ - children: mockChildrenList, + (getContainerChildrenByName as jest.Mock).mockResolvedValue({ + data: mockChildrenList, + paging: { + total: 2, + }, }); render(, { wrapper: MemoryRouter }); From d3ff47ec80778fb5dca73f68f2adbe2747ee984a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:13:43 +0530 Subject: [PATCH 46/63] fix metric tests --- .../MetricSummary/MetricSummary.tsx | 6 +- .../Metric/RelatedMetrics/RelatedMetrics.tsx | 16 +-- .../utils/CustomizePage/CustomizePageUtils.ts | 98 +++++-------------- .../MetricDetailsClassBase.ts | 20 ++-- .../utils/MetricEntityUtils/MetricUtils.tsx | 6 ++ 5 files changed, 49 insertions(+), 97 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx index 00efa6a1c136..466f4a8bd2ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/MetricSummary/MetricSummary.tsx @@ -118,11 +118,7 @@ const MetricSummary = ({
- + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/RelatedMetrics/RelatedMetrics.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/RelatedMetrics/RelatedMetrics.tsx index 9dfe5290e1b2..a967b1ba5a17 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/RelatedMetrics/RelatedMetrics.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/RelatedMetrics/RelatedMetrics.tsx @@ -29,27 +29,27 @@ import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityIcon } from '../../../utils/TableUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetOption } from '../../DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface'; import TagsV1 from '../../Tag/TagsV1/TagsV1.component'; import './related-metrics.less'; import { RelatedMetricsForm } from './RelatedMetricsForm'; interface RelatedMetricsProps { - hasEditPermission: boolean; - metricDetails: Metric; isInSummaryPanel?: boolean; - onMetricUpdate?: (updatedData: Metric, key: keyof Metric) => Promise; } const RelatedMetrics: FC = ({ - metricDetails, - hasEditPermission, - onMetricUpdate, isInSummaryPanel = false, }) => { const { t } = useTranslation(); const [isEdit, setIsEdit] = useState(false); const [isShowMore, setIsShowMore] = useState(false); + const { + data: metricDetails, + onUpdate: onMetricUpdate, + permissions, + } = useGenericContext(); const { defaultValue, @@ -167,7 +167,7 @@ const RelatedMetrics: FC = ({ {!isEdit && !isEmpty(relatedMetrics) && - hasEditPermission && + permissions.EditAll && !metricDetails.deleted && ( = ({ {isEmpty(relatedMetrics) && !isEdit && - hasEditPermission && + permissions.EditAll && !metricDetails.deleted && (
setIsEdit(true)}> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 680232a0f130..345c3889d110 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -27,6 +27,7 @@ import databaseSchemaClassBase from '../DatabaseSchemaClassBase'; import domainClassBase from '../Domain/DomainClassBase'; import { getEntityName } from '../EntityUtils'; import i18n from '../i18next/LocalUtil'; +import metricDetailsClassBase from '../MetricEntityUtils/MetricDetailsClassBase'; import pipelineClassBase from '../PipelineClassBase'; import searchIndexClassBase from '../SearchIndexDetailsClassBase'; import storedProcedureClassBase from '../StoredProcedureBase'; @@ -152,67 +153,6 @@ export const getTabLabelFromId = (tab: EntityTabs) => { } }; -export const getTableDefaultTabs = () => { - const tabs = tableClassBase.getTableDetailPageTabsIds(); - - return tabs; -}; - -export const getTopicDefaultTabs = () => { - const tabs = topicClassBase.getTopicDetailPageTabsIds(); - - return tabs; -}; - -export const getStoredProcedureDefaultTabs = () => { - const tabs = storedProcedureClassBase.getStoredProcedureDetailPageTabsIds(); - - return tabs; -}; - -export const getDashboardDataModelDefaultTabs = () => { - const tabs = - dashboardDataModelClassBase.getDashboardDataModelDetailPageTabsIds(); - - return tabs; -}; - -export const getDatabaseDefaultTabs = () => { - return databaseClassBase.getDatabaseDetailPageTabsIds(); -}; - -export const getPipelineDefaultTabs = () => { - return pipelineClassBase.getPipelineDetailPageTabsIds(); -}; - -export const getDatabaseSchemaDefaultTabs = () => { - return databaseSchemaClassBase.getDatabaseSchemaPageTabsIds(); -}; - -export const getSearchIndexDefaultTabs = () => { - return searchIndexClassBase.getSearchIndexDetailPageTabsIds(); -}; - -export const getContainerDefaultTabs = () => { - return containerDetailsClassBase.getContainerDetailPageTabsIds(); -}; - -export const getDashboardDefaultTabs = () => { - return dashboardDetailsClassBase.getDashboardDetailPageTabsIds(); -}; - -export const getDomainDefaultTabs = () => { - return domainClassBase.getDomainDetailPageTabsIds(); -}; - -export const getAPICollectionDefaultTabs = () => { - return apiCollectionClassBase.getAPICollectionDetailPageTabsIds(); -}; - -export const getAPIEndpointDefaultTabs = () => { - return apiEndpointClassBase.getEndpointDetailPageTabsIds(); -}; - export const getDefaultTabs = (pageType?: string): Tab[] => { switch (pageType) { case PageType.GlossaryTerm: @@ -220,31 +160,33 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { case PageType.Glossary: return getGlossaryDefaultTabs(); case PageType.Table: - return getTableDefaultTabs(); + return tableClassBase.getTableDetailPageTabsIds(); case PageType.Topic: - return getTopicDefaultTabs(); + return topicClassBase.getTopicDetailPageTabsIds(); case PageType.StoredProcedure: - return getStoredProcedureDefaultTabs(); + return storedProcedureClassBase.getStoredProcedureDetailPageTabsIds(); case PageType.DashboardDataModel: - return getDashboardDataModelDefaultTabs(); + return dashboardDataModelClassBase.getDashboardDataModelDetailPageTabsIds(); case PageType.Container: - return getContainerDefaultTabs(); + return containerDetailsClassBase.getContainerDetailPageTabsIds(); case PageType.Database: - return getDatabaseDefaultTabs(); + return databaseClassBase.getDatabaseDetailPageTabsIds(); case PageType.SearchIndex: - return getSearchIndexDefaultTabs(); + return searchIndexClassBase.getSearchIndexDetailPageTabsIds(); case PageType.DatabaseSchema: - return getDatabaseSchemaDefaultTabs(); + return databaseSchemaClassBase.getDatabaseSchemaPageTabsIds(); case PageType.Pipeline: - return getPipelineDefaultTabs(); + return pipelineClassBase.getPipelineDetailPageTabsIds(); case PageType.Dashboard: - return getDashboardDefaultTabs(); + return dashboardDetailsClassBase.getDashboardDetailPageTabsIds(); case PageType.Domain: - return getDomainDefaultTabs(); + return domainClassBase.getDomainDetailPageTabsIds(); case PageType.APICollection: - return getAPICollectionDefaultTabs(); + return apiCollectionClassBase.getAPICollectionDetailPageTabsIds(); case PageType.APIEndpoint: - return getAPIEndpointDefaultTabs(); + return apiEndpointClassBase.getEndpointDetailPageTabsIds(); + case PageType.Metric: + return metricDetailsClassBase.getMetricDetailPageTabsIds(); default: return [ { @@ -290,6 +232,8 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return apiCollectionClassBase.getDefaultLayout(tab); case PageType.APIEndpoint: return apiEndpointClassBase.getDefaultLayout(tab); + case PageType.Metric: + return metricDetailsClassBase.getDefaultLayout(tab); default: return []; } @@ -349,6 +293,8 @@ export const getCustomizableWidgetByPage = ( return apiCollectionClassBase.getCommonWidgetList(); case PageType.APIEndpoint: return apiEndpointClassBase.getCommonWidgetList(); + case PageType.Metric: + return metricDetailsClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; @@ -383,6 +329,8 @@ export const getDummyDataByPage = (pageType: PageType) => { return apiCollectionClassBase.getDummyData(); case PageType.APIEndpoint: return apiEndpointClassBase.getDummyData(); + case PageType.Metric: + return metricDetailsClassBase.getDummyData(); case PageType.LandingPage: default: @@ -427,6 +375,8 @@ export const getWidgetsFromKey = ( return apiCollectionClassBase.getWidgetsFromKey(widgetConfig); case PageType.APIEndpoint: return apiEndpointClassBase.getWidgetsFromKey(widgetConfig); + case PageType.Metric: + return metricDetailsClassBase.getWidgetsFromKey(widgetConfig); default: return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts index 1f7b857a2d45..c0c981972525 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts @@ -90,21 +90,13 @@ class MetricDetailsClassBase { return [ { - h: 2, + h: 6, i: DetailPageWidgetKeys.DESCRIPTION, w: 6, x: 0, y: 0, static: false, }, - { - h: 8, - i: DetailPageWidgetKeys.API_ENDPOINTS, - w: 6, - x: 0, - y: 0, - static: false, - }, { h: 1, i: DetailPageWidgetKeys.DATA_PRODUCTS, @@ -129,12 +121,20 @@ class MetricDetailsClassBase { y: 3, static: false, }, + { + h: 2, + i: DetailPageWidgetKeys.RELATED_METRICS, + w: 2, + x: 6, + y: 4, + static: false, + }, { h: 4, i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, w: 2, x: 6, - y: 6, + y: 5, static: false, }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx index 27d256ab602a..c4b30af04ea9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricUtils.tsx @@ -18,9 +18,11 @@ import { GenericTab } from '../../components/Customization/GenericTab/GenericTab import { CommonWidgets } from '../../components/DataAssets/CommonWidgets/CommonWidgets'; import Lineage from '../../components/Lineage/Lineage.component'; import MetricExpression from '../../components/Metric/MetricExpression/MetricExpression'; +import RelatedMetrics from '../../components/Metric/RelatedMetrics/RelatedMetrics'; import { SourceType } from '../../components/SearchedData/SearchedData.interface'; import LineageProvider from '../../context/LineageProvider/LineageProvider'; import { CSMode } from '../../enums/codemirror.enum'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; import { Language, @@ -185,6 +187,10 @@ export const getMetricDetailsPageTabs = ({ }; export const getMetricWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.RELATED_METRICS)) { + return ; + } + return ( ); From 32f65834d79bb10cf28e50ea7011c9fbe34ed85d Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:07:01 +0530 Subject: [PATCH 47/63] fix ml model customization --- .../MlModelDetail/MlModelDetail.component.tsx | 260 ++++---------- .../MlModelDetail/MlModelDetail.interface.ts | 5 +- .../MlModelFeaturesList.test.tsx | 29 +- .../MlModelDetail/MlModelFeaturesList.tsx | 72 ++-- .../ui/src/generated/system/ui/page.ts | 1 + .../MlModelPage/MlModelPage.component.tsx | 92 ++--- .../utils/CustomizePage/CustomizePageUtils.ts | 11 + .../ui/src/utils/MlModel/MlModelClassBase.ts | 322 ++++++++++++++++++ .../ui/src/utils/MlModelDetailsUtils.ts | 17 - .../ui/src/utils/MlModelDetailsUtils.tsx | 155 +++++++++ 10 files changed, 625 insertions(+), 339 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx index 5f21a84706bf..49d905913b47 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.tsx @@ -15,47 +15,42 @@ import { Col, Row, Table, Tabs, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { isEmpty } from 'lodash'; -import { EntityTags } from 'Models'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; -import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../../constants/ResizablePanel.constants'; -import LineageProvider from '../../../context/LineageProvider/LineageProvider'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { MlHyperParameter } from '../../../generated/api/data/createMlModel'; import { Tag } from '../../../generated/entity/classification/tag'; import { Mlmodel, MlStore } from '../../../generated/entity/data/mlmodel'; -import { TagLabel } from '../../../generated/type/schema'; +import { Page, PageType } from '../../../generated/system/ui/page'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; +import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restoreMlmodel } from '../../../rest/mlModelAPI'; import { getEmptyPlaceholder, getFeedCounts } from '../../../utils/CommonUtils'; +import { + getDetailsTabWithNewLabel, + getTabLabelMapFromTabs, +} from '../../../utils/CustomizePage/CustomizePageUtils'; import { getEntityName } from '../../../utils/EntityUtils'; +import mlModelDetailsClassBase from '../../../utils/MlModel/MlModelClassBase'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; -import { createTagObject, updateTierTag } from '../../../utils/TagsUtils'; +import { updateTierTag } from '../../../utils/TagsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; -import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; -import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; -import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; -import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHeader.component'; -import EntityRightPanel from '../../Entity/EntityRightPanel/EntityRightPanel'; -import Lineage from '../../Lineage/Lineage.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; -import { SourceType } from '../../SearchedData/SearchedData.interface'; import { MlModelDetailProp } from './MlModelDetail.interface'; -import MlModelFeaturesList from './MlModelFeaturesList'; const MlModelDetail: FC = ({ updateMlModelDetailsState, @@ -63,19 +58,17 @@ const MlModelDetail: FC = ({ fetchMlModel, followMlModelHandler, unFollowMlModelHandler, - descriptionUpdateHandler, settingsUpdateHandler, - updateMlModelFeatures, - onExtensionUpdate, onUpdateVote, versionHandler, - tagUpdateHandler, handleToggleDelete, + onMlModelUpdate, }) => { const { t } = useTranslation(); - const { currentUser } = useApplicationStore(); + const { currentUser, selectedPersona } = useApplicationStore(); const history = useHistory(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); + const [customizedPage, setCustomizedPage] = useState(null); const { fqn: decodedMlModelFqn } = useFqn(); @@ -116,7 +109,7 @@ const MlModelDetail: FC = ({ } }, [mlModelDetail.id]); - const { mlModelTags, isFollowing, tier, deleted } = useMemo(() => { + const { isFollowing, deleted } = useMemo(() => { return { ...mlModelDetail, tier: getTierTags(mlModelDetail.tags ?? []), @@ -157,16 +150,6 @@ const MlModelDetail: FC = ({ } }; - const onDescriptionUpdate = async (updatedHTML: string) => { - if (mlModelDetail.description !== updatedHTML) { - const updatedMlModelDetails = { - ...mlModelDetail, - description: updatedHTML, - }; - await descriptionUpdateHandler(updatedMlModelDetails); - } - }; - const onOwnerUpdate = useCallback( async (newOwners?: Mlmodel['owners']) => { const updatedMlModelDetails = { @@ -217,10 +200,6 @@ const MlModelDetail: FC = ({ } }; - const onFeaturesUpdate = async (features: Mlmodel['mlFeatures']) => { - await updateMlModelFeatures({ ...mlModelDetail, mlFeatures: features }); - }; - const getMlHyperParametersColumn: ColumnsType = useMemo( () => [ { @@ -313,16 +292,6 @@ const MlModelDetail: FC = ({ ); }, [mlModelDetail, mlModelStoreColumn]); - const handleTagSelection = async (selectedTags: EntityTags[]) => { - const updatedTags: TagLabel[] | undefined = createTagObject(selectedTags); - - if (updatedTags && mlModelDetail) { - const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; - const updatedMlModel = { ...mlModelDetail, tags: updatedTags }; - await tagUpdateHandler(updatedMlModel); - } - }; - const afterDeleteAction = useCallback( (isSoftDelete?: boolean, version?: number) => isSoftDelete ? handleToggleDelete(version) : history.push('/'), @@ -330,9 +299,6 @@ const MlModelDetail: FC = ({ ); const { - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, editCustomAttributePermission, editLineagePermission, viewAllPermission, @@ -357,163 +323,61 @@ const MlModelDetail: FC = ({ [mlModelPermissions, deleted] ); - const tabs = useMemo( - () => [ - { - name: t('label.feature-plural'), - label: ( - - ), - key: EntityTabs.FEATURES, - children: ( - -
- - - - - ), - ...COMMON_RESIZABLE_PANEL_CONFIG.LEFT_PANEL, - }} - secondPanel={{ - children: ( -
- - editCustomAttributePermission={ - editCustomAttributePermission - } - editGlossaryTermsPermission={ - editGlossaryTermsPermission - } - editTagPermission={editTagsPermission} - entityType={EntityType.MLMODEL} - selectedTags={mlModelTags} - viewAllPermission={viewAllPermission} - onExtensionUpdate={onExtensionUpdate} - onTagSelectionChange={handleTagSelection} - /> -
- ), - ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, - className: - 'entity-resizable-right-panel-container entity-resizable-panel-container', - }} - /> - - - ), - }, - { - label: ( - - ), - key: EntityTabs.ACTIVITY_FEED, - children: ( - - ), - }, - { - label: ( - - ), - key: EntityTabs.DETAILS, - children: ( - -
{getMlHyperParameters} - {getMlModelStore} - - ), - }, - { - label: , - key: EntityTabs.LINEAGE, - children: ( - - - - ), - }, - { - label: ( - - ), - key: EntityTabs.CUSTOM_PROPERTIES, - children: mlModelDetail && ( -
- - entityType={EntityType.MLMODEL} - hasEditAccess={editCustomAttributePermission} - hasPermission={viewAllPermission} - /> -
- ), - }, - ], - [ - feedCount.totalCount, + const tabs = useMemo(() => { + const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs); + + const tabs = mlModelDetailsClassBase.getMlModelDetailPageTabs({ + feedCount, activeTab, mlModelDetail, - mlModelName, - mlModelPermissions, getMlHyperParameters, getMlModelStore, handleFeedCount, - onExtensionUpdate, - onFeaturesUpdate, - onDescriptionUpdate, - deleted, - editTagsPermission, - editGlossaryTermsPermission, - editDescriptionPermission, - editCustomAttributePermission, + fetchEntityFeedCount, editLineagePermission, + editCustomAttributePermission, viewAllPermission, - ] - ); + fetchMlModel, + labelMap: tabLabelMap, + }); + + return getDetailsTabWithNewLabel( + tabs, + customizedPage?.tabs, + EntityTabs.FEATURES + ); + }, [ + feedCount.totalCount, + activeTab, + mlModelDetail, + getMlHyperParameters, + getMlModelStore, + handleFeedCount, + fetchEntityFeedCount, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + fetchMlModel, + customizedPage?.tabs, + ]); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === PageType.MlModel) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona]); return ( = ({ data={mlModelDetail} permissions={mlModelPermissions} type={EntityType.MLMODEL} - onUpdate={settingsUpdateHandler}> + onUpdate={onMlModelUpdate}>
{ fetchMlModel: () => void; followMlModelHandler: () => Promise; unFollowMlModelHandler: () => Promise; - descriptionUpdateHandler: (updatedMlModel: Mlmodel) => Promise; - tagUpdateHandler: (updatedMlModel: Mlmodel) => Promise; - updateMlModelFeatures: (updatedMlModel: Mlmodel) => Promise; settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise; versionHandler: () => void; - onExtensionUpdate: (updatedMlModel: Mlmodel) => Promise; handleToggleDelete: (version?: number) => void; onUpdateVote: (data: QueryVote, id: string) => Promise; + onMlModelUpdate: (data: Mlmodel) => Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.test.tsx index 8389d5255a33..6d1a72f8170f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.test.tsx @@ -13,7 +13,6 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { Mlmodel } from '../../../generated/entity/data/mlmodel'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import MlModelFeaturesList from './MlModelFeaturesList'; @@ -143,27 +142,19 @@ jest.mock( }) ); -const handleFeaturesUpdate = jest.fn(); +const mockHandleFeaturesUpdate = jest.fn(); -const mockProp = { - mlFeatures: mockData['mlFeatures'] as Mlmodel['mlFeatures'], - handleFeaturesUpdate, - permissions: DEFAULT_ENTITY_PERMISSION, - onThreadLinkSelect: jest.fn(), - entityFieldThreads: [ - { - entityLink: - '<#E::mlmodel::mlflow_svc.eta_predictions::mlFeatures::sales::description>', - count: 1, - entityField: 'mlFeatures::sales::description', - }, - ], - entityFqn: 'mlflow_svc.eta_predictions', -}; +jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ + useGenericContext: jest.fn().mockImplementation(() => ({ + data: mockData, + permissions: DEFAULT_ENTITY_PERMISSION, + onUpdate: mockHandleFeaturesUpdate, + })), +})); describe('Test MlModel feature list', () => { it('Should render MlModel feature list component', async () => { - render(, { + render(, { wrapper: MemoryRouter, }); @@ -173,7 +164,7 @@ describe('Test MlModel feature list', () => { }); it('Should render proper feature cards', async () => { - render(, { + render(, { wrapper: MemoryRouter, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx index f71ae8721792..9d4f38a4a929 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelFeaturesList.tsx @@ -14,32 +14,35 @@ import { Card, Col, Divider, Row, Space, Typography } from 'antd'; import { isEmpty } from 'lodash'; import { EntityTags } from 'Models'; -import React, { Fragment, useMemo, useState } from 'react'; +import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EntityType } from '../../../enums/entity.enum'; -import { MlFeature } from '../../../generated/entity/data/mlmodel'; +import { MlFeature, Mlmodel } from '../../../generated/entity/data/mlmodel'; import { TagSource } from '../../../generated/type/schema'; import { getEntityName } from '../../../utils/EntityUtils'; import { createTagObject } from '../../../utils/TagsUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import TableDescription from '../../Database/TableDescription/TableDescription.component'; import TableTags from '../../Database/TableTags/TableTags.component'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; -import { MlModelFeaturesListProp } from './MlModel.interface'; import SourceList from './SourceList.component'; -const MlModelFeaturesList = ({ - mlFeatures, - handleFeaturesUpdate, - permissions, - isDeleted, - entityFqn, -}: MlModelFeaturesListProp) => { +const MlModelFeaturesList = () => { const { t } = useTranslation(); const [selectedFeature, setSelectedFeature] = useState( {} as MlFeature ); const [editDescription, setEditDescription] = useState(false); + const { data, onUpdate, permissions } = useGenericContext(); + + const { mlFeatures, isDeleted, entityFqn } = useMemo(() => { + return { + mlFeatures: data?.mlFeatures, + isDeleted: data?.deleted, + entityFqn: data?.fullyQualifiedName ?? '', + }; + }, [data]); const hasEditPermission = useMemo( () => permissions.EditTags || permissions.EditAll, @@ -51,6 +54,13 @@ const MlModelFeaturesList = ({ [permissions] ); + const handleFeaturesUpdate = useCallback( + async (features: MlFeature[]) => { + await onUpdate({ ...data, mlFeatures: features }); + }, + [data, onUpdate] + ); + const handleCancelEditDescription = () => { setSelectedFeature({}); setEditDescription(false); @@ -58,16 +68,17 @@ const MlModelFeaturesList = ({ const handleDescriptionChange = async (value: string) => { if (!isEmpty(selectedFeature) && editDescription) { - const updatedFeatures = mlFeatures?.map((feature) => { - if (feature.name === selectedFeature.name) { - return { - ...selectedFeature, - description: value, - }; - } else { - return feature; - } - }); + const updatedFeatures = + mlFeatures?.map((feature) => { + if (feature.name === selectedFeature.name) { + return { + ...selectedFeature, + description: value, + }; + } else { + return feature; + } + }) ?? []; await handleFeaturesUpdate(updatedFeatures); handleCancelEditDescription(); } @@ -80,16 +91,17 @@ const MlModelFeaturesList = ({ const newSelectedTags = createTagObject(selectedTags); if (newSelectedTags && targetFeature) { - const updatedFeatures = mlFeatures?.map((feature) => { - if (feature.name === targetFeature?.name) { - return { - ...targetFeature, - tags: newSelectedTags, - }; - } else { - return feature; - } - }); + const updatedFeatures = + mlFeatures?.map((feature) => { + if (feature.name === targetFeature?.name) { + return { + ...targetFeature, + tags: newSelectedTags, + }; + } else { + return feature; + } + }) ?? []; await handleFeaturesUpdate(updatedFeatures); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts index 9785aa844435..fdc5f4af4447 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/page.ts @@ -125,6 +125,7 @@ export enum PageType { GlossaryTerm = "GlossaryTerm", LandingPage = "LandingPage", Metric = "Metric", + MlModel = "MlModel", Pipeline = "Pipeline", SearchIndex = "SearchIndex", StoredProcedure = "StoredProcedure", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx index ca5366075bf8..3826cfb2fb13 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx @@ -131,20 +131,6 @@ const MlModelPage = () => { [mlModelDetail] ); - const descriptionUpdateHandler = async (updatedMlModel: Mlmodel) => { - try { - const response = await saveUpdatedMlModelData(updatedMlModel); - const { description, version } = response; - setMlModelDetail((preVDetail) => ({ - ...preVDetail, - description, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const followMlModel = async () => { try { const res = await addFollower(mlModelDetail.id, USERId); @@ -183,24 +169,6 @@ const MlModelPage = () => { } }; - const onTagUpdate = async (updatedMlModel: Mlmodel) => { - try { - const { tags, version } = await saveUpdatedMlModelData(updatedMlModel); - setMlModelDetail((preVDetail) => ({ - ...preVDetail, - tags, - version, - })); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.entity-updating-error', { - entity: t('label.tag-plural'), - }) - ); - } - }; - const settingsUpdateHandler = async ( updatedMlModel: Mlmodel ): Promise => { @@ -224,41 +192,6 @@ const MlModelPage = () => { } }; - const updateMlModelFeatures = async (updatedMlModel: Mlmodel) => { - try { - const { mlFeatures, version } = await saveUpdatedMlModelData( - updatedMlModel - ); - setMlModelDetail((preVDetail) => ({ - ...preVDetail, - mlFeatures, - version, - })); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - - const handleExtensionUpdate = useCallback( - async (updatedMlModel: Mlmodel) => { - try { - const data = await saveUpdatedMlModelData({ - ...mlModelDetail, - extension: updatedMlModel.extension, - }); - setMlModelDetail(data); - } catch (error) { - showErrorToast( - error as AxiosError, - t('server.entity-updating-error', { - entity: getEntityName(mlModelDetail), - }) - ); - } - }, - [saveUpdatedMlModelData, setMlModelDetail, mlModelDetail] - ); - const versionHandler = () => { history.push( getVersionPath( @@ -304,6 +237,26 @@ const MlModelPage = () => { })); }, []); + const handleMlModelUpdate = useCallback( + async (data: Mlmodel) => { + try { + const response = await saveUpdatedMlModelData(data); + setMlModelDetail((prev) => ({ + ...prev, + ...response, + })); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-updating-error', { + entity: getEntityName(mlModelDetail), + }) + ); + } + }, + [saveUpdatedMlModelData] + ); + useEffect(() => { fetchResourcePermission(mlModelFqn); }, [mlModelFqn]); @@ -326,18 +279,15 @@ const MlModelPage = () => { return ( fetchMlModelDetails(mlModelFqn)} followMlModelHandler={followMlModel} handleToggleDelete={handleToggleDelete} mlModelDetail={mlModelDetail} settingsUpdateHandler={settingsUpdateHandler} - tagUpdateHandler={onTagUpdate} unFollowMlModelHandler={unFollowMlModel} updateMlModelDetailsState={updateMlModelDetailsState} - updateMlModelFeatures={updateMlModelFeatures} versionHandler={versionHandler} - onExtensionUpdate={handleExtensionUpdate} + onMlModelUpdate={handleMlModelUpdate} onUpdateVote={updateVote} /> ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 345c3889d110..2368ffbf1195 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -28,6 +28,7 @@ import domainClassBase from '../Domain/DomainClassBase'; import { getEntityName } from '../EntityUtils'; import i18n from '../i18next/LocalUtil'; import metricDetailsClassBase from '../MetricEntityUtils/MetricDetailsClassBase'; +import mlModelClassBase from '../MlModel/MlModelClassBase'; import pipelineClassBase from '../PipelineClassBase'; import searchIndexClassBase from '../SearchIndexDetailsClassBase'; import storedProcedureClassBase from '../StoredProcedureBase'; @@ -187,6 +188,8 @@ export const getDefaultTabs = (pageType?: string): Tab[] => { return apiEndpointClassBase.getEndpointDetailPageTabsIds(); case PageType.Metric: return metricDetailsClassBase.getMetricDetailPageTabsIds(); + case PageType.MlModel: + return mlModelClassBase.getMlModelDetailPageTabsIds(); default: return [ { @@ -234,6 +237,8 @@ export const getDefaultWidgetForTab = (pageType: PageType, tab: EntityTabs) => { return apiEndpointClassBase.getDefaultLayout(tab); case PageType.Metric: return metricDetailsClassBase.getDefaultLayout(tab); + case PageType.MlModel: + return mlModelClassBase.getDefaultLayout(tab); default: return []; } @@ -295,6 +300,8 @@ export const getCustomizableWidgetByPage = ( return apiEndpointClassBase.getCommonWidgetList(); case PageType.Metric: return metricDetailsClassBase.getCommonWidgetList(); + case PageType.MlModel: + return mlModelClassBase.getCommonWidgetList(); case PageType.LandingPage: default: return []; @@ -331,6 +338,8 @@ export const getDummyDataByPage = (pageType: PageType) => { return apiEndpointClassBase.getDummyData(); case PageType.Metric: return metricDetailsClassBase.getDummyData(); + case PageType.MlModel: + return mlModelClassBase.getDummyData(); case PageType.LandingPage: default: @@ -377,6 +386,8 @@ export const getWidgetsFromKey = ( return apiEndpointClassBase.getWidgetsFromKey(widgetConfig); case PageType.Metric: return metricDetailsClassBase.getWidgetsFromKey(widgetConfig); + case PageType.MlModel: + return mlModelClassBase.getWidgetsFromKey(widgetConfig); default: return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts new file mode 100644 index 000000000000..3894aa702f41 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts @@ -0,0 +1,322 @@ +/* + * Copyright 2024 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 { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { + CUSTOM_PROPERTIES_WIDGET, + DATA_PRODUCTS_WIDGET, + DESCRIPTION_WIDGET, + GLOSSARY_TERMS_WIDGET, + GridSizes, + TAGS_WIDGET, +} from '../../constants/CustomizeWidgets.constants'; +import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; +import { EntityTabs } from '../../enums/entity.enum'; +import { + FeatureSourceDataType, + FeatureType, + Mlmodel, + MlModelServiceType, +} from '../../generated/entity/data/mlmodel'; +import { Tab } from '../../generated/system/ui/uiCustomization'; +import { FeedCounts } from '../../interface/feed.interface'; +import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; +import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; +import { + getMlModelDetailsPageTabs, + getMlModelWidgetsFromKey, +} from '../MlModelDetailsUtils'; + +export interface MlModelDetailPageTabProps { + feedCount: FeedCounts; + activeTab: EntityTabs; + editLineagePermission: boolean; + editCustomAttributePermission: boolean; + viewAllPermission: boolean; + fetchMlModel: () => void; + handleFeedCount: (data: FeedCounts) => void; + mlModelDetail: Mlmodel; + getMlHyperParameters: JSX.Element; + getMlModelStore: JSX.Element; + fetchEntityFeedCount: () => void; + labelMap: Record; +} + +class MlModelDetailsClassBase { + tabs = []; + + constructor() { + this.tabs = []; + } + + public getMlModelDetailPageTabs( + mlModelDetailsPageProps: MlModelDetailPageTabProps + ): TabProps[] { + return getMlModelDetailsPageTabs(mlModelDetailsPageProps); + } + + public getMlModelDetailPageTabsIds(): Tab[] { + return [ + EntityTabs.OVERVIEW, + EntityTabs.EXPRESSION, + EntityTabs.ACTIVITY_FEED, + EntityTabs.LINEAGE, + EntityTabs.CUSTOM_PROPERTIES, + ].map((tab: EntityTabs) => ({ + id: tab, + name: tab, + displayName: getTabLabelFromId(tab), + layout: this.getDefaultLayout(tab), + editable: tab === EntityTabs.OVERVIEW, + })); + } + + public getDefaultLayout(tab?: EntityTabs) { + if (tab && tab !== EntityTabs.OVERVIEW) { + return []; + } + + return [ + { + h: 6, + i: DetailPageWidgetKeys.DESCRIPTION, + w: 6, + x: 0, + y: 0, + static: false, + }, + { + h: 1, + i: DetailPageWidgetKeys.DATA_PRODUCTS, + w: 2, + x: 6, + y: 1, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, + y: 2, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, + w: 2, + x: 6, + y: 3, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.RELATED_METRICS, + w: 2, + x: 6, + y: 4, + static: false, + }, + { + h: 4, + i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, + w: 2, + x: 6, + y: 5, + static: false, + }, + ]; + } + public getDummyData(): Mlmodel { + return { + id: '6ef964b1-edb7-4d7c-85f1-51845197206c', + name: 'eta_predictions', + fullyQualifiedName: 'mlflow_svc.eta_predictions', + displayName: 'ETA Predictions', + description: 'ETA Predictions Model', + algorithm: 'Neural Network', + mlFeatures: [ + { + name: 'sales', + dataType: FeatureType.Numerical, + description: 'Sales amount', + fullyQualifiedName: 'mlflow_svc.eta_predictions.sales', + featureSources: [ + { + name: 'gross_sales', + dataType: FeatureSourceDataType.Integer, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.fact_sale.gross_sales', + dataSource: { + id: '8cb6dbd3-1c4b-48c3-ab53-1e964f355d03', + type: 'table', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.fact_sale', + description: + // eslint-disable-next-line max-len + 'The fact table captures the value of products sold or returned, as well as the values of other charges such as taxes and shipping costs. The sales table contains one row per order line item, one row per returned line item, and one row per shipping charge. Use this table when you need financial metrics.', + href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/8cb6dbd3-1c4b-48c3-ab53-1e964f355d03', + }, + tags: [], + }, + ], + }, + { + name: 'persona', + dataType: FeatureType.Categorical, + description: 'type of buyer', + fullyQualifiedName: 'mlflow_svc.eta_predictions.persona', + featureSources: [ + { + name: 'membership', + dataType: FeatureSourceDataType.String, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer.membership', + dataSource: { + id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', + type: 'table', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer', + description: + // eslint-disable-next-line max-len + 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', + href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/7de1572e-e66a-47b4-aec0-67366f1c56fb', + }, + tags: [], + }, + { + name: 'platform', + dataType: FeatureSourceDataType.String, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer.platform', + dataSource: { + id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', + type: 'table', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer', + description: + // eslint-disable-next-line max-len + 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', + href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/7de1572e-e66a-47b4-aec0-67366f1c56fb', + }, + tags: [], + }, + ], + featureAlgorithm: 'PCA', + }, + ], + mlHyperParameters: [ + { + name: 'regularisation', + value: '0.5', + }, + { + name: 'random', + value: 'hello', + }, + ], + target: 'ETA_time', + dashboard: { + id: '1faa74ba-9952-4591-aef2-9acfaf9c14d1', + type: 'dashboard', + name: 'eta_predictions_performance', + fullyQualifiedName: 'sample_superset.eta_predictions_performance', + description: '', + displayName: 'ETA Predictions Performance', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/dashboards/1faa74ba-9952-4591-aef2-9acfaf9c14d1', + }, + mlStore: { + storage: 's3://path-to-pickle', + imageRepository: 'https://docker.hub.com/image', + }, + server: 'http://my-server.ai/', + href: 'http://sandbox-beta.open-metadata.org/api/v1/mlmodels/6ef964b1-edb7-4d7c-85f1-51845197206c', + owners: [], + followers: [], + tags: [], + usageSummary: { + dailyStats: { + count: 0, + percentileRank: 0, + }, + weeklyStats: { + count: 0, + percentileRank: 0, + }, + monthlyStats: { + count: 0, + percentileRank: 0, + }, + date: new Date('2025-02-22'), + }, + version: 1.2, + updatedAt: 1722587630921, + updatedBy: 'harsh.s', + service: { + id: 'b8cbebe5-58f0-493e-81f9-ad76e7695164', + type: 'mlmodelService', + name: 'mlflow_svc', + fullyQualifiedName: 'mlflow_svc', + displayName: 'mlflow_svc', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/mlmodelServices/b8cbebe5-58f0-493e-81f9-ad76e7695164', + }, + serviceType: MlModelServiceType.Mlflow, + deleted: false, + domain: { + id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', + type: 'domain', + name: 'domain.with.dot', + fullyQualifiedName: '"domain.with.dot"', + description: 'domain.with.dot', + displayName: 'domain.with.dot', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/761f0a12-7b08-4889-acc3-b8d4d11a7865', + }, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + }; + } + + public getCommonWidgetList() { + return [ + DESCRIPTION_WIDGET, + DATA_PRODUCTS_WIDGET, + TAGS_WIDGET, + GLOSSARY_TERMS_WIDGET, + { + fullyQualifiedName: DetailPageWidgetKeys.RELATED_METRICS, + name: i18n.t('label.related-metrics'), + data: { + gridSizes: ['large'] as GridSizes[], + }, + }, + CUSTOM_PROPERTIES_WIDGET, + ]; + } + + public getWidgetsFromKey(widgetConfig: WidgetConfig) { + return getMlModelWidgetsFromKey(widgetConfig); + } +} + +const mlModelDetailsClassBase = new MlModelDetailsClassBase(); + +export default mlModelDetailsClassBase; +export { MlModelDetailsClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts deleted file mode 100644 index 1cb212132f1d..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022 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 { TabSpecificField } from '../enums/entity.enum'; - -// eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.DOMAIN},${TabSpecificField.OWNERS}, ${TabSpecificField.DASHBOARD},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx new file mode 100644 index 000000000000..bd1edd011903 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 2022 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 { Col, Row } from 'antd'; +import { t } from 'i18next'; +import React from 'react'; +import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; +import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; +import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; +import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; +import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; +import Lineage from '../components/Lineage/Lineage.component'; +import MlModelFeaturesList from '../components/MlModel/MlModelDetail/MlModelFeaturesList'; +import { SourceType } from '../components/SearchedData/SearchedData.interface'; +import LineageProvider from '../context/LineageProvider/LineageProvider'; +import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; +import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; +import { PageType } from '../generated/system/ui/page'; +import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; +import i18n from './i18next/LocalUtil'; +import { MlModelDetailPageTabProps } from './MlModel/MlModelClassBase'; + +// eslint-disable-next-line max-len +export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.DOMAIN},${TabSpecificField.OWNERS}, ${TabSpecificField.DASHBOARD},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; + +export const getMlModelDetailsPageTabs = ({ + feedCount, + activeTab, + editLineagePermission, + editCustomAttributePermission, + viewAllPermission, + fetchMlModel, + handleFeedCount, + mlModelDetail, + getMlHyperParameters, + getMlModelStore, + fetchEntityFeedCount, + labelMap, +}: MlModelDetailPageTabProps) => { + return [ + { + name: t('label.feature-plural'), + label: ( + + ), + key: EntityTabs.FEATURES, + children: , + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.DETAILS, + children: ( + + {getMlHyperParameters} + {getMlModelStore} + + ), + }, + { + label: ( + + ), + key: EntityTabs.LINEAGE, + children: ( + + + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: mlModelDetail && ( +
+ + entityType={EntityType.MLMODEL} + hasEditAccess={editCustomAttributePermission} + hasPermission={viewAllPermission} + /> +
+ ), + }, + ]; +}; + +export const getMlModelWidgetsFromKey = (widgetConfig: WidgetConfig) => { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.RELATED_METRICS)) { + return ; + } + + return ( + + ); +}; From 4bdbcbeed6f20bbc9560e6ca945cba630b6de6f7 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:11:15 +0530 Subject: [PATCH 48/63] fix test for mlmodel page --- .../MlModel/MlModelDetail/MlModelDetail.component.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.test.tsx index f8f31178d1b4..3470e8d0ee18 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModel/MlModelDetail/MlModelDetail.component.test.tsx @@ -168,7 +168,7 @@ const mockProp = { fetchFeedHandler: jest.fn(), postFeedHandler: jest.fn(), deletePostHandler: jest.fn(), - + onMlModelUpdate: jest.fn(), updateThreadHandler: jest.fn(), entityFieldThreadCount: [], entityFieldTaskCount: [], From 4ba3ca49dfeb8467f993c1135caafbeb288f072a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 17:07:52 +0530 Subject: [PATCH 49/63] fix Metric details tests --- .../pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx index 8d63fc00ce22..a089050ab730 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricDetailsPage/MetricDetailsPage.tsx @@ -83,7 +83,7 @@ const MetricDetailsPage = () => { return { ...previous, version: res.version, - ...(key && { [key]: res[key] }), + ...(key ? { [key]: res[key] } : res), }; }); } catch (error) { From 6ebc687257684365650cb61d73d0503e5f967902 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 22 Feb 2025 23:27:10 +0530 Subject: [PATCH 50/63] fix tests --- .../ui/src/components/Topic/TopicSchema/TopicSchema.tsx | 6 +++--- .../pages/APICollectionPage/APICollectionVersionPage.tsx | 9 +++++++++ .../DatabaseSchemaVersionPage.tsx | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx index d4cdaf0daeb9..37198496ba41 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicSchema/TopicSchema.tsx @@ -107,10 +107,10 @@ const TopicSchemaFields: FC = ({ } = useMemo( () => ({ hasDescriptionEditAccess: - permissions.EditAll ?? permissions.EditDescription, - hasTagEditAccess: permissions.EditAll ?? permissions.EditTags, + permissions.EditAll || permissions.EditDescription, + hasTagEditAccess: permissions.EditAll || permissions.EditTags, hasGlossaryTermEditAccess: - permissions.EditAll ?? permissions.EditGlossaryTerms, + permissions.EditAll || permissions.EditGlossaryTerms, }), [permissions] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx index 71f157d6f6ff..64dbac73fd39 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionVersionPage.tsx @@ -19,6 +19,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; @@ -283,6 +284,14 @@ const APICollectionVersionPage = () => { key: EntityTabs.API_ENDPOINT, children: ( +
+ + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx index 1f37c496d1b1..1d213e31bbf9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx @@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; +import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../components/common/Loader/Loader'; import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPrevious.interface'; @@ -250,6 +251,14 @@ function DatabaseSchemaVersionPage() { key: EntityTabs.TABLE, children: ( + + + From 3d0b9a6f2fabd5a12cf21e40c2eb240be3ce2443 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:28:47 +0530 Subject: [PATCH 51/63] fix test for database schema version page --- .../src/main/resources/json/schema/system/ui/page.json | 3 ++- .../DatabaseSchemaVersionPage.test.tsx | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json index 0937c753558b..40f2cb7c92dc 100644 --- a/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json +++ b/openmetadata-spec/src/main/resources/json/schema/system/ui/page.json @@ -27,7 +27,8 @@ "Domain", "APICollection", "APIEndpoint", - "Metric" + "Metric", + "MlModel" ] } }, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx index ddc9367e12d5..faa74d1d93a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.test.tsx @@ -174,6 +174,10 @@ jest.mock( }) ); +jest.mock('../../components/common/EntityDescription/DescriptionV1', () => + jest.fn().mockImplementation(() =>
description
) +); + describe('DatabaseSchemaVersionPage', () => { it('should render all necessary components', async () => { await act(async () => { From 0a9d3c22266ee429cb824ed83cece7eb52410e8f Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sun, 23 Feb 2025 15:08:08 +0530 Subject: [PATCH 52/63] reset max failure for playwright --- openmetadata-ui/src/main/resources/ui/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright.config.ts b/openmetadata-ui/src/main/resources/ui/playwright.config.ts index 35a86f6f8e40..c47a3699213b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright.config.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 4 : undefined, - // maxFailures: 30, + maxFailures: 30, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], From ee5f4f5b17bf1c3345ef28be79eae0f2b923f092 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:46:24 +0530 Subject: [PATCH 53/63] improve icons --- .../generated/system/ui/uiCustomization.ts | 4 ++ .../ui/src/utils/Persona/PersonaUtils.ts | 42 ++++++++++--------- .../resources/ui/src/utils/TableUtils.tsx | 5 +-- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/uiCustomization.ts b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/uiCustomization.ts index 6f7d7ac29d1f..de86db95d766 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/uiCustomization.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/system/ui/uiCustomization.ts @@ -210,6 +210,8 @@ export enum EntityType { * This schema defines the type used for describing different types of pages. */ export enum PageType { + APICollection = "APICollection", + APIEndpoint = "APIEndpoint", Container = "Container", Dashboard = "Dashboard", DashboardDataModel = "DashboardDataModel", @@ -219,6 +221,8 @@ export enum PageType { Glossary = "Glossary", GlossaryTerm = "GlossaryTerm", LandingPage = "LandingPage", + Metric = "Metric", + MlModel = "MlModel", Pipeline = "Pipeline", SearchIndex = "SearchIndex", StoredProcedure = "StoredProcedure", diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Persona/PersonaUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Persona/PersonaUtils.ts index c795e521d1ad..e7c8ac9f844b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Persona/PersonaUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Persona/PersonaUtils.ts @@ -17,41 +17,45 @@ import { ReactComponent as DatabaseIcon } from '../../assets/svg/database-colore import { ReactComponent as GlossaryIcon } from '../../assets/svg/glossary-colored.svg'; import { ReactComponent as GovernIcon } from '../../assets/svg/governance.svg'; import { ReactComponent as HomepageIcon } from '../../assets/svg/homepage.svg'; +import { ReactComponent as APICollectionIcon } from '../../assets/svg/ic-api-collection.svg'; +import { ReactComponent as APIEndpointIcon } from '../../assets/svg/ic-api-endpoint.svg'; import { ReactComponent as DashboardDataModelIcon } from '../../assets/svg/ic-dashboard-data-model-colored.svg'; import { ReactComponent as SchemaIcon } from '../../assets/svg/ic-database-schema-colored.svg'; import { ReactComponent as MessagingIcon } from '../../assets/svg/messaging-colored.svg'; +import { ReactComponent as MetricColoredIcon } from '../../assets/svg/metric-colored.svg'; +import { ReactComponent as MlModelIcon } from '../../assets/svg/ml-model-colored.svg'; import { ReactComponent as NavigationIcon } from '../../assets/svg/navigation.svg'; import { ReactComponent as PipelineIcon } from '../../assets/svg/pipeline-colored.svg'; import { ReactComponent as SearchIcon } from '../../assets/svg/search-colored.svg'; import { ReactComponent as StorageIcon } from '../../assets/svg/storage-colored.svg'; import { ReactComponent as StoredProcedureIcon } from '../../assets/svg/stored-procedure-colored.svg'; import { ReactComponent as TableIcon } from '../../assets/svg/table-colored.svg'; - -import { EntityType } from '../../enums/entity.enum'; import { PageType } from '../../generated/system/ui/uiCustomization'; import { SettingMenuItem } from '../GlobalSettingsUtils'; import i18n from '../i18next/LocalUtil'; const ENTITY_ICONS: Record = { - [EntityType.TABLE]: TableIcon, - [EntityType.CONTAINER]: StorageIcon, - [EntityType.DASHBOARD]: DashboardIcon, - [EntityType.DASHBOARD_DATA_MODEL]: DashboardDataModelIcon, - [EntityType.DATABASE]: DatabaseIcon, - [EntityType.DATABASE_SCHEMA]: SchemaIcon, - [EntityType.DOMAIN]: SchemaIcon, - [EntityType.GLOSSARY]: GlossaryIcon, - [EntityType.GLOSSARY_TERM]: GlossaryIcon, - [EntityType.PIPELINE]: PipelineIcon, - [EntityType.SEARCH_INDEX]: SearchIcon, - [EntityType.STORED_PROCEDURE]: StoredProcedureIcon, - [EntityType.TOPIC]: MessagingIcon, - [EntityType.GOVERN]: GovernIcon, + [PageType.Table]: TableIcon, + [PageType.Container]: StorageIcon, + [PageType.Dashboard]: DashboardIcon, + [PageType.DashboardDataModel]: DashboardDataModelIcon, + [PageType.Database]: DatabaseIcon, + [PageType.DatabaseSchema]: SchemaIcon, + [PageType.Domain]: SchemaIcon, + [PageType.Glossary]: GlossaryIcon, + [PageType.GlossaryTerm]: GlossaryIcon, + [PageType.Pipeline]: PipelineIcon, + [PageType.SearchIndex]: SearchIcon, + [PageType.StoredProcedure]: StoredProcedureIcon, + [PageType.Topic]: MessagingIcon, + ['govern']: GovernIcon, ['dataAssets']: DataAssetsIcon, ['homepage']: HomepageIcon, ['navigation']: NavigationIcon, - ['governance']: GovernIcon, - [PageType.LandingPage]: MessagingIcon, + [PageType.APICollection]: APICollectionIcon, + [PageType.APIEndpoint]: APIEndpointIcon, + [PageType.MlModel]: MlModelIcon, + [PageType.Metric]: MetricColoredIcon, }; export const getCustomizePageCategories = (): SettingMenuItem[] => { @@ -87,7 +91,7 @@ const generateSettingItems = (pageType: PageType): SettingMenuItem => ({ key: pageType, label: startCase(pageType), description: pageType, - icon: ENTITY_ICONS[camelCase(pageType)], + icon: ENTITY_ICONS[pageType], }); export const getCustomizePageOptions = ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 789c93bb3292..2670aabc4c7b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -67,12 +67,11 @@ import { ReactComponent as IconDistKey } from '../assets/svg/icon-distribution.s import { ReactComponent as IconKeyLineThrough } from '../assets/svg/icon-key-line-through.svg'; import { ReactComponent as IconKey } from '../assets/svg/icon-key.svg'; import { ReactComponent as IconNotNullLineThrough } from '../assets/svg/icon-not-null-line-through.svg'; -import { ReactComponent as IconSortLineThrough } from '../assets/svg/icon-sort-line-through.svg'; -import { ReactComponent as IconTestSuite } from '../assets/svg/icon-test-suite.svg'; - import { ReactComponent as IconNotNull } from '../assets/svg/icon-not-null.svg'; import { ReactComponent as RoleIcon } from '../assets/svg/icon-role-grey.svg'; +import { ReactComponent as IconSortLineThrough } from '../assets/svg/icon-sort-line-through.svg'; import { ReactComponent as IconSortKey } from '../assets/svg/icon-sort.svg'; +import { ReactComponent as IconTestSuite } from '../assets/svg/icon-test-suite.svg'; import { ReactComponent as IconUniqueLineThrough } from '../assets/svg/icon-unique-line-through.svg'; import { ReactComponent as IconUnique } from '../assets/svg/icon-unique.svg'; import { ReactComponent as KPIIcon } from '../assets/svg/kpi.svg'; From f4d9a3dc04fd515365a7084e72e8788fd83f39c7 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:29:17 +0530 Subject: [PATCH 54/63] address comments --- .../entity/ingestion/ServiceBaseClass.ts | 10 + .../ContainerWidget/ContainerWidget.tsx | 13 +- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 13 +- .../Customization/GenericTab/GenericTab.tsx | 4 +- .../GenericWidget/GenericWidget.tsx | 384 +----------------- .../PipelineDetails/PipelineDetails.test.tsx | 4 - .../constants/CustomizeWidgets.constants.ts | 44 +- .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/styles/layout/page-layout.less | 4 + .../CustomizeGlossaryTermBaseClass.ts | 127 ++++++ .../utils/CustomizePage/CustomizePageUtils.ts | 6 - .../resources/ui/src/utils/TableClassBase.ts | 134 +++++- 12 files changed, 330 insertions(+), 414 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index 5f6f13cdf231..2b88a4cf8609 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -517,6 +517,16 @@ class ServiceBaseClass { await expect(page.getByTestId('markdown-parser').first()).toHaveText( description ); + + // Check for right side widgets visibility + await expect(page.getByTestId('KnowledgePanel.Tags')).toBeVisible(); + await expect( + page.getByTestId('KnowledgePanel.GlossaryTerms') + ).toBeVisible(); + await expect(page.getByTestId('KnowledgePanel.DataProducts')).toBeVisible(); + await expect( + page.getByTestId('KnowledgePanel.CustomProperties') + ).toBeVisible(); } async runAdditionalTests( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx index f9fd3034a8da..5a9daa534359 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Container/ContainerWidget/ContainerWidget.tsx @@ -32,15 +32,16 @@ export const ContainerWidget = () => { editTagsPermission, deleted, } = useMemo(() => { + const isDeleted = containerData?.deleted; + return { editDescriptionPermission: - permissions.EditAll || - permissions.EditDescription || - permissions.EditCustomFields, + (permissions.EditAll || permissions.EditDescription) && !isDeleted, editGlossaryTermsPermission: - permissions.EditAll || permissions.EditGlossaryTerms, - editTagsPermission: permissions.EditAll || permissions.EditTags, - deleted: containerData?.deleted, + (permissions.EditAll || permissions.EditGlossaryTerms) && !isDeleted, + editTagsPermission: + (permissions.EditAll || permissions.EditTags) && !isDeleted, + deleted: isDeleted, }; }, [permissions, containerData]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx index 527efcccf396..9a6ad6b73f28 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -17,7 +17,10 @@ import React, { useCallback, useMemo, useState } from 'react'; import RGL, { Layout, WidthProvider } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; -import { CommonWidgetType } from '../../../constants/CustomizeWidgets.constants'; +import { + CommonWidgetType, + TAB_GRID_MAX_COLUMNS, +} from '../../../constants/CustomizeWidgets.constants'; import { EntityTabs } from '../../../enums/entity.enum'; import { Document } from '../../../generated/entity/docStore/document'; import { Page, Tab } from '../../../generated/system/ui/page'; @@ -112,7 +115,7 @@ export const CustomizeTabWidget = () => { tabs: [ ...items, { - name: 'New Tab', + name: t('label.new-tab'), layout: [], id: newActiveKey, editable: true, @@ -240,7 +243,7 @@ export const CustomizeTabWidget = () => { newWidgetData as unknown as Document, placeholderWidgetKey, widgetSize, - 8 + TAB_GRID_MAX_COLUMNS ) ); setIsWidgetModalOpen(false); @@ -307,7 +310,7 @@ export const CustomizeTabWidget = () => { { setIsWidgetModalOpen(false)} - maxGridSizeSupport={8} + maxGridSizeSupport={TAB_GRID_MAX_COLUMNS} open={isWidgetModalOpen} placeholderWidgetKey={placeholderWidgetKey} widgetsList={getCustomizableWidgetByPage(currentPageType)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx index bd0f0a2b010d..10edd08dbac4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericTab/GenericTab.tsx @@ -55,10 +55,10 @@ export const GenericTab = ({ type }: GenericTabProps) => { return renderedWidget ? (
+ key={widget.i}> {renderedWidget}
) : null; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx index cb7d4ec94097..821762599430 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx @@ -14,6 +14,10 @@ import { CloseOutlined, DragOutlined } from '@ant-design/icons'; import { Card, Space } from 'antd'; import { noop, startCase } from 'lodash'; import React, { useMemo } from 'react'; +import { + DUMMY_OWNER_LIST, + DUMMY_TAGS_LIST, +} from '../../../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys, GlossaryTermDetailPageWidgetKeys, @@ -21,7 +25,6 @@ import { import { EntityType } from '../../../enums/entity.enum'; import { Container, - DataType, EntityReference, TagSource, } from '../../../generated/entity/data/container'; @@ -32,6 +35,7 @@ import { WidgetCommonProps } from '../../../pages/CustomizablePage/CustomizableP import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import containerDetailsClassBase from '../../../utils/ContainerDetailsClassBase'; +import customizeGlossaryTermPageClassBase from '../../../utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; import dashboardDataModelClassBase from '../../../utils/DashboardDataModelBase'; import domainClassBase from '../../../utils/Domain/DomainClassBase'; import { renderReferenceElement } from '../../../utils/GlossaryUtils'; @@ -49,10 +53,7 @@ import ModelTab from '../../Dashboard/DataModel/DataModels/ModelTab/ModelTab.com import SchemaTable from '../../Database/SchemaTable/SchemaTable.component'; import DataProductsContainer from '../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import GlossaryTermTab from '../../Glossary/GlossaryTermTab/GlossaryTermTab.component'; -import { - ModifiedGlossary, - useGlossaryStore, -} from '../../Glossary/useGlossary.store'; +import { useGlossaryStore } from '../../Glossary/useGlossary.store'; import TagsViewer from '../../Tag/TagsViewer/TagsViewer'; import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface'; import TopicSchemaFields from '../../Topic/TopicSchema/TopicSchema'; @@ -71,159 +72,9 @@ export const GenericWidget = (props: WidgetCommonProps) => { if ( props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.TERMS_TABLE) ) { - setGlossaryChildTerms([ - { - id: 'ea7c8380-34a9-4ea9-93ea-a812c0e838d6', - name: 'Finance', - displayName: 'Finance', - description: - 'A finance department is the unit of a business responsible for obtaining and handling any monies on behalf of the organization', - - fullyQualifiedName: 'Business Department.Finance', - - glossary: { - id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - type: 'glossary', - name: 'Business Department', - fullyQualifiedName: 'Business Department', - description: - 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', - displayName: 'Business Department', - deleted: false, - }, - references: [], - version: 0.9, - updatedAt: 1727894458563, - updatedBy: 'anandbhandari', - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/ea7c8380-34a9-4ea9-93ea-a812c0e838d6', - owners: [], - - status: 'Approved', - deleted: false, - - mutuallyExclusive: false, - childrenCount: 1, - }, - { - id: 'a8409ff4-b540-4ab0-9332-73f34125651c', - name: 'FOO', - displayName: '', - description: 'VCASCAS', - - fullyQualifiedName: 'Business Department.FOO', - synonyms: [], - glossary: { - id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - type: 'glossary', - name: 'Business Department', - fullyQualifiedName: 'Business Department', - description: - 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', - displayName: 'Business Department', - deleted: false, - }, - references: [], - version: 0.1, - updatedAt: 1724662513442, - updatedBy: 'teddy', - owners: [], - status: 'Approved', - deleted: false, - mutuallyExclusive: false, - childrenCount: 0, - }, - { - id: '5c415db9-0927-4815-b31b-ae8247ea6b0a', - name: 'Human resources', - displayName: 'Human resources', - description: - 'Human resources (HR) is the department in a company that handles all things related to employees.', - - fullyQualifiedName: 'Business Department.Human resources', - synonyms: ['Manpower', 'Human capital'], - glossary: { - id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - type: 'glossary', - name: 'Business Department', - fullyQualifiedName: 'Business Department', - description: - 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', - displayName: 'Business Department', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaries/dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - }, - references: [], - version: 0.2, - updatedAt: 1701067069097, - updatedBy: 'sonal.w', - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/5c415db9-0927-4815-b31b-ae8247ea6b0a', - owners: [], - changeDescription: { - fieldsAdded: [], - fieldsUpdated: [ - { - name: 'status', - oldValue: 'Draft', - newValue: 'Approved', - }, - ], - fieldsDeleted: [], - previousVersion: 0.1, - }, - status: 'Approved', - deleted: false, - - mutuallyExclusive: false, - childrenCount: 0, - }, - { - id: 'e866ee75-711a-4649-968d-3ea889bd75b8', - name: 'Marketing', - displayName: 'Marketing', - description: - 'A marketing department is a division within a business that helps to promote its brand, products and services.', - style: {}, - fullyQualifiedName: 'Business Department.Marketing', - synonyms: ['Sell', 'Retails'], - glossary: { - id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - type: 'glossary', - name: 'Business Department', - fullyQualifiedName: 'Business Department', - description: - 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', - displayName: 'Business Department', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaries/dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', - }, - references: [], - version: 0.2, - updatedAt: 1700558309238, - updatedBy: 'shailesh', - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/e866ee75-711a-4649-968d-3ea889bd75b8', - owners: [], - - status: 'Rejected', - deleted: false, - - mutuallyExclusive: false, - childrenCount: 1, - }, - { - id: '288cfb46-a4c2-45a4-9dc0-321eac165812', - name: 'test_business_term', - displayName: 'Test Business Term', - description: 'this is test_business_term', - fullyQualifiedName: 'Business Department.test_business_term', - version: 0.2, - updatedAt: 1728547870161, - updatedBy: 'karan', - href: 'http://sandbox-beta.open-metadata.org/api/v1/glossaryTerms/288cfb46-a4c2-45a4-9dc0-321eac165812', - owners: [], - deleted: false, - mutuallyExclusive: false, - }, - ] as ModifiedGlossary[]); + setGlossaryChildTerms( + customizeGlossaryTermPageClassBase.getGlossaryChildTerms() + ); } return () => setGlossaryChildTerms([]); @@ -241,28 +92,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { displayType={DisplayType.READ_MORE} showNoDataPlaceholder={false} tagType={TagSource.Glossary} - tags={[ - { - tagFQN: 'BusinessGlossary.Purchase', - source: TagSource.Glossary, - name: 'Purchase', - }, - { - tagFQN: 'Person.BankNumber', - source: TagSource.Glossary, - name: 'BankNumber', - }, - { - tagFQN: 'Hospitality.Guest Type', - source: TagSource.Glossary, - name: 'Guest Type', - }, - { - tagFQN: 'Financial Services', - source: TagSource.Glossary, - name: 'Auto Loan', - }, - ]} + tags={DUMMY_TAGS_LIST} /> ); } else if ( @@ -314,55 +144,13 @@ export const GenericWidget = (props: WidgetCommonProps) => { displayType={DisplayType.READ_MORE} showNoDataPlaceholder={false} tagType={TagSource.Classification} - tags={[ - { - tagFQN: 'General.BankNumber', - source: TagSource.Classification, - name: 'BankNumber', - }, - { - tagFQN: 'General.DriverLicense', - source: TagSource.Classification, - name: 'DriverLicense', - }, - { - tagFQN: 'PII.Sensitive', - source: TagSource.Classification, - name: 'Sensitive', - }, - { - tagFQN: 'Tier.Tier1', - source: TagSource.Classification, - name: 'Tier1', - }, - { - tagFQN: 'PersonalData.SpecialCategory', - source: TagSource.Classification, - name: 'SpecialCategory', - }, - ]} + tags={DUMMY_TAGS_LIST} /> ); } else if ( props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.OWNER) ) { - return ( - - ); + return ; } else if ( props.widgetKey.startsWith(DetailPageWidgetKeys.CUSTOM_PROPERTIES) || props.widgetKey.startsWith( @@ -381,23 +169,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { } else if ( props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.REVIEWER) ) { - return ( - - ); + return ; } else if ( props.widgetKey.startsWith(DetailPageWidgetKeys.DESCRIPTION) || props.widgetKey.startsWith(GlossaryTermDetailPageWidgetKeys.DESCRIPTION) @@ -409,135 +181,7 @@ export const GenericWidget = (props: WidgetCommonProps) => { } else if (props.widgetKey.startsWith(DetailPageWidgetKeys.TABLE_SCHEMA)) { return ( - data={{ - ...tableClassBase.getDummyData(), - columns: [ - { - name: 'address_id', - dataType: DataType.Numeric, - dataTypeDisplay: 'numeric', - description: 'Unique identifier for the address.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', - tags: [], - ordinalPosition: 1, - }, - { - name: 'shop_id', - dataType: DataType.Numeric, - dataTypeDisplay: 'numeric', - description: - 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim_shop table.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.shop_id', - tags: [], - ordinalPosition: 2, - }, - { - name: 'first_name', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'First name of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.first_name', - tags: [], - ordinalPosition: 3, - }, - { - name: 'last_name', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'Last name of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.last_name', - tags: [], - ordinalPosition: 4, - }, - { - name: 'address', - dataType: DataType.Varchar, - dataLength: 500, - dataTypeDisplay: 'varchar', - description: 'Clean address test', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.address', - tags: [], - ordinalPosition: 5, - }, - { - name: 'company', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: - "The name of the customer's business, if one exists.", - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.company', - tags: [], - ordinalPosition: 7, - }, - { - name: 'city', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'The name of the city. For example, Palo Alto.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.city', - tags: [], - ordinalPosition: 8, - }, - { - name: 'region', - dataType: DataType.Varchar, - dataLength: 512, - dataTypeDisplay: 'varchar', - description: - // eslint-disable-next-line max-len - 'The name of the region, such as a province or state, where the customer is located. For example, Ontario or New York. This column is the same as CustomerAddress.province in the Admin API.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.region', - tags: [], - ordinalPosition: 9, - }, - { - name: 'zip', - dataType: DataType.Varchar, - dataLength: 10, - dataTypeDisplay: 'varchar', - description: 'The ZIP or postal code. For example, 90210.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.zip', - tags: [], - ordinalPosition: 10, - }, - { - name: 'country', - dataType: DataType.Varchar, - dataLength: 50, - dataTypeDisplay: 'varchar', - description: - 'The full name of the country. For example, Canada.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.country', - tags: [], - ordinalPosition: 11, - }, - { - name: 'phone', - dataType: DataType.Varchar, - dataLength: 15, - dataTypeDisplay: 'varchar', - description: 'The phone number of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.phone', - tags: [], - ordinalPosition: 12, - }, - ], - }} + data={tableClassBase.getDummyData()} permissions={DEFAULT_ENTITY_PERMISSION} type={EntityType.TABLE} onUpdate={async () => noop()}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx index 0fc35e9c0f6c..ec1139505de3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Pipeline/PipelineDetails/PipelineDetails.test.tsx @@ -245,10 +245,6 @@ jest.mock('../../../hoc/LimitWrapper', () => { return jest.fn().mockImplementation(({ children }) =>
{children}
); }); -// jest.mock('../../../utils/PipelineDetailsUtils', () => ({ -// getPipelineDetailPageTabs: jest.fn().mockReturnValue([]), -// })); - jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({ GenericProvider: jest .fn() diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts index affccd7311c6..c13d8ac8a1bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/CustomizeWidgets.constants.ts @@ -15,8 +15,12 @@ import { DetailPageWidgetKeys, GlossaryTermDetailPageWidgetKeys, } from '../enums/CustomizeDetailPage.enum'; +import { EntityType } from '../enums/entity.enum'; +import { TagSource } from '../generated/type/tagLabel'; import i18n from '../utils/i18next/LocalUtil'; +export const TAB_GRID_MAX_COLUMNS = 8; + export type GridSizes = keyof typeof WidgetWidths; export interface CommonWidgetType { fullyQualifiedName: string; @@ -43,7 +47,7 @@ export const TAGS_WIDGET: CommonWidgetType = { export const GLOSSARY_TERMS_WIDGET: CommonWidgetType = { fullyQualifiedName: DetailPageWidgetKeys.GLOSSARY_TERMS, - name: i18n.t('label.glossary-term'), + name: i18n.t('label.glossary-term-plural'), data: { gridSizes: ['small'] }, }; @@ -79,7 +83,7 @@ export const REFERENCES_WIDGET: CommonWidgetType = { export const REVIEWER_WIDGET: CommonWidgetType = { fullyQualifiedName: GlossaryTermDetailPageWidgetKeys.REVIEWER, - name: i18n.t('label.reviewer'), + name: i18n.t('label.reviewer-plural'), data: { gridSizes: ['small'] }, }; @@ -102,3 +106,39 @@ export const DATA_PRODUCTS_WIDGET: CommonWidgetType = { name: i18n.t('label.data-product-plural'), data: { gridSizes: ['small'] }, }; + +export const DUMMY_TAGS_LIST = [ + { + tagFQN: 'BusinessGlossary.Purchase', + source: TagSource.Glossary, + name: 'Purchase', + }, + { + tagFQN: 'Person.BankNumber', + source: TagSource.Glossary, + name: 'BankNumber', + }, + { + tagFQN: 'Hospitality.Guest Type', + source: TagSource.Glossary, + name: 'Guest Type', + }, + { + tagFQN: 'Financial Services', + source: TagSource.Glossary, + name: 'Auto Loan', + }, +]; + +export const DUMMY_OWNER_LIST = [ + { + name: 'Aaron Singh', + type: EntityType.USER, + id: '123', + }, + { + name: 'Engineering', + type: EntityType.TEAM, + id: '123', + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index b3544b9c0440..e8fb8a717676 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -801,6 +801,7 @@ "need-help": "Need Help", "new": "New", "new-password": "New Password", + "new-tab": "New Tab", "new-term": "New Term", "new-test-suite": "New Test Suite", "next": "Next", diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less index 54d77e4ccf2e..2d31f150cb6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less @@ -56,3 +56,7 @@ margin: 16px auto 0 auto; padding: 0; } + +.overflow-scroll { + overflow: scroll; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts index 435c9995e356..dbbcd0b8ad31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass.ts @@ -386,6 +386,133 @@ class CustomizeGlossaryTermPageClassBase { RELATED_TERMS_WIDGET, ]; } + + public getGlossaryChildTerms() { + return [ + { + id: 'ea7c8380-34a9-4ea9-93ea-a812c0e838d6', + name: 'Finance', + displayName: 'Finance', + description: + 'A finance department is the unit of a business responsible for obtaining and handling any monies on behalf of the organization', + fullyQualifiedName: 'Business Department.Finance', + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.9, + updatedAt: 1727894458563, + owners: [], + status: 'Approved', + deleted: false, + mutuallyExclusive: false, + childrenCount: 1, + }, + { + id: 'a8409ff4-b540-4ab0-9332-73f34125651c', + name: 'FOO', + displayName: '', + description: 'VCASCAS', + + fullyQualifiedName: 'Business Department.FOO', + synonyms: [], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.1, + updatedAt: 1724662513442, + updatedBy: 'teddy', + owners: [], + status: 'Approved', + deleted: false, + mutuallyExclusive: false, + childrenCount: 0, + }, + { + id: '5c415db9-0927-4815-b31b-ae8247ea6b0a', + name: 'Human resources', + displayName: 'Human resources', + description: + 'Human resources (HR) is the department in a company that handles all things related to employees.', + + fullyQualifiedName: 'Business Department.Human resources', + synonyms: ['Manpower', 'Human capital'], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.2, + updatedAt: 1701067069097, + owners: [], + status: 'Approved', + deleted: false, + mutuallyExclusive: false, + childrenCount: 0, + }, + { + id: 'e866ee75-711a-4649-968d-3ea889bd75b8', + name: 'Marketing', + displayName: 'Marketing', + description: + 'A marketing department is a division within a business that helps to promote its brand, products and services.', + style: {}, + fullyQualifiedName: 'Business Department.Marketing', + synonyms: ['Sell', 'Retails'], + glossary: { + id: 'dae534b6-f5d1-4fc7-9ddf-0d1ec9df5c7e', + type: 'glossary', + name: 'Business Department', + fullyQualifiedName: 'Business Department', + description: + 'Businesses often have several departments that perform unique functions, allowing them to operate efficiently and successfully.', + displayName: 'Business Department', + deleted: false, + }, + references: [], + version: 0.2, + updatedAt: 1700558309238, + owners: [], + status: 'Rejected', + deleted: false, + mutuallyExclusive: false, + childrenCount: 1, + }, + { + id: '288cfb46-a4c2-45a4-9dc0-321eac165812', + name: 'test_business_term', + displayName: 'Test Business Term', + description: 'this is test_business_term', + fullyQualifiedName: 'Business Department.test_business_term', + version: 0.2, + updatedAt: 1728547870161, + owners: [], + deleted: false, + mutuallyExclusive: false, + }, + ]; + } } const customizeGlossaryTermPageClassBase = diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 2368ffbf1195..ecb748fd8f6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -358,12 +358,6 @@ export const getWidgetsFromKey = ( return topicClassBase.getWidgetsFromKey(widgetConfig); case PageType.StoredProcedure: return storedProcedureClassBase.getWidgetsFromKey(widgetConfig); - // case PageType.GlossaryTerm: - // return customizeGlossaryTermPageClassBase.getWidgetsFromKey(widgetConfig); - // case PageType.Glossary: - // return customizeGlossaryPageClassBase.getWidgetsFromKey(widgetConfig); - // case PageType.LandingPage: - // return customizeMyDataPageClassBase.getWidgetsFromKey(widgetConfig); case PageType.DashboardDataModel: return dashboardDataModelClassBase.getWidgetsFromKey(widgetConfig); case PageType.Container: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 5996009c7f8a..fad87897a051 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -23,7 +23,6 @@ import { OperationPermission } from '../context/PermissionProvider/PermissionPro import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { - Constraint, ConstraintType, DatabaseServiceType, DataType, @@ -220,18 +219,127 @@ class TableClassBase { ], }, columns: [ + { + name: 'address_id', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: 'Unique identifier for the address.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', + tags: [], + ordinalPosition: 1, + }, { name: 'shop_id', - displayName: 'Shop Id Customer', - dataType: DataType.Number, + dataType: DataType.Numeric, dataTypeDisplay: 'numeric', description: - 'Unique identifier for the store. This column is the primary key for this table.', + 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim_shop table.', fullyQualifiedName: - 'sample_data.ecommerce_db.shopify."dim.shop".shop_id', + 'sample_data.ecommerce_db.shopify.dim_address_clean.shop_id', tags: [], - constraint: Constraint.PrimaryKey, - ordinalPosition: 1, + ordinalPosition: 2, + }, + { + name: 'first_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'First name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.first_name', + tags: [], + ordinalPosition: 3, + }, + { + name: 'last_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Last name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.last_name', + tags: [], + ordinalPosition: 4, + }, + { + name: 'address', + dataType: DataType.Varchar, + dataLength: 500, + dataTypeDisplay: 'varchar', + description: 'Clean address test', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address', + tags: [], + ordinalPosition: 5, + }, + { + name: 'company', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: "The name of the customer's business, if one exists.", + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.company', + tags: [], + ordinalPosition: 7, + }, + { + name: 'city', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'The name of the city. For example, Palo Alto.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.city', + tags: [], + ordinalPosition: 8, + }, + { + name: 'region', + dataType: DataType.Varchar, + dataLength: 512, + dataTypeDisplay: 'varchar', + description: + // eslint-disable-next-line max-len + 'The name of the region, such as a province or state, where the customer is located. For example, Ontario or New York. This column is the same as CustomerAddress.province in the Admin API.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.region', + tags: [], + ordinalPosition: 9, + }, + { + name: 'zip', + dataType: DataType.Varchar, + dataLength: 10, + dataTypeDisplay: 'varchar', + description: 'The ZIP or postal code. For example, 90210.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.zip', + tags: [], + ordinalPosition: 10, + }, + { + name: 'country', + dataType: DataType.Varchar, + dataLength: 50, + dataTypeDisplay: 'varchar', + description: 'The full name of the country. For example, Canada.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.country', + tags: [], + ordinalPosition: 11, + }, + { + name: 'phone', + dataType: DataType.Varchar, + dataLength: 15, + dataTypeDisplay: 'varchar', + description: 'The phone number of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.phone', + tags: [], + ordinalPosition: 12, }, ], owners: [ @@ -286,18 +394,6 @@ class TableClassBase { serviceType: DatabaseServiceType.BigQuery, tags: [], followers: [], - changeDescription: { - fieldsAdded: [ - { - name: 'owner', - newValue: - '{"id":"38be030f-f817-4712-bc3b-ff7b9b9b805e","type":"user","name":"aaron_johnson0","fullyQualifiedName":"aaron_johnson0","displayName":"Aaron Johnson","deleted":false}', - }, - ], - fieldsUpdated: [], - fieldsDeleted: [], - previousVersion: 0.1, - }, deleted: false, }; } From 7e581512473e969f2ba00472beab0eaf01fd467b Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:50:53 +0530 Subject: [PATCH 55/63] update translation --- .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/es-es.json | 3 ++- .../ui/src/locale/languages/fr-fr.json | 13 +++++++------ .../ui/src/locale/languages/gl-es.json | 1 + .../ui/src/locale/languages/he-he.json | 5 +++-- .../ui/src/locale/languages/ja-jp.json | 19 ++++++++++--------- .../ui/src/locale/languages/mr-in.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pr-pr.json | 5 +++-- .../ui/src/locale/languages/pt-br.json | 7 ++++--- .../ui/src/locale/languages/pt-pt.json | 7 ++++--- .../ui/src/locale/languages/ru-ru.json | 3 ++- .../ui/src/locale/languages/th-th.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + 14 files changed, 41 insertions(+), 27 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index d131d50b20fe..51e49af41e43 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -801,6 +801,7 @@ "need-help": "Need Help", "new": "Neu", "new-password": "Neues Passwort", + "new-tab": "Neuer Tab", "new-term": "Neuer Begriff", "new-test-suite": "Neue Test-Suite", "next": "Nächster", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 16972c7151da..63ebbed672fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -801,7 +801,8 @@ "need-help": "necesita ayuda", "new": "Nuevo", "new-password": "Nueva Contraseña", - "new-term": "Nuevo Término", + "new-tab": "Nueva pestaña", + "new-term": "Nuevo término", "new-test-suite": "Nueva Suite de Pruebas", "next": "Siguiente", "no": "No", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 986bdf446c3d..abac09a81342 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -801,7 +801,8 @@ "need-help": "Need Help", "new": "Nouveau", "new-password": "Nouveau mot de passe", - "new-term": "Nouveau Terme", + "new-tab": "Nouvel onglet", + "new-term": "Nouveau terme", "new-test-suite": "Nouvel Ensemble de Tests", "next": "Suivant", "no": "Non", @@ -1485,7 +1486,7 @@ "configure-webhook-name-message": "OpenMetadata peut être configuré pour automatiquement envoyer des notifications {{webhookType}} webhooks en utilisant OpenMetadata. Entrez le {{webhookType}} nom du webhook et l'URL du point de terminaison où recevoir la requête HTTP. Utilisez le filtre d'événements pour recevoir seulement les notifications pour certains types de ressources. Filtrez les événements selon leur date de création, mise à jour, ou de suppression. Ajoutez une description pour comprendre l'utilisation du webhook. Vous pouvez utiliser les options avancées pour mettre en place une clé secrète partagée pour vérifier l'événement webhook {{webhookType}} en utilisant une signature HMAC.", "configured-sso-provider-is-not-supported": "Le fournisseur SSO configuré \"{{provider}}\" n'est pas supporté. Veuillez vérifier la configuration d'authentification du serveur.", "confirm-delete-message": "Êtes-vous sûr de vouloir supprimer de manière permanente ce message ?", - "connection-details-description": "Chaque service a son propre ensemble de conditions et voici les bases de ce dont vous avez besoin pour vous connecter. Les conditions de connexion sont générées à partir du schéma JSON pour ce service. Les champs obligatoires sont marqués d’un astérisque.", + "connection-details-description": "Chaque service a son propre ensemble de conditions et voici les bases de ce dont vous avez besoin pour vous connecter. Les conditions de connexion sont générées à partir du schéma JSON pour ce service. Les champs obligatoires sont marqués d'un astérisque.", "connection-test-failed": "Le test de connexion a échoué.", "connection-test-successful": "Le test de connectivité a réussi.", "connection-test-warning": "Le test de connexion a réussi partiellement : Certaines étapes ont échoué, nous n'ingérerons que les métadonnées partielles.", @@ -1566,7 +1567,7 @@ "enables-end-to-end-metadata-management": "Activer la gestion des métadonnées de bout en bout avec la découverte des données, la dualité des données, l'observabilité, et la collaboration", "endpoint-should-be-valid": "Le point de terminaison doit être une URL valide.", "ensure-airflow-set-up-correctly-before-heading-to-ingest-metadata": "Vérifiez que Airflow est correctement configuré avant de passer à l'ingestion de métadonnées.", - "ensure-elasticsearch-is-up-and-running": "Vérifiez que le conteneur Elasticsearch est en cours d’exécution.", + "ensure-elasticsearch-is-up-and-running": "Vérifiez que le conteneur Elasticsearch est en cours d'exécution.", "enter-a-field": "Entrer un {{field}}", "enter-column-description": "Entrer la Description de la Colonne", "enter-comma-separated-field": "Entrer {{field}} séparé par une virgule(,)", @@ -1662,7 +1663,7 @@ "invalid-object-key": "Clé d'objet non valide. Doit commencer par une lettre, un underscore ou signe dollar, suivi de lettres, underscores, signes dollars, ou chiffres.", "invalid-property-name": "Nom de propriété non valide", "invalid-unix-epoch-time-milliseconds": "Invalid Unix epoch time in milliseconds", - "jwt-token": "Le Jeton que vous avez généré peut être utilisé pour accéder à l’API OpenMetadata.", + "jwt-token": "Le Jeton que vous avez généré peut être utilisé pour accéder à l'API OpenMetadata.", "jwt-token-expiry-time-message": "Délai d'expiration du jeton JWT en secondes.", "kill-ingestion-warning": "Une fois que vous avez interrompu cette Ingestion, tous les workflows en cours d'éxécutions et en files d'attente seront arrêtés et marqués comme aillant échoué.", "kpi-subtitle": "Identifier les indicateurs de performance clés (KPIs) qui reflètent le mieux la santé de vos actifs de données.", @@ -1901,7 +1902,7 @@ "source-aligned-domain-type-description": "Domaines qui concentrent les données analytiques reflétant les faits métiers générés par les systèmes opérationnels et appelées produits de données natifs.", "special-character-not-allowed": "Les caractères spéciaux ne sont pas autorisés", "sql-query-tooltip": "Requête avec 1 ligne ou plus entraînera l'échec du test.", - "sso-provider-not-supported": "Le fournisseur SSO {{provider}} n’est pas pris en charge.", + "sso-provider-not-supported": "Le fournisseur SSO {{provider}} n'est pas pris en charge.", "stage-file-location-message": "Nom du fichier temporaire pour stocker les journaux de requête avant le traitement. Chemin de fichier absolu requis.", "star-on-github-description": "Contribuez avec des Etoiles pour aider les passionnés de data à découvrir OpenMetadata!", "still-running-into-issue": "Si vous rencontrez toujours des problèmes, veuillez nous contacter sur slack.", @@ -1928,7 +1929,7 @@ "token-security-description": "Toute personne en possession de votre Jeton JWT pourra envoyer des requêtes à l'API REST d'OpenMetadata. Ne surtout pas rendre public le Jeton JWT dans le code de votre application. Ne le partagez pas sur GitHub ou ailleurs en ligne.", "total-entity-insight": "Montre le nombre le plus récent d'actifs de données par type.", "tour-follow-step": "Suivre un actif de données pour rester informé des modifications qui lui sont apportées. Dans votre flux d'activité, toutes les modifications apportées à l'actif de données que vous suivez sont affichées. Vous recevrez également des alertes si cet actif de données présente des problèmes de qualité des données.", - "tour-high-level-assets-information-step": "Dans la page Détails des actifs de données, vous obtenez une vue à 360 degrés de l’actif de données pour vous aider à comprendre tout le contexte des données et à en tirer le meilleur parti.", + "tour-high-level-assets-information-step": "Dans la page Détails des actifs de données, vous obtenez une vue à 360 degrés de l'actif de données pour vous aider à comprendre tout le contexte des données et à en tirer le meilleur parti.", "tour-owner-step": "Ici vous pouvez attribuer la propriété d'un actif de données à une équipe ou à un individu. Collaborez avec les propriétaires de données - posez des questions pour comprendre les données, demandez des descriptions manquantes ou suggérez même des modifications.", "tour-step-activity-feed": "<0>{{text}} aide à comprendre comment les données évoluent dans votre organisation.", "tour-step-click-on-entity-tab": "Cliquer sur l'onglet <0>\"{{text}}\".", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index 374e746a9c81..013c07063922 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -801,6 +801,7 @@ "need-help": "Necesitas axuda", "new": "Novo", "new-password": "Novo contrasinal", + "new-tab": "Nova pestana", "new-term": "Novo termo", "new-test-suite": "Novo conxunto de probas", "next": "Seguinte", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 5d82cd979298..98a39883974e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -801,7 +801,8 @@ "need-help": "Need Help", "new": "חדש", "new-password": "סיסמה חדשה", - "new-term": "מונח עיסקי חדש", + "new-tab": "כרטיסייה חדשה", + "new-term": "מונח חדש", "new-test-suite": "מערכת בדיקות חדשה", "next": "הבא", "no": "לא", @@ -1973,7 +1974,7 @@ "viewing-older-version": "צפיה בגרסה ישנה \n עבור לאחרונה כדי לעדכן פרטים", "webhook-listing-message": "ה-webhook מאפשר לשירותים חיצוניים לקבל התראות על אירועי שינוי בנתוני המטה-דאטה המתרחשים בארגונך דרך API. רשום כתובות URL עם אינטגרציית webhook כדי לקבל התראות על אירועים במטה-דאטה. אפשר להוסיף, לרשום, לעדכן ולמחוק webhooks.", "webhook-type-listing-message": "ספק עדכונים בזמן אמת ליצרנים ולצרכנים של מטה-דאטה דרך {{webhookType}} התראות. השתמש ב-webhooks של {{webhookType}} כדי לשלוח התראות על אירועי שינוי בנתוני המטה-דאטה בארגונך דרך API. אפשר להוסיף, לרשום, לעדכן ולמחוק את אלה webhooks.", - "welcome-screen-message": "גלה את כל הנתונים שלך במקום אחד ושתף פעולה בצורה חלקה עם הצוות שלך על נתונים שאתה יכול לסמוך עליהם.", + "welcome-screen-message": "גלה את כל הנתונים שלך במקום אחד ושתף פעולה בצורה חלקה עם הצוות שלך על נתונים שאתה יכול לסמוך עליו.", "welcome-to-om": "ברוך הבא ל-OpenMetadata!", "welcome-to-open-metadata": "ברוך הבא ל-OpenMetadata!", "would-like-to-start-adding-some": "רוצה להתחיל להוסיף משהו?", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index afedf5e5129b..f37f740c014e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -801,7 +801,8 @@ "need-help": "Need Help", "new": "新しい", "new-password": "新しいパスワード", - "new-term": "New Term", + "new-tab": "新しいタブ", + "new-term": "新しい用語", "new-test-suite": "新しいテストスイート", "next": "次へ", "no": "いいえ", @@ -1417,7 +1418,7 @@ "add-kpi-message": "Identify the Key Performance Indicators (KPI) that best reflect the health of your data assets. Review your data assets based on Description, Ownership, and Tier. Define your target metrics in absolute or percentage to track your progress. Finally, set a start and end date to achieve your data goals.", "add-new-service-description": "Choose from the range of services that OpenMetadata integrates with. To add a new service, start by selecting a Service Category (Database, Messaging, Dashboard, or Pipeline). From the list of available services, select the one you'd want to integrate with.", "add-policy-message": "Policies are assigned to teams. In OpenMetadata, a policy is a collection of rules, which define access based on certain conditions. We support rich SpEL (Spring Expression Language) based conditions. All the operations supported by an entity are published. Use these fine grained operations to define the conditional rules for each policy. Create well-defined policies based on conditional rules to build rich access control roles.", - "add-query-helper-message": "Add a SQL query to execute in the database. The same query can be added to multiple tables by selecting from the tables in the option ‘Query used in’. Choose to describe your query for future reference.", + "add-query-helper-message": "Add a SQL query to execute in the database. The same query can be added to multiple tables by selecting from the tables in the option 'Query used in'. Choose to describe your query for future reference.", "add-role-message": "Roles are assigned to Users. In OpenMetadata, Roles are a collection of Policies. Each Role must have at least one policy attached to it. A Role supports multiple policies with a one to many relationship. Ensure that the necessary policies are created before creating a new role. Build rich access control roles with well-defined policies based on conditional rules.", "adding-new-asset-to-team": "Click on Add Asset, which will take you to the Explore page. From there, you'll be able to assign a Team as the Owner of the asset.", "adding-new-entity-is-easy-just-give-it-a-spin": "新しい{{entity}}の追加は簡単です。試してみてください!", @@ -1485,7 +1486,7 @@ "configure-webhook-name-message": "OpenMetadata can be configured to automatically send out event notifications to registered {{webhookType}} webhooks through OpenMetadata. Enter the {{webhookType}} webhook name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications for the required entities. Filter events based on when an entity is created, updated, or deleted. Add a description to note the use case of the webhook. You can use advanced configuration to set up a shared secret key to verify the {{webhookType}} webhook events using HMAC signature.", "configured-sso-provider-is-not-supported": "The configured SSO Provider \"{{provider}}\" is not supported. Please check the authentication configuration in the server.", "confirm-delete-message": "このメッセージを恒久的に削除しても良いですか?", - "connection-details-description": "Every service comes with its standard set of requirements and here are the basics of what you’d need to connect. The connection requirements are generated from the JSON schema for that service. The mandatory fields are marked with an asterisk.", + "connection-details-description": "Every service comes with its standard set of requirements and here are the basics of what you'd need to connect. The connection requirements are generated from the JSON schema for that service. The mandatory fields are marked with an asterisk.", "connection-test-failed": "Connection test was failed.", "connection-test-successful": "接続テストが成功しました", "connection-test-warning": "Test connection partially successful: Some steps had failures, we will only ingest partial metadata.", @@ -1595,7 +1596,7 @@ "entity-saved-successfully": "{{entity}} saved successfully", "entity-size-in-between": "{{entity}}のサイズは{{min}}以上{{max}}以下にしてください", "entity-size-must-be-between-2-and-64": "{{entity}}のサイズは2以上64以下", - "entity-transfer-message": "Click on Confirm if you’d like to move <0>{{from}} {{entity}} under <0>{{to}} {{entity}}.", + "entity-transfer-message": "Click on Confirm if you'd like to move <0>{{from}} {{entity}} under <0>{{to}} {{entity}}.", "enum-property-update-message": "列挙型の値の更新が開始されました。完了後に通知されます。", "enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", @@ -1667,8 +1668,8 @@ "kill-ingestion-warning": "Once you kill this Ingestion, all running and queued workflows will be stopped and marked as Failed.", "kpi-subtitle": "Identify the Key Performance Indicators (KPI) that best reflect the health of your data assets.", "kpi-target-achieved": "目標達成おめでとうございます!", - "kpi-target-achieved-before-time": "Awesome! You’ve reached your target well before time.", - "kpi-target-overdue": "Never mind. It’s time to restructure your goals and progress faster.", + "kpi-target-achieved-before-time": "Awesome! You've reached your target well before time.", + "kpi-target-overdue": "Never mind. It's time to restructure your goals and progress faster.", "leave-the-team-team-name": "チーム {{teamName}} から抜ける", "length-validator-error": "At least {{length}} {{field}} required", "lineage-ingestion-description": "Lineage ingestion can be configured and deployed after a metadata ingestion has been set up. The lineage ingestion workflow obtains the query history, parses CREATE, INSERT, MERGE... queries and prepares the lineage between the involved entities. The lineage ingestion can have only one pipeline for a database service. Define the Query Log Duration (in days) and Result Limit to start.", @@ -1701,7 +1702,7 @@ "name-of-the-bucket-dbt-files-stored": "保存されているdbtファイルにあるバケット名", "new-conversation": "新しく会話を始める", "new-to-the-platform": "新規利用者の場合", - "no-access-placeholder": "You don’t have access, please check with the admin to get permissions", + "no-access-placeholder": "You don't have access, please check with the admin to get permissions", "no-activity-feed": "We have not found any activity within the data assets you own or the ones you are currently following.", "no-announcement-message": "お知らせはありません。「お知らせを追加」で追加してください。", "no-asset-available": "アセットはありません。", @@ -1775,7 +1776,7 @@ "no-version-type-available": "No {{type}} version available", "no-widgets-to-add": "No new widgets to add", "nodes-per-layer-message": "Please enter a value for nodes per layer", - "nodes-per-layer-tooltip": "Choose to display ‘n’ number of nodes per layer. If the existing nodes exceed the defined number of nodes, then pagination will be shown.", + "nodes-per-layer-tooltip": "Choose to display 'n' number of nodes per layer. If the existing nodes exceed the defined number of nodes, then pagination will be shown.", "not-followed-anything": "あなたはまだ何もフォローしていません。", "notification-description": "Set up notifications to received real-time updates and timely alerts.", "om-description": "Centralized metadata store, to discover, collaborate and get your data right.", @@ -1889,7 +1890,7 @@ "service-created-entity-description": "The has been created successfully. Visit the newly created service to take a look at the details. {{entity}}", "service-description": "Set up connectors and ingest metadata from diverse sources", "service-name-length": "サービス名は1以上128以下の文字列でなければいけません", - "service-requirements-description": "Every service comes with its standard set of requirements and here are the basics of what you’d need to connect.", + "service-requirements-description": "Every service comes with its standard set of requirements and here are the basics of what you'd need to connect.", "service-with-delimiters-not-allowed": "サービス名に区切り文字は使えません", "service-with-space-not-allowed": "サービス名に空白は使えません", "session-expired": "セッションがタイムアウトしました。OpenMetadataにアクセスするには再度サインインしてください。", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index 05f92fcbe0e4..ff01bb0742c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -801,6 +801,7 @@ "need-help": "मदत हवी आहे", "new": "नवीन", "new-password": "नवीन पासवर्ड", + "new-tab": "New Tab", "new-term": "नवीन संज्ञा", "new-test-suite": "नवीन चाचणी संच", "next": "पुढे", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 5021dac10257..560c1c9c516b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -801,6 +801,7 @@ "need-help": "Need Help", "new": "Nieuw", "new-password": "Nieuw wachtwoord", + "new-tab": "Nieuw tabblad", "new-term": "Nieuwe term", "new-test-suite": "Nieuwe testsuite", "next": "Volgende", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index cb95a5390fad..dd6857c75e27 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -801,6 +801,7 @@ "need-help": "نیاز به کمک", "new": "جدید", "new-password": "رمز عبور جدید", + "new-tab": "برگه جدید", "new-term": "اصطلاح جدید", "new-test-suite": "مجموعه آزمون جدید", "next": "بعدی", @@ -1523,7 +1524,7 @@ "dbt-cloud-type": "در صورت وجود چندین {{type}} در حساب dbt cloud، شناسه {{type}} را که می‌خواهید نتایج اجرای dbt را از آن استخراج کنید، مشخص کنید.", "dbt-ingestion-description": "یک جریان کار dbt می‌تواند بعد از تنظیم یک ورود متادیتا پیکربندی و مستقر شود. چندین پایپ‌لاین dbt می‌توانند برای همان سرویس پایگاه داده تنظیم شوند. پایپ‌لاین، برگه dbt موجودیت جدول را تغذیه می‌کند، از گره‌های dbt خط‌مش ایجاد می‌کند و تست‌های dbt را اضافه می‌کند. برای شروع، پیکربندی منبع فایل‌های dbt را اضافه کنید.", "dbt-manifest-file-path": "مسیر فایل manifest dbt برای استخراج مدل‌های dbt و مرتبط کردن آنها با جداول.", - "dbt-optional-config": "پیکربندی اختیاری برای به‌روزرسانی توصیف از dbt یا نه", + "dbt-optional-config": "پیکربندی اختیاری برای به‌روزرسانی توضیحات از dbt یا نه", "dbt-result-file-path": "مسیر فایل نتایج اجرای dbt برای استخراج اطلاعات نتایج تست.", "dbt-run-result-http-path-message": "dbt نتایج را در یک مسیر http اجرا می‌کند تا نتایج تست را استخراج کند.", "deeply-understand-table-relations-message": "پروducers و مصرف‌کنندگان داده را در یک پلتفرم واحد دریافت کنید و بهره‌وری را تسریع کنید. همکاری با افراد و داده‌ها از ابزارهای مختلف در یک مکان متمرکز بهبود می‌یابد.", @@ -1636,7 +1637,7 @@ "glossary-tag-update-description": "این اقدام تگ را به تمامی دارایی‌های مرتبط با اصطلاح لغت‌نامه به‌روزرسانی می‌کند.", "glossary-tag-update-modal-title-failed": "اعتبارسنجی برای دارایی‌های داده‌ای زیر ناموفق بود.", "glossary-tag-update-modal-title-validating": "اعتبارسنجی دارایی‌های داده‌ای", - "glossary-term-description": "هر اصطلاح در واژه‌نامه دارای یک تعریف منحصر به فرد است. علاوه بر تعریف استاندارد برای یک مفهوم، مترادف‌ها و اصطلاحات مرتبط (مثلاً اصطلاحات والد و فرزند) نیز می‌توانند مشخص شوند. مراجع می‌توانند به دارایی‌های مرتبط با اصطلاحات اضافه شوند. اصطلاحات جدید را می‌توان به واژه‌نامه اضافه یا به‌روزرسانی کرد. اصطلاحات واژه‌نامه می‌توانند توسط کاربران خاص بررسی و پذیرفته یا رد شوند.", + "glossary-term-description": "هر اصطلاح در واژه‌نامه دارای یک تعریف منحصر به فرد است. علاوه بر تعریف استاندارد برای یک مفهوم، مترادف‌ها و اصطلاحات مرتبط (مثلاً اصطلاحات والد و فرزند) نیز می‌توانند مشخص شوند. مراجع می‌توانند به دارایی‌های مرتبط با اصطلاحات اضافه شوند. اصطلاحات جدید می‌توانند به واژه‌نامه اضافه یا به‌روزرسانی کرد. اصطلاحات واژه‌نامه می‌توانند توسط کاربران خاص بررسی و پذیرفته یا رد شوند.", "glossary-term-status": "اصطلاح واژه‌نامه {{status}} شد.", "go-back-to-login-page": "بازگشت به صفحه ورود", "govern-url-size-message": "نسبت ابعاد آیکون باید 1:1 باشد و اندازه پیشنهادی 64 × 64 پیکسل است.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index 98625f41f16c..3efc1bcdd9fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -800,8 +800,9 @@ "navigation": "Navigation", "need-help": "Need Help", "new": "Novo", - "new-password": "Nova Senha", - "new-term": "Novo Termo", + "new-password": "Nova senha", + "new-tab": "Nova aba", + "new-term": "Novo termo", "new-test-suite": "Novo Conjunto de Testes", "next": "Próximo", "no": "Não", @@ -1417,7 +1418,7 @@ "add-kpi-message": "Identifique os Indicadores Chave de Desempenho (KPI) que melhor refletem a saúde dos seus ativos de dados. Revise seus ativos de dados com base em Descrição, Propriedade e Nível. Defina suas métricas-alvo em absoluto ou porcentagem para acompanhar seu progresso. Finalmente, defina uma data de início e término para alcançar seus objetivos de dados.", "add-new-service-description": "Escolha entre a variedade de serviços com os quais o OpenMetadata se integra. Para adicionar um novo serviço, comece selecionando uma Categoria de Serviço (Banco de Dados, Mensagens, Painel ou Pipeline). Da lista de serviços disponíveis, selecione aquele que deseja integrar.", "add-policy-message": "As políticas são atribuídas às equipes. No OpenMetadata, uma política é uma coleção de regras que definem o acesso com base em certas condições. Nós suportamos condições ricas baseadas em SpEL (Spring Expression Language). Todas as operações suportadas por uma entidade são publicadas. Use essas operações detalhadas para definir as regras condicionais para cada política. Crie políticas bem definidas baseadas em regras condicionais para construir funções ricas de controle de acesso.", - "add-query-helper-message": "Adicione uma consulta SQL para ser executada no banco de dados. A mesma consulta pode ser adicionada a várias tabelas selecionando nas tabelas na opção ‘Consulta usada em’. Escolha descrever sua consulta para referência futura.", + "add-query-helper-message": "Adicione uma consulta SQL para ser executada no banco de dados. A mesma consulta pode ser adicionada a várias tabelas selecionando nas tabelas na opção 'Consulta usada em'. Escolha descrever sua consulta para referência futura.", "add-role-message": "As funções são atribuídas aos Usuários. No OpenMetadata, as Funções são uma coleção de Políticas. Cada Função deve ter pelo menos uma política anexada a ela. Uma Função suporta várias políticas com uma relação de um para muitos. Certifique-se de que as políticas necessárias sejam criadas antes de criar uma nova função. Construa funções ricas de controle de acesso com políticas bem definidas baseadas em regras condicionais.", "adding-new-asset-to-team": "Click on Add Asset, which will take you to the Explore page. From there, you'll be able to assign a Team as the Owner of the asset.", "adding-new-entity-is-easy-just-give-it-a-spin": "Adicionar uma nova {{entity}} é fácil, basta dar uma chance!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index e3ceccc31bca..bd91873c7e75 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -800,8 +800,9 @@ "navigation": "Navigation", "need-help": "Need Help", "new": "Novo", - "new-password": "Nova Senha", - "new-term": "Novo Termo", + "new-password": "Nova senha", + "new-tab": "Novo separador", + "new-term": "Novo termo", "new-test-suite": "Novo Conjunto de Testes", "next": "Próximo", "no": "Não", @@ -1417,7 +1418,7 @@ "add-kpi-message": "Identifique os Indicadores Chave de Desempenho (KPI) que melhor refletem a saúde dos seus ativos de dados. Revise seus ativos de dados com base em Descrição, Propriedade e Nível. Defina suas métricas-alvo em absoluto ou percentagem para acompanhar seu progresso. Finalmente, defina uma data de início e término para alcançar seus objetivos de dados.", "add-new-service-description": "Escolha entre a variedade de serviços com os quais o OpenMetadata se integra. Para adicionar um novo serviço, comece selecionando uma Categoria de Serviço (Banco de Dados, Mensagens, Painel ou Pipeline). Da lista de serviços disponíveis, selecione aquele que deseja integrar.", "add-policy-message": "As políticas são atribuídas às equipas. No OpenMetadata, uma política é uma coleção de regras que definem o acesso com base em certas condições. Nós suportamos condições ricas baseadas em SpEL (Spring Expression Language). Todas as operações suportadas por uma entidade são publicadas. Use essas operações detalhadas para definir as regras condicionais para cada política. Crie políticas bem definidas baseadas em regras condicionais para construir funções ricas de controle de acesso.", - "add-query-helper-message": "Adicione uma consulta SQL para ser executada no banco de dados. A mesma consulta pode ser adicionada a várias tabelas selecionando nas tabelas na opção ‘Consulta usada em’. Escolha descrever sua consulta para referência futura.", + "add-query-helper-message": "Adicione uma consulta SQL para ser executada no banco de dados. A mesma consulta pode ser adicionada a várias tabelas selecionando nas tabelas na opção 'Consulta usada em'. Escolha descrever sua consulta para referência futura.", "add-role-message": "As funções são atribuídas aos Utilizadores. No OpenMetadata, as Funções são uma coleção de Políticas. Cada Função deve ter pelo menos uma política anexada a ela. Uma Função suporta várias políticas com uma relação de um para muitos. Certifique-se de que as políticas necessárias sejam criadas antes de criar uma nova função. Construa funções ricas de controle de acesso com políticas bem definidas baseadas em regras condicionais.", "adding-new-asset-to-team": "Click on Add Asset, which will take you to the Explore page. From there, you'll be able to assign a Team as the Owner of the asset.", "adding-new-entity-is-easy-just-give-it-a-spin": "Adicionar uma nova {{entity}} é fácil, basta dar uma chance!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 1604b06e054b..bd6919dc278c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -801,6 +801,7 @@ "need-help": "Need Help", "new": "Новый", "new-password": "Новый пароль", + "new-tab": "Новая вкладка", "new-term": "Новый термин", "new-test-suite": "Новый набор тестов", "next": "Следующий", @@ -1636,7 +1637,7 @@ "glossary-tag-update-description": "This action will update the tag to all Assets linked to the Glossary Term", "glossary-tag-update-modal-title-failed": "Validation failed for the following data assets", "glossary-tag-update-modal-title-validating": "Validating data assets", - "glossary-term-description": "Каждый термин в глоссарии имеет уникальное определение. Наряду с определением стандартного термина для понятия можно указать синонимы, а также связанные термины (например, родительские и дочерние термины). Ссылки могут быть добавлены к объектам данных, связанным с терминами. Новые термины могут быть добавлены или обновлены в Глоссарий. Термины глоссария могут быть просмотрены определенными пользователями, которые могут принять или отклонить термины.", + "glossary-term-description": "Каждый термин в глоссарии имеет уникальное определение. Наряду с определением стандартного термина для понятия можно указать синонимы, а также связанные термины (например, родительские и дочерние термины). Ссылки могут быть добавлены к объектам, связанным с терминами. Новые термины могут быть добавлены или обновлены в Глоссарий. Термины глоссария могут быть просмотрены определенными пользователями, которые могут принять или отклонить термины.", "glossary-term-status": "Glossary Term was {{status}}.", "go-back-to-login-page": "Вернуться на страницу входа", "govern-url-size-message": "Icon aspect ratio should be 1:1 and Recommended size should be 64 x 64 px", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index f9aecc23492f..263fbf7ae7ae 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -801,6 +801,7 @@ "need-help": "ต้องการความช่วยเหลือ", "new": "ใหม่", "new-password": "รหัสผ่านใหม่", + "new-tab": "New Tab", "new-term": "คำศัพท์ใหม่", "new-test-suite": "ชุดทดสอบใหม่", "next": "ถัดไป", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index e7f2271bbe41..7f4cf672aebe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -801,6 +801,7 @@ "need-help": "Need Help", "new": "新", "new-password": "新密码", + "new-tab": "新标签页", "new-term": "新术语", "new-test-suite": "新质控测试", "next": "下一步", From 25516b2cde261b104c0dfc4136c7be542257c848 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:26:02 +0530 Subject: [PATCH 56/63] update as per comments --- .../ActivityFeedTabNew.component.tsx | 3 +- .../CustomizeTabWidget/CustomizeTabWidget.tsx | 2 +- .../CommonWidgets/CommonWidgets.tsx | 16 +++------ .../Settings/Users/UsersNew.component.tsx | 2 -- .../CustomizablePage/CustomizablePage.tsx | 8 +++-- .../CustomizeDetailPage.interface.ts | 35 +++++++++++++++++++ .../CustomizeDetailsPage.tsx} | 20 +++++------ .../CommonWidget/CommonWidgetClassBase.ts | 25 +++++++++++++ .../utils/CustomizePage/CustomizePageUtils.ts | 5 +++ .../resources/ui/src/utils/DomainUtils.tsx | 7 ---- 10 files changed, 88 insertions(+), 35 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/CustomizeDetailsPage/CustomizeDetailPage.interface.ts rename openmetadata-ui/src/main/resources/ui/src/pages/{CustomizeTableDetailPage/CustomizeTableDetailPage.tsx => CustomizeDetailsPage/CustomizeDetailsPage.tsx} (89%) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/CommonWidget/CommonWidgetClassBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTabNew.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTabNew.component.tsx index f95e1b4d3c6f..df9b43ab672d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTabNew.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTabNew.component.tsx @@ -45,6 +45,7 @@ import { import { useAuth } from '../../../hooks/authHooks'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useElementInView } from '../../../hooks/useElementInView'; +import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { getFeedCount } from '../../../rest/feedsAPI'; import { getFeedCounts, Transi18next } from '../../../utils/CommonUtils'; @@ -72,7 +73,6 @@ const componentsVisibility = { }; export const ActivityFeedTabNew = ({ - fqn, owners = [], columns, entityType, @@ -89,6 +89,7 @@ export const ActivityFeedTabNew = ({ const { currentUser } = useApplicationStore(); const { isAdminUser } = useAuth(); const initialRender = useRef(true); + const { fqn } = useFqn(); const [elementRef, isInView] = useElementInView({ ...observerOptions, root: document.querySelector('#center-container'), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx index 9a6ad6b73f28..6a34ead23435 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/CustomizeTabWidget/CustomizeTabWidget.tsx @@ -148,7 +148,7 @@ export const CustomizeTabWidget = () => { tabs: newPanes, } as Page); - onChange(newActiveKey ?? EntityTabs.OVERVIEW); + newActiveKey !== activeKey && onChange(newActiveKey ?? EntityTabs.OVERVIEW); }; const onEdit = ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index 5fa5ecec14a2..bce7aa0d8c2a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isEmpty, noop } from 'lodash'; +import { isEmpty } from 'lodash'; import { EntityTags } from 'Models'; import React, { useMemo } from 'react'; import { @@ -24,8 +24,8 @@ import { Table } from '../../../generated/entity/data/table'; import { EntityReference } from '../../../generated/entity/type'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import commonWidgetClassBase from '../../../utils/CommonWidget/CommonWidgetClassBase'; import { getEntityName } from '../../../utils/EntityUtils'; -import { getWidgetFromKey } from '../../../utils/GlossaryTerm/GlossaryTermUtil'; import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; import { createTagObject } from '../../../utils/TagsUtils'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; @@ -234,22 +234,16 @@ export const CommonWidgets = ({ return />; } - return getWidgetFromKey({ - widgetConfig: widgetConfig, - handleOpenAddWidgetModal: noop, - handlePlaceholderWidgetKey: noop, - handleRemoveWidget: noop, - isEditView: false, - }); + return commonWidgetClassBase.getCommonWidgetsFromConfig(widgetConfig); }, [widgetConfig, descriptionWidget, glossaryWidget, tagsWidget]); return (
+ key={widgetConfig.i}> {widget}
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersNew.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersNew.component.tsx index 6dcd281cca56..acf0c124a3c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersNew.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersNew.component.tsx @@ -180,7 +180,6 @@ const Users = ({ { case PageType.Database: case PageType.Container: case PageType.SearchIndex: + case PageType.Metric: + case PageType.MlModel: + case PageType.APIEndpoint: + case PageType.APICollection: return ( - { @@ -50,14 +53,7 @@ export const CustomizeTableDetailPage = ({ // call the hook to set the direction of the grid layout useGridLayoutDirection(); - const asyncNoop = async () => { - noop(); - }; - if (!currentPageType) { - // eslint-disable-next-line no-console - console.error('currentPageType is not defined'); - return null; } @@ -79,7 +75,9 @@ export const CustomizeTableDetailPage = ({ { + noop(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index 811d1a5c848f..8d7c41f07fc1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -354,13 +354,6 @@ export const getDomainDetailTabs = ({ ), key: EntityTabs.DOCUMENTATION, children: , - // onUpdate(data as Domain)} - // /> - // ), }, ...(!isVersionsView ? [ From 23774fd7be60787ed7687e2082a2e78a395e1c3b Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:28:40 +0530 Subject: [PATCH 57/63] update localization --- .../ui/src/locale/languages/ko-kr.json | 3997 +++++++++-------- 1 file changed, 2063 insertions(+), 1934 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index 60765d638d11..65917bb83e38 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -1,1936 +1,2065 @@ { - "label": { - "aborted": "중단됨", - "accept": "수락", - "accept-all": "전체 수락", - "accept-suggestion": "제안 수락", - "access": "접근", - "access-block-time": "접근 차단 시간", - "access-control": "접근 제어", - "access-token": "접근 토큰", - "accessed": "접근됨", - "account": "계정", - "account-email": "계정 이메일", - "account-name": "계정 이름", - "acknowledged": "인정됨", - "action": "작업", - "action-plural": "작업들", - "active": "활성", - "active-uppercase": "ACTIVE", - "active-user": "활성 사용자", - "active-with-error": "오류와 함께 활성", - "activity": "활동", - "activity-feed": "활동 피드", - "activity-feed-and-task-plural": "활동 피드 및 작업", - "activity-feed-plural": "활동 피드들", - "activity-lowercase": "활동", - "activity-lowercase-plural": "활동들", - "add": "추가", - "add-a-new-service": "새 서비스 추가", - "add-an-image": "이미지 추가", - "add-custom-entity-property": "맞춤형 {{entity}} 속성 추가", - "add-deploy": "추가 및 배포", - "add-entity": "{{entity}} 추가", - "add-entity-metric": "{{entity}} 메트릭 추가", - "add-entity-test": "{{entity}} 테스트 추가", - "add-new-entity": "새로운 {{entity}} 추가", - "add-suggestion": "제안 추가", - "add-workflow-ingestion": "{{workflow}} 수집 추가", - "added": "추가됨", - "added-entity": "추가된 {{entity}}", - "added-lowercase": "추가됨", - "added-yet-lowercase": "아직 추가되지 않음.", - "adding-new-classification": "새 분류 추가 중", - "adding-new-tag": "{{categoryName}}에 새 태그 추가 중", - "address": "주소", - "admin": "관리자", - "admin-plural": "관리자들", - "admin-profile": "관리자 프로필", - "advanced": "고급", - "advanced-config": "고급 구성", - "advanced-configuration": "고급 설정", - "advanced-entity": "고급 {{entity}}", - "advanced-search": "고급 검색", - "aggregate": "집계", - "airflow-config-plural": "airflow 구성들", - "alert": "경고", - "alert-lowercase": "경고", - "alert-lowercase-plural": "경고들", - "alert-plural": "경고들", - "alert-type": "경고 유형", - "algorithm": "알고리즘", - "all": "전체", - "all-activity": "모든 활동", - "all-data-asset-plural": "모든 데이터 자산", - "all-domain-plural": "모든 도메인", - "all-entity": "모든 {{entity}}", - "all-lowercase": "전체", - "all-threads": "모든 스레드", - "and-lowercase": "그리고", - "announcement": "공지", - "announcement-lowercase": "공지", - "announcement-on-entity": "{{entity}}에 대한 공지", - "announcement-plural": "공지사항", - "announcement-title": "공지 제목", - "api-collection": "API 컬렉션", - "api-collection-plural": "API 컬렉션들", - "api-endpoint": "API 엔드포인트", - "api-endpoint-plural": "API 엔드포인트들", - "api-uppercase": "API", - "api-uppercase-plural": "API들", - "app-analytic-plural": "앱 분석들", - "app-lowercase": "앱", - "app-plural": "앱들", - "application": "애플리케이션", - "application-by-developer": "<0>{{dev}}의 {{app}}", - "application-plural": "애플리케이션들", - "applied-advanced-search": "적용된 고급 검색", - "apply": "적용", - "approve": "승인", - "approved": "승인됨", - "april": "4월", - "argument-plural": "인수들", - "as-lowercase": "처럼", - "asset": "자산", - "asset-lowercase": "자산", - "asset-plural": "자산들", - "assigned": "할당됨", - "assigned-entity": "할당된 {{entity}}", - "assigned-to-me": "나에게 할당됨", - "assignee": "담당자", - "assignee-plural": "담당자들", - "assume-role-arn": "Assume Role의 Role Arn", - "assume-role-session-name": "Assume Role의 Role Session Name", - "assume-role-source-identity": "Assume Role의 Source Identity", - "attention": "주의", - "audience": "청중", - "august": "8월", - "auth-config-lowercase-plural": "인증 구성들", - "auth-mechanism": "인증 메커니즘", - "auth-x509-certificate-url": "인증 공급자 x509 인증서 URL", - "auth0": "Auth0", - "authentication-uri": "인증 URI", - "authority": "권한", - "authorize-app": "{{app}} 승인", - "auto-pii-confidence-score": "자동 PII 신뢰도 점수", - "auto-tag-pii-uppercase": "자동 PII 태그", - "automatically-generate": "자동 생성", - "average-session": "평균 세션 시간", - "awaiting-status": "대기 중 상태", - "aws-access-key-id": "AWS 접근 키 ID", - "aws-region": "AWS 리전", - "aws-secret-access-key": "AWS 비밀 접근 키", - "aws-session-token": "AWS 세션 토큰", - "azure": "Azure", - "azure-config-source": "Azure 구성 소스", - "back": "뒤로", - "back-to-login-lowercase": "로그인 페이지로 돌아가기", - "basic-configuration": "기본 구성", - "batch-size": "배치 크기", - "before-number-of-day-plural": "{{numberOfDays}}일 전", - "beta": "베타", - "bot": "봇", - "bot-detail": "봇 상세 정보", - "bot-lowercase": "봇", - "bot-plural": "봇들", - "broker-plural": "브로커들", - "browse": "찾아보기", - "browse-app-plural": "앱 찾아보기", - "browse-csv-file": "CSV 파일 찾아보기", - "by-entity": "{{entity}} 기준", - "by-lowercase": "의", - "ca-certs": "CA 인증서", - "cancel": "취소", - "category": "카테고리", - "category-plural": "카테고리들", - "change-entity": "{{entity}} 변경", - "change-parent-entity": "상위 {{entity}} 변경", - "chart": "차트", - "chart-entity": "{{entity}} 차트", - "chart-plural": "차트들", - "chart-type": "차트 유형", - "check-status": "상태 확인", - "children": "자식", - "children-lowercase": "자식", - "claim-ownership": "소유권 주장", - "classification": "분류", - "classification-lowercase": "분류", - "classification-lowercase-plural": "분류들", - "classification-plural": "분류들", - "clean-up-policy-plural-lowercase": "정리 정책들", - "clear": "명확히", - "clear-entity": "{{entity}} 제거", - "click-here": "여기를 클릭", - "client-email": "클라이언트 이메일", - "client-id": "클라이언트 ID", - "client-secret": "클라이언트 비밀", - "client-x509-certificate-url": "클라이언트 x509 인증서 URL", - "close": "닫기", - "close-with-comment": "댓글과 함께 닫기", - "closed": "닫힘", - "closed-lowercase": "닫힘", - "closed-task-plural": "닫힌 작업들", - "closed-this-task-lowercase": "이 작업 닫음", - "cloud-config-source": "클라우드 구성 소스", - "code": "코드", - "collapse": "접기", - "collapse-all": "전체 접기", - "collection": "컬렉션", - "collection-plural": "컬렉션들", - "color": "색상", - "column": "열", - "column-entity": "{{entity}} 열", - "column-lowercase": "열", - "column-lowercase-plural": "열들", - "column-plural": "열들", - "column-profile": "열 프로파일", - "comment": "댓글", - "comment-lowercase": "댓글", - "complete": "완료", - "completed": "완료됨", - "completed-entity": "{{entity}} 완료됨", - "compute-row-count": "행 수 계산", - "condition": "조건", - "config": "구성", - "configuration": "설정", - "configure": "구성", - "configure-a-service": "서비스 구성", - "configure-dbt-model": "dbt 모델 구성", - "configure-entity": "{{entity}} 구성", - "confirm": "확인", - "confirm-lowercase": "확인", - "confirm-new-password": "새 비밀번호 확인", - "confirm-password": "비밀번호 확인", - "connection": "연결", - "connection-details": "연결 세부 정보", - "connection-entity": "{{entity}} 연결", - "connection-status": "연결 상태", - "connection-timeout": "연결 시간 초과", - "connection-timeout-plural": "연결 시간 초과", - "connector": "커넥터", - "constraint": "제약", - "consumer-aligned": "소비자 맞춤", - "container": "컨테이너", - "container-column": "컨테이너 열", - "container-plural": "컨테이너들", - "conversation": "대화", - "conversation-lowercase": "대화", - "conversation-plural": "대화들", - "copied": "복사됨", - "copy": "복사", - "cost-analysis": "비용 분석", - "count": "수", - "create": "생성", - "create-entity": "{{entity}} 생성", - "create-new-test-suite": "새 테스트 스위트 생성", - "created-a-task-lowercase": "작업 생성됨", - "created-by": "생성자", - "created-by-me": "내가 생성함", - "created-date": "생성 날짜", - "created-lowercase": "생성됨", - "creating-account": "계정 생성 중", - "creating-lowercase": "생성 중", - "credentials-type": "자격 증명 유형", - "criteria": "기준", - "cron": "크론", - "custom": "맞춤", - "custom-attribute-plural": "맞춤 속성들", - "custom-entity": "맞춤 {{entity}}", - "custom-logo": "맞춤 로고", - "custom-logo-configuration": "맞춤 로고 구성", - "custom-metric": "맞춤 메트릭", - "custom-oidc": "CustomOidc", - "custom-property": "맞춤 속성", - "custom-property-plural": "맞춤 속성들", - "custom-range": "맞춤 범위", - "custom-theme": "맞춤 테마", - "customise": "맞춤 설정", - "customize-entity": "{{entity}} 맞춤 설정", - "dag": "Dag", - "dag-view": "DAG 보기", - "daily-active-users-on-the-platform": "플랫폼의 일일 활성 사용자 수", - "dashboard": "대시보드", - "dashboard-data-model-plural": "대시보드 데이터 모델들", - "dashboard-detail-plural-lowercase": "대시보드 상세 정보들", - "dashboard-lowercase": "대시보드", - "dashboard-lowercase-plural": "대시보드들", - "dashboard-name": "대시보드 이름", - "dashboard-plural": "대시보드들", - "data-aggregate": "데이터 집계", - "data-aggregation": "데이터 집계", - "data-asset": "데이터 자산", - "data-asset-name": "데이터 자산 이름", - "data-asset-plural": "데이터 자산들", - "data-asset-plural-with-field": "{{field}}가 있는 데이터 자산들", - "data-asset-type": "데이터 자산 유형", - "data-assets-report": "데이터 자산 보고서", - "data-assets-with-tier-plural": "티어가 있는 데이터 자산들", - "data-collaboration": "데이터 협업", - "data-contract-plural": "데이터 계약들", - "data-count-plural": "데이터 수", - "data-discovery": "데이터 검색", - "data-distribution": "데이터 분포", - "data-entity": "데이터 {{entity}}", - "data-insight": "데이터 인사이트", - "data-insight-active-user-summary": "가장 활동적인 사용자", - "data-insight-chart": "데이터 인사이트 차트", - "data-insight-description-summary-type": "{{type}} 설명이 있는 비율", - "data-insight-ingestion": "데이터 인사이트 수집", - "data-insight-owner-summary-type": "{{type}} 소유자 비율", - "data-insight-plural": "데이터 인사이트들", - "data-insight-report": "데이터 인사이트 보고서", - "data-insight-report-alert": "데이터 인사이트 보고서 경고", - "data-insight-summary": "{{organization}} 한 눈에 본 상태", - "data-insight-tier-summary": "티어별 총 데이터 자산", - "data-insight-top-viewed-entity-summary": "가장 많이 본 데이터 자산", - "data-insight-total-entity-summary": "총 데이터 자산", - "data-model": "데이터 모델", - "data-model-column": "데이터 모델 열", - "data-model-plural": "데이터 모델들", - "data-model-type": "데이터 모델 유형", - "data-observability": "데이터 관측성", - "data-product": "데이터 제품", - "data-product-lowercase": "데이터 제품", - "data-product-plural": "데이터 제품들", - "data-profiler-metrics": "데이터 프로파일러 메트릭", - "data-proportion-plural": "데이터 비율들", - "data-quality": "데이터 품질", - "data-quality-test": "데이터 품질 테스트", - "data-quartile-plural": "데이터 사분위수들", - "data-range": "데이터 범위", - "data-type": "데이터 유형", - "data-volume": "데이터 볼륨", - "database": "데이터베이스", - "database-lowercase": "데이터베이스", - "database-name": "데이터베이스 이름", - "database-plural": "데이터베이스들", - "database-schema": "데이터베이스 스키마", - "database-schema-plural": "데이터베이스 스키마들", - "database-service-name": "데이터베이스 서비스 이름", - "date": "날짜", - "date-and-time": "날짜 & 시간", - "date-filter": "날짜 필터", - "day": "일", - "day-left": "{{day}}일 남음", - "days-change-lowercase": "{{days}}일 변화", - "dbt-bucket-name": "dbt 버킷 이름", - "dbt-catalog-file-path": "dbt 카탈로그 파일 경로", - "dbt-catalog-http-path": "dbt 카탈로그 HTTP 경로", - "dbt-classification-name": "dbt 분류 이름", - "dbt-cloud-account-auth-token": "dbt 클라우드 계정 인증 토큰", - "dbt-cloud-account-id": "dbt 클라우드 계정 ID", - "dbt-cloud-job-id": "dbt 클라우드 작업 ID", - "dbt-cloud-project-id": "dbt 클라우드 프로젝트 ID", - "dbt-cloud-url": "dbt 클라우드 URL", - "dbt-configuration-source": "dbt 구성 소스", - "dbt-configuration-source-type": "dbt 구성 소스 유형", - "dbt-ingestion": "dbt 수집", - "dbt-lowercase": "dbt", - "dbt-manifest-file-path": "dbt 매니페스트 파일 경로", - "dbt-object-prefix": "dbt 객체 접두사", - "dbt-run-result-file-path": "dbt 실행 결과 파일 경로", - "dbt-run-result-http-path": "dbt 실행 결과 HTTP 경로", - "dbt-source": "dbt 소스", - "deactivated": "비활성화됨", - "december": "12월", - "default": "기본", - "default-persona": "기본 페르소나", - "delete": "삭제", - "delete-entity": "{{entity}} 삭제", - "delete-property-name": "{{propertyName}} 속성 삭제", - "delete-tag-classification": "{{isCategory}} 태그 삭제", - "delete-uppercase": "삭제", - "deleted": "삭제됨", - "deleted-entity": "삭제된 {{entity}}", - "deleted-lowercase": "삭제됨", - "deleting-lowercase": "삭제 중", - "deploy": "배포", - "deployed": "배포됨", - "deployed-lowercase": "배포됨", - "deploying-lowercase": "배포 중", - "description": "설명", - "description-kpi": "KPI 설명", - "description-lowercase": "설명", - "description-plural": "설명들", - "destination": "대상", - "detail-plural": "상세 정보들", - "developed-by-developer": "{{developer}}가 개발함", - "disable": "비활성화", - "disable-lowercase": "비활성화", - "disable-tag": "태그 비활성화", - "disabled": "비활성화됨", - "discover": "발견", - "display-name": "표시 이름", - "display-name-lowercase": "표시 이름", - "distinct": "고유", - "doc-plural": "문서들", - "doc-plural-lowercase": "문서들", - "document": "문서", - "documentation": "문서화", - "documentation-lowercase": "문서", - "domain": "도메인", - "domain-lowercase": "도메인", - "domain-lowercase-plural": "도메인들", - "domain-plural": "도메인들", - "domain-type": "도메인 유형", - "down-vote": "다운 보트", - "downstream-depth": "다운스트림 깊이", - "duplicate": "중복", - "duration": "기간", - "dynamic-assertion": "동적 단언", - "edge": "엣지", - "edge-information": "엣지 정보", - "edge-lowercase": "엣지", - "edit": "수정", - "edit-amp-accept-suggestion": "수정 및 제안 수락", - "edit-an-announcement": "공지 수정", - "edit-chart-name": "차트 수정: \"{{name}}\"", - "edit-description-for": "{{entityName}}의 설명 수정", - "edit-entity": "수정 {{entity}}", - "edit-entity-name": "{{entityType}} 수정: \"{{entityName}}\"", - "edit-glossary-display-name": "용어집 표시 이름 수정", - "edit-glossary-name": "용어집 이름 수정", - "edit-workflow-ingestion": "{{workflow}} 수집 수정", - "edited": "수정됨", - "effect": "효과", - "elastic-search-re-index": "Elasticsearch 재인덱스", - "elasticsearch": "Elasticsearch", - "email": "이메일", - "email-configuration": "이메일 구성", - "email-configuration-lowercase": "이메일 구성", - "email-lowercase": "이메일", - "email-plural": "이메일들", - "emailing-entity": "이메일 발송 {{entity}}", - "embed-image": "이미지 삽입", - "embed-link": "링크 삽입", - "enable": "활성화", - "enable-debug-log": "디버그 로그 활성화", - "enable-lowercase": "활성화", - "enable-partition": "파티션 활성화", - "enable-smtp-server": "SMTP 서버 활성화", - "enabled": "활성화됨", - "end-date": "종료 날짜", - "end-date-time-zone": "종료 날짜: ({{timeZone}})", - "end-entity": "{{entity}} 종료", - "endpoint": "엔드포인트", - "endpoint-plural": "엔드포인트들", - "endpoint-url": "엔드포인트 URL", - "endpoint-url-for-aws": "AWS용 엔드포인트 URL", - "enter": "입력", - "enter-entity": "{{entity}} 입력", - "enter-entity-name": "{{entity}} 이름 입력", - "enter-entity-value": "{{entity}} 값 입력", - "enter-field-description": "{{field}} 설명 입력", - "enter-property-value": "속성 값 입력", - "enter-type-password": "{{type}} 비밀번호 입력", - "entity": "엔터티", - "entity-count": "{{entity}} 수", - "entity-detail-plural": "{{entity}} 상세 정보들", - "entity-feed-plural": "엔터티 피드들", - "entity-hyphen-value": "{{entity}} - {{value}}", - "entity-id": "{{entity}} ID", - "entity-id-match": "ID로 일치", - "entity-index": "{{entity}} 인덱스", - "entity-list": "{{entity}} 목록", - "entity-name": "{{entity}} 이름", - "entity-plural": "엔터티들", - "entity-proportion": "{{entity}} 비율", - "entity-record-plural": "{{entity}} 레코드들", - "entity-reference": "엔터티 참조", - "entity-reference-plural": "엔터티 참조들", - "entity-reference-types": "엔터티 참조 유형들", - "entity-service": "{{entity}} 서비스", - "entity-type-plural": "{{entity}} 유형", - "entity-version-detail-plural": "{{entity}} 버전 상세 정보들", - "enum-value-plural": "열거형 값들", - "equation": "방정식", - "error": "오류", - "error-plural": "오류들", - "event-publisher-plural": "이벤트 발행자들", - "event-type": "이벤트 유형", - "event-type-lowercase": "이벤트 유형", - "every": "모든", - "exclude": "제외", - "execution-date": "실행 날짜", - "execution-plural": "실행들", - "execution-time": "실행 시간", - "exit-fit-to-screen": "화면 맞춤 종료", - "exit-version-history": "버전 기록 종료", - "expand": "확장", - "expand-all": "전체 확장", - "expert-lowercase": "전문가", - "expert-plural": "전문가들", - "explore": "탐색", - "explore-asset-plural-with-type": "{{type}} 자산 탐색", - "explore-data": "데이터 탐색", - "explore-now": "지금 탐색", - "export": "내보내기", - "export-entity": "{{entity}} 내보내기", - "extend-open-meta-data": "OpenMetadata 확장", - "extension": "확장", - "external": "외부", - "failed": "실패함", - "failure-comment": "실패 댓글", - "failure-context": "실패 상황", - "failure-plural": "실패들", - "failure-reason": "실패 이유", - "favicon-url": "파비콘 URL", - "feature": "기능", - "feature-lowercase": "기능", - "feature-plural": "기능들", - "feature-plural-used": "사용된 기능들", - "february": "2월", - "feed-filter-plural": "피드 필터들", - "feed-lowercase": "피드", - "feed-plural": "피드들", - "field": "필드", - "field-change": "필드 변경", - "field-entity": "{{field}} {{entity}}", - "field-invalid": "{{field}}이(가) 유효하지 않음", - "field-plural": "필드들", - "field-required": "{{field}}이(가) 필요함", - "field-required-plural": "{{field}}들이 필요함", - "file": "파일", - "filter": "필터", - "filter-pattern": "필터 패턴", - "filter-plural": "필터들", - "filtered": "필터됨", - "filtering-condition": "필터링 조건", - "first": "첫 번째", - "first-lowercase": "첫 번째", - "first-quartile": "첫 사분위", - "fit-to-screen": "화면 맞춤", - "flush-interval-secs": "플러시 간격(초)", - "follow": "팔로우", - "followed-lowercase": "팔로우됨", - "follower-plural": "팔로워들", - "followers-of-entity-name": "{{entityName}}의 팔로워들", - "following": "팔로잉", - "for-lowercase": "위한", - "foreign-key": "외래 키", - "forgot-password": "비밀번호 찾기", - "format": "형식", - "fqn-uppercase": "FQN", - "frequently-joined-column-plural": "자주 조인된 열들", - "frequently-joined-table-plural": "자주 조인된 테이블들", - "friday": "금요일", - "from-lowercase": "부터", - "full-name": "전체 이름", - "full-screen": "전체 화면", - "function": "함수", - "g-chat": "G 채팅", - "gcs-config": "GCS 구성", - "gcs-config-source": "GCS 구성 소스", - "gcs-credential-path": "GCS 자격증명 경로", - "gcs-credential-value": "GCS 자격증명 값들", - "generate": "생성", - "generate-new-token": "새 토큰 생성", - "generated-by": "생성됨", - "get-app-support": "앱 지원 받기", - "glossary": "용어집", - "glossary-lowercase": "용어집", - "glossary-lowercase-plural": "용어집들", - "glossary-name": "용어집 이름", - "glossary-plural": "용어집들", - "glossary-term": "용어", - "glossary-term-lowercase": "용어", - "glossary-term-lowercase-plural": "용어들", - "glossary-term-plural": "용어들", - "go-back": "돌아가기", - "go-to-home-page": "홈페이지로 이동", - "google": "구글", - "google-account-service-type": "Google Cloud 서비스 계정 유형.", - "google-client-id": "Google Cloud 클라이언트 ID.", - "google-cloud-auth-provider": "Google Cloud 인증 공급자 인증서.", - "google-cloud-auth-uri": "Google Cloud 인증 URI.", - "google-cloud-client-certificate-uri": "Google Cloud 클라이언트 인증서 URI.", - "google-cloud-email": "Google Cloud 이메일.", - "google-cloud-private-key": "Google Cloud 개인 키.", - "google-cloud-private-key-id": "Google Cloud 개인 키 ID.", - "google-cloud-project-id": "Google Cloud 프로젝트 ID.", - "google-cloud-token-uri": "Google Cloud 토큰 URI.", - "govern": "거버넌스", - "governance": "거버넌스", - "group": "그룹", - "has-been-action-type-lowercase": "이미 {{actionType}}됨", - "health-check": "헬스 체크", - "help": "도움말", - "here-lowercase": "여기", - "hide": "숨기기", - "hide-deleted-entity": "삭제된 {{entity}} 숨기기", - "history": "히스토리", - "home": "홈", - "hour": "시", - "http-config-source": "HTTP 구성 소스", - "hyper-parameter-plural": "하이퍼 파라미터들", - "icon-url": "아이콘 URL", - "image-repository": "이미지 저장소", - "import": "가져오기", - "import-entity": "{{entity}} 가져오기", - "in-lowercase": "안에", - "in-open-metadata": "OpenMetadata 내에서", - "in-review": "검토 중", - "inactive-announcement-plural": "비활성 공지사항들", - "incident": "인시던트", - "incident-manager": "인시던트 관리자", - "incident-status": "인시던트 상태", - "include": "포함", - "include-entity": "{{entity}} 포함", - "include-owner": "소유자 포함", - "incomplete": "불완전함", - "index-states": "인덱스 상태", - "ingest-sample-data": "샘플 데이터 수집", - "ingestion": "수집", - "ingestion-lowercase": "수집", - "ingestion-pipeline": "수집 파이프라인", - "ingestion-pipeline-name": "수집 파이프라인 이름", - "ingestion-plural": "수집", - "ingestion-workflow-lowercase": "수집 워크플로우", - "inherited-entity": "상속된 {{entity}}", - "inherited-role-plural": "상속된 역할들", - "insert": "삽입", - "insight-plural": "인사이트들", - "install": "설치", - "install-airflow-api": "Airflow 관리 API 설치", - "install-service-connectors": "서비스 커넥터 설치", - "installed": "설치됨", - "installed-lowercase": "설치됨", - "instance-lowercase": "인스턴스", - "integration-plural": "통합", - "inter-quartile-range": "사분위 범위", - "internal": "내부", - "interval": "간격", - "interval-type": "간격 유형", - "interval-unit": "간격 단위", - "invalid-condition": "유효하지 않은 조건", - "invalid-name": "유효하지 않은 이름", - "is-ready-for-preview": "미리보기를 위한 준비 완료", - "issue-plural": "이슈들", - "items-selected-lowercase": "선택된 항목들", - "january": "1월", - "job-lowercase": "작업", - "join": "가입", - "join-entity": "가입 <0>{{entity}}", - "join-team": "팀 가입", - "joinable": "가입 가능", - "json-data": "JSON 데이터", - "july": "7월", - "jump-to-end": "끝으로 이동", - "june": "6월", - "jwt-token-expiry-time": "JWT 토큰 만료 시간", - "jwt-uppercase": "JWT", - "keyword-lowercase-plural": "키워드들", - "kill": "종료", - "kpi-display-name": "KPI 표시 이름", - "kpi-list": "KPI 목록", - "kpi-name": "KPI 이름", - "kpi-title": "핵심 성과 지표 (KPI)", - "kpi-uppercase": "KPI", - "kpi-uppercase-plural": "KPIs", - "landing-page": "랜딩 페이지", - "language": "언어", - "large": "대형", - "last": "마지막", - "last-error": "마지막 오류", - "last-failed-at": "마지막 실패 시간", - "last-lowercase": "마지막", - "last-name-lowercase": "성", - "last-no-of-day-plural": "최근 {{day}}일", - "last-number-of-days": "최근 {{numberOfDays}}일", - "last-run": "마지막 실행", - "last-run-result": "마지막 실행 결과", - "last-updated": "최근 업데이트", - "latest": "최신", - "layer-plural": "레이어들", - "learn-more": "자세히 알아보기", - "learn-more-and-support": "자세히 알아보기 및 지원", - "leave-team": "팀 나가기", - "less": "더 적게", - "less-lowercase": "더 적게", - "line": "선", - "line-plural": "선들", - "lineage": "계보", - "lineage-config": "계보 구성", - "lineage-data-lowercase": "계보 데이터", - "lineage-ingestion": "계보 수집", - "lineage-lowercase": "계보", - "lineage-node-lowercase": "계보 노드", - "lineage-source": "계보 출처", - "link": "링크", - "list": "목록", - "list-entity": "{{entity}} 목록", - "live": "실시간", - "load-more": "더 불러오기", - "loading": "불러오는 중", - "local-config-source": "로컬 구성 소스", - "location": "위치", - "log-plural": "로그들", - "log-viewer": "로그 뷰어", - "logged-in-user-lowercase": "로그인한 사용자", - "login": "로그인", - "login-configuration": "로그인 구성", - "logo-url": "로고 URL", - "logout": "로그아웃", - "machine-learning": "머신 러닝", - "major": "주요", - "manage-entity": "관리 {{entity}}", - "manage-rule": "규칙 관리", - "mandatory": "필수", - "march": "3월", - "mark-all-deleted-table-plural": "모든 삭제된 테이블 표시", - "mark-deleted-entity": "삭제된 {{entity}} 표시", - "mark-deleted-table-plural": "삭제된 테이블 표시", - "markdown-guide": "Markdown 가이드", - "market-place": "마켓플레이스", - "matches": "일치", - "matrix": "매트릭스", - "max": "최대", - "max-login-fail-attempt-plural": "최대 로그인 실패 시도", - "maximum-size-lowercase": "최대 크기", - "may": "5월", - "mean": "평균", - "median": "중앙값", - "medium": "중간", - "member-plural": "멤버들", - "mention-plural": "멘션들", - "message-lowercase": "메시지", - "message-lowercase-plural": "메시지들", - "message-plural-lowercase": "메시지들", - "message-schema": "메시지 스키마", - "messaging": "메시징", - "messaging-lowercase": "메시징", - "messaging-plural": "메시징들", - "metadata": "메타데이터", - "metadata-ingestion": "메타데이터 수집", - "metadata-lowercase": "메타데이터", - "metadata-plural": "메타데이터들", - "metadata-to-es-config-optional": "메타데이터 to ES 구성 (옵션)", - "metapilot": "메타파일럿", - "metapilot-suggested-description": "메타파일럿 제안 설명", - "metric-configuration": "메트릭 구성", - "metric-type": "메트릭 유형", - "metric-value": "메트릭 값", - "metrics-summary": "메트릭 요약", - "middot-symbol": "·", - "min": "최소", - "minor": "사소한", - "minute": "분", - "minute-lowercase": "분", - "minute-plural": "분", - "ml-feature-plural": "ML 기능들", - "ml-model": "ML 모델", - "ml-model-lowercase": "ML 모델", - "ml-model-lowercase-plural": "ML 모델들", - "ml-model-plural": "ML 모델들", - "mode": "모드", - "model": "모델", - "model-name": "모델 이름", - "model-plural": "모델들", - "model-store": "모델 저장소", - "monday": "월요일", - "monogram-url": "모노그램 URL", - "month": "월", - "more": "더 보기", - "more-help": "추가 도움말", - "more-lowercase": "더", - "most-active-user": "가장 활동적인 사용자", - "most-recent-session": "가장 최근 세션", - "move-the-entity": "{{entity}} 이동", - "ms": "밀리초", - "ms-team-plural": "MS 팀들", - "multi-select": "다중 선택", - "mutually-exclusive": "상호 배타적", - "my-data": "내 데이터", - "name": "이름", - "name-lowercase": "이름", - "need-help": "도움 필요", - "new": "새로운", - "new-password": "새 비밀번호", - "new-term": "새 용어", - "new-test-suite": "새 테스트 스위트", - "next": "다음", - "no": "아니오", - "no-comma-cancel": "아니오, 취소", - "no-data-asset-found-for": "{{entity}}에 대한 데이터 자산을 찾을 수 없음", - "no-data-found": "데이터를 찾을 수 없음", - "no-description": "설명이 없음", - "no-diff-available": "차이가 없음", - "no-entity": "해당 {{entity}}이(가) 없음", - "no-entity-selected": "선택된 {{entity}}이(가) 없음", - "no-matching-data-asset": "일치하는 데이터 자산을 찾을 수 없음", - "no-of-test": "테스트 수", - "no-owner": "소유자 없음", - "no-parameter-available": "사용 가능한 파라미터가 없음", - "no-result-found": "결과를 찾을 수 없음.", - "no-reviewer": "검토자 없음", - "no-tags-added": "추가된 태그가 없음", - "nodes-per-layer": "레이어당 노드 수", - "non-partitioned": "비파티션", - "none": "없음", - "not-found-lowercase": "찾을 수 없음", - "not-null": "널이 아님", - "not-used": "사용되지 않음", - "notification": "알림", - "notification-alert": "알림 경고", - "notification-plural": "알림들", - "november": "11월", - "null": "널", - "number": "숫자", - "number-of-object-plural": "객체 수", - "number-of-retries": "재시도 횟수", - "number-of-rows": "행 수", - "number-reply-plural": "{{number}}개의 답글", - "object-plural": "객체들", - "observability": "모니터링", - "observability-alert": "모니터링 경고", - "october": "10월", - "of-lowercase": "의", - "ok": "확인", - "okta": "Okta", - "okta-service-account-email": "Okta 서비스 계정 이메일", - "old": "이전", - "old-password": "이전 비밀번호", - "older-reply-lowercase": "이전 답글", - "older-reply-plural-lowercase": "이전 답글들", - "om-jwt-token": "OpenMetadata JWT 토큰", - "on-demand": "온디맨드", - "on-lowercase": "에", - "one-reply": "답글 1개", - "open": "열기", - "open-lowercase": "열림", - "open-metadata": "OpenMetadata", - "open-metadata-logo": "OpenMetadata 로고", - "open-metadata-updated": "OpenMetadata 업데이트됨!", - "open-metadata-url": "OpenMetadata URL", - "open-task-plural": "열린 작업들", - "operation-plural": "작업들", - "option": "옵션", - "or-lowercase": "또는", - "ordinal-position": "서수 위치", - "org-url": "OrgUrl", - "overview": "개요", - "owned-lowercase": "소유됨", - "owner": "소유자", - "owner-kpi": "소유자 KPI", - "owner-lowercase": "소유자", - "owner-plural": "소유자들", - "page-not-found": "페이지를 찾을 수 없음", - "page-views-by-data-asset-plural": "데이터 자산별 페이지 조회수", - "parameter": "파라미터", - "parameter-plural": "파라미터들", - "parent": "상위", - "parsing-timeout-limit": "쿼리 파싱 시간 초과 제한", - "partition-lowercase-plural": "파티션들", - "partition-plural": "파티션들", - "partitioned": "파티션화됨", - "passed": "통과", - "password": "비밀번호", - "password-lowercase": "비밀번호", - "password-not-match": "비밀번호가 일치하지 않음", - "password-type": "{{type}} 비밀번호", - "path": "경로", - "pause": "일시정지", - "paused-uppercase": "PAUSED", - "pctile-lowercase": "백분위", - "pending-task": "대기 중인 작업", - "pending-task-plural": "대기 중인 작업들", - "percentage": "백분율", - "permanently-delete": "영구 삭제", - "permanently-lowercase": "영구적으로", - "permission-plural": "권한들", - "persona": "페르소나", - "persona-plural": "페르소나들", - "personal-user-data": "개인 사용자 데이터", - "pipe-symbol": "|", - "pipeline": "파이프라인", - "pipeline-detail-plural": "파이프라인 상세 정보들", - "pipeline-detail-plural-lowercase": "파이프라인 상세 정보들", - "pipeline-lowercase": "파이프라인", - "pipeline-lowercase-plural": "파이프라인들", - "pipeline-name": "파이프라인 이름", - "pipeline-plural": "파이프라인들", - "pipeline-state": "파이프라인 상태", - "platform": "플랫폼", - "play": "재생", - "please-enter-value": "{{name}} 값을 입력해 주세요", - "please-password-type-first": "먼저 비밀번호를 입력해 주세요", - "please-select": "선택해 주세요", - "please-select-entity": "{{entity}}을(를) 선택해 주세요", - "plus-count-more": "+ {{count}}개 더", - "plus-symbol": "+", - "policy": "정책", - "policy-lowercase": "정책", - "policy-lowercase-plural": "정책들", - "policy-name": "정책 이름", - "policy-plural": "정책들", - "popularity": "인기도", - "posted-on-lowercase": "게시됨", - "precision": "정밀도", - "preference-plural": "환경 설정들", - "press": "누르세요", - "preview": "미리보기", - "preview-data": "데이터 미리보기", - "previous": "이전", - "pricing": "가격", - "primary-key": "기본 키", - "privacy-policy": "개인정보 보호 정책", - "private-key": "개인 키", - "private-key-id": "개인 키 ID", - "profile": "프로필", - "profile-config": "프로필 구성", - "profile-lowercase": "프로필", - "profile-name": "프로필 이름", - "profile-sample-type": "프로필 샘플 {{type}}", - "profiler": "프로파일러", - "profiler-amp-data-quality": "프로파일러 & 데이터 품질", - "profiler-configuration": "프로파일러 구성", - "profiler-ingestion": "프로파일러 수집", - "profiler-lowercase": "프로파일러", - "profiler-setting-plural": "프로파일러 설정들", - "profiler-timeout-second-plural-label": "타임아웃(초)", - "project": "프로젝트", - "project-id": "프로젝트 ID", - "project-lowercase": "프로젝트", - "property": "속성", - "public-team": "공개 팀", - "quality": "품질", - "query": "쿼리", - "query-log-duration": "쿼리 로그 기간", - "query-lowercase": "쿼리", - "query-lowercase-plural": "쿼리들", - "query-plural": "쿼리들", - "query-used-in": "쿼리 사용 위치", - "range": "범위", - "re-assign": "재할당", - "re-deploy": "재배포", - "re-enter-new-password": "새 비밀번호 재입력", - "re-index-all": "모두 재인덱스", - "re-index-elasticsearch": "Elasticsearch 재인덱스", - "re-verify": "재검증", - "reaction-lowercase-plural": "반응들", - "read-type": "{{type}} 읽기", - "reason": "이유", - "receiver-plural": "수신자들", - "recent-announcement-plural": "최근 공지사항들", - "recent-run-plural": "최근 실행들", - "recent-search-term-plural": "최근 검색어들", - "recent-views": "최근 조회", - "recently-viewed": "최근 조회됨", - "record-plural": "레코드들", - "recreate-index-plural": "인덱스 재생성들", - "reference-plural": "참조들", - "refresh": "새로고침", - "refresh-log": "로그 새로고침", - "regenerate-registration-token": "등록 토큰 재생성", - "region-name": "리전 이름", - "registry": "레지스트리", - "reject": "거부", - "reject-all": "모두 거부", - "rejected": "거부됨", - "related-term-plural": "관련 용어들", - "relevance": "관련성", - "remove": "제거", - "remove-entity": "{{entity}} 제거", - "remove-entity-lowercase": "remove {{entity}}", - "remove-lowercase": "제거", - "removed": "제거됨", - "removed-entity": "제거된 {{entity}}", - "removing-user": "사용자 제거 중", - "rename": "이름 변경", - "rename-entity": "{{entity}} 이름 변경", - "replication-factor": "복제 계수", - "reply": "답글", - "reply-in-conversation": "대화 내에서 답글", - "reply-lowercase": "답글", - "reply-lowercase-plural": "답글들", - "request": "요청", - "request-method": "요청 메서드", - "request-schema-field": "요청 스키마 필드", - "request-tag-plural": "요청 태그들", - "requirement-plural": "요구 사항들", - "reset": "재설정", - "reset-default-layout": "기본 레이아웃 재설정", - "reset-your-password": "비밀번호 재설정", - "resolution": "해결", - "resolve": "해결", - "resolved": "해결됨", - "resolved-by": "해결자", - "resource-permission-lowercase": "리소스 권한", - "resource-plural": "리소스들", - "response": "응답", - "response-schema-field": "응답 스키마 필드", - "restore": "복구", - "restore-entity": "{{entity}} 복구", - "restored-lowercase": "복구됨", - "result-limit": "결과 제한", - "result-plural": "결과들", - "resume": "재개", - "retention-period": "보존 기간", - "retention-size": "보존 크기", - "retention-size-lowercase": "보존 크기", - "return": "반환", - "reviewer": "검토자", - "reviewer-plural": "검토자들", - "revoke-token": "토큰 취소", - "role": "역할", - "role-lowercase": "역할", - "role-lowercase-plural": "역할들", - "role-name": "역할 이름", - "role-plural": "역할들", - "row": "행", - "row-count-lowercase": "행 수", - "row-plural": "행들", - "rtl-ltr-direction": "RTL/LTR 방향", - "rule": "규칙", - "rule-effect": "규칙 효과", - "rule-lowercase": "규칙", - "rule-lowercase-plural": "규칙들", - "rule-name": "규칙 이름", - "rule-plural": "규칙들", - "run": "실행", - "run-at": "실행 시간", - "run-now": "지금 실행", - "run-type": "실행 유형", - "running": "실행 중", - "runs-for": "대상 실행", - "s3-config-source": "S3 구성 소스", - "sample": "샘플", - "sample-data": "샘플 데이터", - "sample-data-count": "샘플 데이터 수", - "sample-data-count-lowercase": "샘플 데이터 수", - "saturday": "토요일", - "save": "저장", - "scale": "크기 조정", - "schedule": "일정", - "schedule-for-entity": "{{entity}}용 스케줄러", - "schedule-info": "스케줄 정보", - "schedule-interval": "스케줄 간격", - "schedule-to-run-every": "매번 실행될 스케줄", - "schedule-type": "스케줄 유형", - "schema": "스키마", - "schema-definition": "스키마 정의", - "schema-field": "스키마 필드", - "schema-field-plural": "스키마 필드들", - "schema-name": "스키마 이름", - "schema-plural": "스키마들", - "schema-text": "스키마 텍스트", - "scope-plural": "스코프들", - "search": "검색", - "search-by-type": "{{type}}로 검색", - "search-entity": "{{entity}} 검색", - "search-for-type": "{{type}} 검색", - "search-index": "검색 인덱스", - "search-index-ingestion": "검색 인덱스 수집", - "search-index-plural": "검색 인덱스들", - "search-index-setting-plural": "검색 인덱스 설정들", - "second-plural": "초", - "seconds": "초", - "secret-key": "비밀 키", - "select": "선택", - "select-a-chart": "차트 선택", - "select-a-metric-type": "메트릭 유형 선택", - "select-a-policy": "정책 선택", - "select-add-test-suite": "테스트 스위트 선택/추가", - "select-all-entity": "모든 {{entity}} 선택", - "select-column-plural-to-exclude": "제외할 열들 선택", - "select-column-plural-to-include": "포함할 열들 선택", - "select-entity": "{{entity}} 선택", - "select-field": "{{field}} 선택", - "select-to-search": "검색할 항목 선택", - "select-type": "유형 선택", - "selected-entity": "선택된 {{entity}}", - "selected-lowercase": "선택됨", - "send": "전송", - "send-now": "지금 전송", - "send-to": "전송 대상", - "sender-email": "발신자 이메일", - "september": "9월", - "server": "서버", - "server-endpoint": "서버 엔드포인트", - "server-port": "서버 포트", - "service": "서비스", - "service-account-email": "서비스 계정 이메일", - "service-configuration-lowercase": "서비스 구성", - "service-created-successfully": "서비스가 성공적으로 생성됨", - "service-detail-lowercase-plural": "서비스 상세 정보들", - "service-lowercase": "서비스", - "service-lowercase-plural": "서비스들", - "service-name": "서비스 이름", - "service-plural": "서비스들", - "service-sso": "{{serviceType}} SSO", - "service-type": "서비스 유형", - "session-plural": "세션들", - "setting-plural": "설정들", - "setup-guide": "설정 가이드", - "severity": "심각도", - "shift": "변경", - "show": "보이기", - "show-deleted": "삭제된 항목 보이기", - "show-deleted-entity": "삭제된 {{entity}} 보이기", - "show-less": "더 적게 보이기", - "show-log-plural": "로그 보이기", - "show-more-entity": "{{entity}} 더 보기", - "show-or-hide-advanced-config": "{{showAdv}} 고급 구성", - "sign-in-with-sso": "{{sso}}로 로그인", - "size": "크기", - "size-evolution-graph": "크기 변화 그래프", - "skew": "스큐", - "skipped": "건너뜀", - "slack": "슬랙", - "slack-support": "슬랙 지원", - "slash-symbol": "/", - "small": "소형", - "soft-delete": "소프트 삭제", - "soft-deleted-lowercase": "소프트 삭제됨", - "soft-lowercase": "소프트", - "source": "출처", - "source-aligned": "출처 맞춤", - "source-column": "출처 열", - "source-match": "이벤트 출처로 일치", - "source-plural": "출처들", - "source-url": "출처 URL", - "specific-data-asset-plural": "특정 데이터 자산들", - "sql-uppercase": "SQL", - "sql-uppercase-query": "SQL 쿼리", - "sso-uppercase": "SSO", - "stage-file-location": "스테이지 파일 위치", - "star": "별", - "star-open-metadata": "OpenMetadata에 별 표시", - "star-us-on-github": "GitHub에서 별 주세요", - "start-date-time-zone": "시작 날짜: ({{timeZone}})", - "start-elasticsearch-docker": "Elasticsearch Docker 시작", - "start-entity": "{{entity}} 시작", - "started": "시작됨", - "started-following": "팔로우 시작", - "status": "상태", - "stay-up-to-date": "최신 정보 유지", - "step": "단계", - "stop-re-index-all": "모두 재인덱스 중지", - "stopped": "중지됨", - "storage": "스토리지", - "storage-plural": "스토리지들", - "stored-procedure": "저장 프로시저", - "stored-procedure-plural": "저장 프로시저들", - "style": "스타일", - "sub-domain": "서브 도메인", - "sub-domain-lowercase": "서브 도메인", - "sub-domain-lowercase-plural": "서브 도메인들", - "sub-domain-plural": "서브 도메인들", - "sub-team-plural": "서브 팀들", - "submit": "제출", - "subscription": "구독", - "success": "성공", - "successfully-lowercase": "성공적으로", - "successfully-uploaded": "성공적으로 업로드됨", - "suggest": "제안", - "suggest-entity": "{{entity}} 제안", - "suggested-by": "제안자", - "suggested-description": "제안된 설명", - "suggested-description-plural": "제안된 설명들", - "suggestion": "제안", - "suggestion-lowercase-plural": "제안들", - "suggestion-pending": "제안 대기 중", - "suite": "스위트", - "sum": "합계", - "summary": "요약", - "sunday": "일요일", - "support": "지원", - "support-url": "지원 URL", - "supported-language-plural": "지원 언어들", - "synonym-lowercase-plural": "동의어들", - "synonym-plural": "동의어들", - "table": "테이블", - "table-constraint-plural": "테이블 제약들", - "table-constraints": "테이블 제약들", - "table-entity-text": "테이블 {{entityText}}", - "table-lowercase": "테이블", - "table-lowercase-plural": "테이블들", - "table-partition-plural": "테이블 파티션들", - "table-plural": "테이블들", - "table-profile": "테이블 프로파일", - "table-tests-summary": "테이블 테스트 요약", - "table-type": "테이블 유형", - "table-update-plural": "테이블 업데이트들", - "tag": "태그", - "tag-category-lowercase": "태그 카테고리", - "tag-lowercase": "태그", - "tag-lowercase-plural": "태그들", - "tag-plural": "태그들", - "target": "대상", - "target-column": "대상 열", - "task": "작업", - "task-entity": "{{entity}} 작업", - "task-lowercase": "작업", - "task-plural": "작업들", - "task-title": "작업 #{{title}}", - "team": "팀", - "team-asset-plural": "팀 자산들", - "team-lowercase": "팀", - "team-plural": "팀들", - "team-plural-lowercase": "팀들", - "team-type": "팀 유형", - "team-user-management": "팀 및 사용자 관리", - "tenant-id": "테넌트 ID", - "term": "용어", - "term-lowercase": "용어", - "term-plural": "용어들", - "test": "테스트", - "test-case": "테스트 케이스", - "test-case-lowercase": "테스트 케이스", - "test-case-lowercase-plural": "테스트 케이스들", - "test-case-name": "테스트 케이스 이름", - "test-case-plural": "테스트 케이스들", - "test-case-result": "테스트 케이스 결과", - "test-email": "테스트 이메일", - "test-email-connection": "이메일 연결 테스트", - "test-entity": "{{entity}} 테스트", - "test-plural": "테스트들", - "test-suite": "테스트 스위트", - "test-suite-ingestion": "테스트 스위트 수집", - "test-suite-lowercase": "테스트 스위트", - "test-suite-lowercase-plural": "테스트 스위트들", - "test-suite-plural": "테스트 스위트들", - "test-suite-status": "테스트 스위트 상태", - "test-suite-summary": "테스트 스위트 요약", - "test-type": "테스트 유형", - "testing-connection": "연결 테스트 중", - "tests-summary": "테스트 요약", - "text": "텍스트", - "theme": "테마", - "third-quartile": "세 번째 사분위", - "thread": "스레드", - "thread-plural-lowercase": "스레드들", - "three-dash-symbol": "---", - "three-dots-symbol": "•••", - "thursday": "목요일", - "tier": "티어", - "tier-number": "티어{{tier}}", - "tier-plural-lowercase": "티어들", - "time": "시간", - "timeout": "타임아웃", - "timezone": "타임존", - "title": "제목", - "to-lowercase": "에게", - "token-end-point": "토큰 엔드포인트", - "token-expiration": "토큰 만료", - "token-expired": "토큰 만료됨", - "token-security": "토큰 보안", - "token-uri": "토큰 URI", - "topic": "토픽", - "topic-lowercase": "토픽", - "topic-lowercase-plural": "토픽들", - "topic-name": "토픽 이름", - "topic-plural": "토픽들", - "total": "총계", - "total-entity": "총 {{entity}}", - "total-index-sent": "총 전송된 인덱스", - "tour": "투어", - "tracking": "추적", - "transportation-strategy": "운송 전략", - "tree": "트리", - "trigger": "트리거", - "trigger-type": "트리거 유형", - "triggered-lowercase": "트리거됨", - "triggering-lowercase": "트리거 중", - "try-again": "다시 시도", - "tuesday": "화요일", - "type": "유형", - "type-entities": "{{type}} 엔터티들", - "type-filed-name": "{{fieldName}} 유형", - "type-lowercase": "유형", - "type-to-confirm": "확인을 위해 <0>{{text}} 입력", - "un-follow": "언팔로우", - "uninstall": "제거", - "uninstall-lowercase": "제거", - "uninstalled-lowercase": "제거됨", - "unique": "고유", - "unpause": "일시정지 해제", - "up-vote": "업 보트", - "update": "업데이트", - "update-description": "설명 업데이트", - "update-entity": "{{entity}} 업데이트", - "update-image": "이미지 업데이트", - "update-request-tag-plural": "요청 태그 업데이트들", - "updated": "업데이트됨", - "updated-by": "업데이트한 사람", - "updated-lowercase": "업데이트됨", - "updated-on": "업데이트 시간", - "updating-lowercase": "업데이트 중", - "upload": "업로드", - "upload-csv-uppercase-file": "CSV 파일 업로드", - "upload-image": "이미지 업로드", - "upstream-depth": "업스트림 깊이", - "url-lowercase": "url", - "url-uppercase": "URL", - "usage": "사용", - "usage-ingestion": "사용 수집", - "usage-lowercase": "사용", - "use-aws-credential-plural": "AWS 자격증명 사용", - "use-fqn-for-filtering": "FQN 사용하여 필터링", - "use-ssl-uppercase": "SSL 사용", - "used-by": "사용됨", - "user": "사용자", - "user-account": "사용자 계정", - "user-analytics-report": "사용자 분석 보고서", - "user-lowercase": "사용자", - "user-permission-plural": "사용자 권한들", - "user-plural": "사용자들", - "username": "사용자명", - "username-or-email": "사용자명 또는 이메일", - "valid-condition": "유효한 조건", - "validating-condition": "조건 검사 중...", - "validation-error-plural": "유효성 검사 오류들!", - "value": "값", - "value-count": "값 수", - "value-plural": "값들", - "verify-cert-plural": "인증서 검증", - "version": "버전", - "version-plural": "버전들", - "version-plural-history": "버전 기록들", - "view": "보기", - "view-all": "전체 보기", - "view-definition": "정의 보기", - "view-entity": "{{entity}} 보기", - "view-more": "더 보기", - "view-new-count": "{{count}}개 새 항목 보기", - "view-parsing-timeout-limit": "정의 파싱 타임아웃 제한 보기", - "view-plural": "보기들", - "visit-developer-website": "개발자 웹사이트 방문", - "volume-change": "볼륨 변화", - "wants-to-access-your-account": "{{username}} 계정 접근을 원함", - "warning": "경고", - "warning-plural": "경고들", - "web-analytics-report": "웹 분석 보고서", - "webhook": "웹훅", - "webhook-display-text": "웹훅 {{displayText}}", - "wednesday": "수요일", - "week": "주", - "weekly-usage": "주간 사용량", - "whats-new": "새 소식", - "whats-new-version": "새 소식 ({{version}})", - "widget": "위젯", - "widget-lowercase": "위젯", - "workflow-plural": "워크플로우들", - "yes": "예", - "yes-comma-confirm": "예, 확인", - "yesterday": "어제", - "your-entity": "당신의 {{entity}}" - }, - "message": { - "access-block-time-message": "최대 로그인 실패 시도 후 몇 밀리초 동안 접근이 차단됩니다.", - "access-control-description": "조직의 계층 구조에 맞게 역할과 정책을 사용하여 팀 접근을 구성하세요.", - "access-to-collaborate": "누구나 팀에 참여하고 데이터를 열람하며 협업할 수 있도록 접근을 허용하세요.", - "action-has-been-done-but-deploy-successfully": " {{action}}되었으며 성공적으로 배포되었습니다", - "action-has-been-done-but-failed-to-deploy": " {{action}}되었으나 배포에 실패했습니다", - "active-users": "활성 사용자 수를 표시합니다.", - "add-data-asset-domain": "{{domain}}에 서비스나 데이터 자산을 추가하여 시작하세요.", - "add-kpi-message": "데이터 자산의 건강 상태를 가장 잘 반영하는 핵심 성과 지표(KPI)를 파악하세요. 데이터 자산을 설명, 소유권, 티어 기준으로 검토하고, 목표 메트릭을 절대값이나 백분율로 정의하여 진행 상황을 추적하세요. 마지막으로 데이터 목표 달성을 위한 시작일과 종료일을 설정하세요.", - "add-new-service-description": "OpenMetadata가 통합하는 다양한 서비스 중에서 선택하세요. 새 서비스를 추가하려면 먼저 서비스 카테고리(데이터베이스, 메시징, 대시보드 또는 파이프라인)를 선택한 후, 사용 가능한 서비스 목록에서 원하는 서비스를 선택하세요.", - "add-policy-message": "정책은 팀에 할당됩니다. OpenMetadata에서 정책은 특정 조건에 따라 접근을 정의하는 규칙 모음입니다. 우리는 풍부한 SpEL(Spring Expression Language) 기반 조건을 지원합니다. 엔터티에서 지원되는 모든 작업이 게시되므로, 이 세분화된 작업들을 사용하여 각 정책의 조건부 규칙을 정의하고, 이를 통해 강력한 접근 제어 역할을 구축하세요.", - "add-query-helper-message": "데이터베이스에서 실행할 SQL 쿼리를 추가하세요. 동일한 쿼리는 '쿼리 사용 위치' 옵션에서 여러 테이블에 추가할 수 있습니다. 향후 참고할 수 있도록 쿼리에 대한 설명도 추가하세요.", - "add-role-message": "역할은 사용자에게 할당됩니다. OpenMetadata에서 역할은 정책의 집합이며, 각 역할에는 최소 하나의 정책이 연결되어야 합니다. 역할은 일대다 관계로 여러 정책을 지원하므로, 새 역할 생성 전에 필요한 정책들이 미리 생성되어 있는지 확인하고, 조건부 규칙에 기반한 잘 정의된 정책으로 강력한 접근 제어 역할을 구축하세요.", - "adding-new-asset-to-team": "자산 추가 버튼을 클릭하면 탐색 페이지로 이동하며, 그곳에서 자산의 소유자로 팀을 할당할 수 있습니다.", - "adding-new-entity-is-easy-just-give-it-a-spin": "새로운 {{entity}} 추가는 쉽습니다. 한번 시도해 보세요!", - "adding-new-tag": "{{categoryName}}에 새 태그를 추가 중입니다.", - "adding-new-user-to-entity": "{{entity}}에 새로운 사용자를 추가 중입니다.", - "admin-only-action": "이 작업은 관리자만 수행할 수 있습니다.", - "advanced-search-message": "and/or 조건을 사용한 구문 편집기로 올바른 데이터 자산을 빠르게 찾아보세요.", - "aggregate-domain-type-description": "이벤트와 트랜잭션 데이터를 포함하는 온라인 서비스 및 트랜잭션 데이터에 가까운 도메인입니다.", - "airflow-guide-message": "OpenMetadata는 수집 커넥터 실행을 위해 Airflow를 사용합니다. 우리는 수집 커넥터 배포를 위한 관리 API를 개발했습니다. OpenMetadata Airflow 인스턴스를 사용하거나, 아래 가이드를 참고하여 Airflow에 관리 API를 설치하세요.", - "airflow-host-ip-address": "OpenMetadata는 IP <0>{{hostIp}}에서 귀하의 리소스에 연결합니다. 네트워크 보안 설정에서 인바운드 트래픽을 허용했는지 확인하세요.", - "alerts-description": "웹훅을 사용하여 시기적절한 알림으로 최신 상태를 유지하세요.", - "alerts-destination-description": "Slack, MS Teams, 이메일 또는 웹훅으로 알림을 전송하세요.", - "alerts-filter-description": "알림의 범위를 좁히기 위해 변경 이벤트를 지정하세요.", - "alerts-source-description": "경고를 활성화할 출처를 지정하세요.", - "alerts-trigger-description": "'스키마 변경' 또는 '테스트 실패'와 같은 중요한 트리거 이벤트를 선택하여 알림을 생성하세요.", - "all-charts-are-mapped": "모든 차트가 기존 KPI와 매핑되었습니다.", - "already-a-user": "이미 사용자이신가요?", - "and-followed-owned-by-name": "그리고 당신이 팔로우 중인 팀은 {{userName}}이(가) 소유합니다.", - "announcement-action-description": "다가오는 유지보수, 업데이트 및 삭제에 대해 팀에 알리기 위해 배너를 설정하세요.", - "announcement-created-successfully": "공지사항이 성공적으로 생성되었습니다!", - "announcement-invalid-start-time": "공지 시작 시간은 종료 시간보다 앞서야 합니다.", - "app-already-installed": "애플리케이션이 이미 설치되었습니다", - "app-disabled-successfully": "애플리케이션이 성공적으로 비활성화되었습니다", - "app-installed-successfully": "애플리케이션이 성공적으로 설치되었습니다", - "app-uninstalled-successfully": "애플리케이션이 성공적으로 제거되었습니다", - "appearance-configuration-message": "회사 로고, 모노그램, 파비콘 및 브랜드 색상으로 OpenMetadata를 맞춤 설정하세요.", - "application-action-successfully": "애플리케이션이 {{action}}되었습니다.", - "application-disabled-message": "애플리케이션이 현재 비활성화되어 있습니다. 헤더의 점 3개 메뉴를 클릭하여 활성화하세요.", - "application-to-improve-data": "MetaPilot, Data Insights, 검색 인덱싱 애플리케이션을 사용하여 데이터를 개선하세요.", - "are-you-sure": "정말 확실합니까?", - "are-you-sure-action-property": "정말로 {{propertyName}}을(를) {{action}}하시겠습니까?", - "are-you-sure-delete-entity": "정말로 {{entity}} 속성을 삭제하시겠습니까?", - "are-you-sure-delete-property": "정말로 {{propertyName}} 속성을 삭제하시겠습니까?", - "are-you-sure-delete-tag": "정말로 {{type}} \"{{tagName}}\"을(를) 삭제하시겠습니까?", - "are-you-sure-to-revoke-access": "JWT 토큰의 접근 권한을 취소하시겠습니까?", - "are-you-sure-to-revoke-access-personal-access": "개인 접근 토큰의 접근 권한을 취소하시겠습니까?", - "are-you-sure-want-to-enable": "정말로 {{entity}}을(를) 활성화하시겠습니까?", - "are-you-sure-want-to-text": "정말로 {{text}}하시겠습니까?", - "are-you-sure-you-want-to-remove-child-from-parent": "정말로 {{parent}}에서 {{child}}를 제거하시겠습니까?", - "are-you-want-to-restore": "정말로 {{entity}}을(를) 복구하시겠습니까?", - "assess-data-reliability-with-data-profiler-lineage": "올바른 데이터 거버넌스 접근을 통해 데이터 기반 세상에서 경쟁 우위를 확보하세요. 데이터를 안전하게 보호하고, 비즈니스 혁신과 성장을 지원하세요.", - "assigned-you-a-new-task-lowercase": "새로운 작업이 귀하에게 할당되었습니다.", - "assigning-team-entity-description": "{{name}}에 {{entity}}을(를) 추가하세요. 이 {{entity}}은(는) {{name}} 팀 및 그 하위 팀의 모든 사용자에게 상속됩니다.", - "at-least-one-policy": "최소 하나의 정책을 입력하세요.", - "auth-configuration-missing": "인증 구성이 누락되었습니다.", - "authProvider-is-not-basic": "AuthProvider가 Basic이 아닙니다.", - "bot-email-confirmation": "{{botName}} 봇의 {{email}} 확인", - "can-not-add-widget": "크기 제한으로 인해 이 섹션에 위젯을 추가할 수 없습니다.", - "can-you-add-a-description": "설명을 추가해 주실 수 있나요?", - "checkout-service-connectors-doc": "여기에는 서비스 데이터를 인덱싱하기 위한 다양한 커넥터들이 있습니다. 커넥터를 확인해 보세요.", - "click-text-to-view-details": "<0>{{text}}를 클릭하여 세부 정보를 확인하세요.", - "closed-this-task": "이 작업을 닫았습니다.", - "collaborate-with-other-user": "다른 사용자와 협업하기 위해", - "compute-row-count-helper-text": "테스트 케이스의 통과 및 실패한 행 수를 계산하세요.", - "confidence-percentage-message": "NLP 모델이 열에 PII 데이터가 포함되었는지 판단할 때 사용할 신뢰 수준을 설정하세요.", - "configure-a-service-description": "고유한 서비스 이름을 입력하세요. 서비스 이름은 동일 카테고리 내에서 유일해야 합니다. 예를 들어, 데이터베이스 서비스에서는 MySQL과 Snowflake가 동일한 이름(예: customer_data)을 사용할 수 없지만, 다른 서비스 카테고리(대시보드, 파이프라인)에서는 동일한 이름을 사용할 수 있습니다. 서비스 이름에는 공백이 허용되지 않으며, '-'와 '_'는 사용 가능합니다. 또한 설명을 추가하세요.", - "configure-airflow": "UI를 통해 메타데이터 추출을 설정하려면 먼저 Airflow를 구성하고 연결해야 합니다. 자세한 내용은 <0>{{text}}를 참조하세요.", - "configure-dbt-model-description": "dbt 모델은 원시 데이터로부터 테이블을 생성하는 변환 로직을 제공합니다. 계보는 테이블 간 데이터 경로를 추적하지만, dbt 모델은 구체적인 정보를 제공합니다. 필요한 dbt 소스 공급자를 선택하고 필수 필드를 채우세요. OpenMetadata와 dbt를 통합하여 테이블 생성에 사용된 모델을 확인하세요.", - "configure-glossary-term-description": "용어집의 모든 용어는 고유한 정의를 가집니다. 개념의 표준 용어를 정의하는 것 외에도, 동의어 및 관련 용어(예: 상위 및 하위 용어)를 지정할 수 있습니다. 용어와 관련된 자산에 대한 참조도 추가할 수 있으며, 새 용어를 추가하거나 기존 용어를 업데이트할 수 있습니다. 일부 사용자가 용어를 검토하여 승인하거나 거부할 수 있습니다.", - "configure-search-re-index": "<0>{{settings}}로 이동하여 지금 실행 버튼을 클릭해 데이터를 재인덱스하세요.", - "configure-webhook-message": "OpenMetadata는 등록된 웹훅으로 이벤트 알림을 자동 전송하도록 구성할 수 있습니다. 웹훅 이름과 HTTP 콜백을 받을 엔드포인트 URL을 입력하세요. 관심 있는 이벤트(예: 엔터티 생성, 업데이트, 삭제)에 기반하여 알림을 받으려면 이벤트 필터를 사용하고, 웹훅의 용도와 사용 사례를 이해할 수 있도록 설명을 추가하세요. 고급 구성을 통해 HMAC 서명을 이용한 웹훅 이벤트 검증을 위한 공유 비밀 키를 설정할 수도 있습니다.", - "configure-webhook-name-message": "OpenMetadata는 등록된 {{webhookType}} 웹훅을 통해 이벤트 알림을 자동 전송하도록 구성할 수 있습니다. {{webhookType}} 웹훅 이름과 HTTP 콜백을 받을 엔드포인트 URL을 입력하세요. 필요한 엔터티에 대해서만 알림을 받도록 이벤트 필터를 사용하고, 웹훅의 사용 사례를 기록하기 위해 설명을 추가하세요. 또한, 고급 구성을 통해 HMAC 서명을 이용한 {{webhookType}} 웹훅 이벤트 검증을 위한 공유 비밀 키를 설정할 수 있습니다.", - "configured-sso-provider-is-not-supported": "구성된 SSO 공급자 \"{{provider}}\"은(는) 지원되지 않습니다. 서버의 인증 구성을 확인하세요.", - "confirm-delete-message": "이 메시지를 영구적으로 삭제하시겠습니까?", - "connection-details-description": "각 서비스에는 연결을 위한 기본 요구 사항이 있으며, 이 요구 사항은 해당 서비스의 JSON 스키마에서 생성됩니다. 필수 필드는 별표(*)로 표시됩니다.", - "connection-test-failed": "연결 테스트에 실패했습니다. 연결 및 실패한 단계의 권한을 확인하세요.", - "connection-test-successful": "연결 테스트가 성공했습니다.", - "connection-test-warning": "연결 테스트가 일부 성공했습니다. 일부 단계에서 실패가 발생하여 부분적인 메타데이터만 수집됩니다.", - "consumer-aligned-domain-type-description": "여러 소스로부터 데이터를 수집 및 정제하여, 고객 360, 고객 세션 등과 같은 집계 데이터와 데이터 제품을 다른 도메인이 사용할 수 있도록 제공하는 도메인입니다.", - "copied-to-clipboard": "클립보드에 복사됨", - "copy-to-clipboard": "클립보드에 복사", - "create-new-domain-guide": "데이터 메시(Mesh)는 도메인 지향 설계 개념에 따라 특정 비즈니스 도메인별로 데이터를 조직하는 분산 데이터 아키텍처입니다. 팀은 해당 도메인의 운영 및 분석 데이터를 소유하며, 데이터 계약에 따라 소비자에게 제품으로 데이터를 제공합니다. 이는 도메인에 구애받지 않는 셀프 서비스 데이터 인프라에 의해 지원되며, 컨설팅을 위한 지원 팀이 존재합니다.", - "create-new-glossary-guide": "용어집은 조직 내 개념과 용어를 정의하기 위해 사용되는 통제된 어휘집입니다. 용어집은 특정 도메인(예: 비즈니스, 기술)에 국한될 수 있으며, 표준 용어와 개념, 동의어 및 관련 용어를 정의할 수 있습니다. 또한 누가 어떻게 용어를 추가할지에 대한 제어가 가능합니다.", - "create-or-update-email-account-for-bot": "계정 이메일을 변경하면 봇 사용자가 업데이트되거나 새로 생성됩니다.", - "created-this-task-lowercase": "이 작업을 생성했습니다", - "custom-classification-name-dbt-tags": "dbt 태그를 위한 맞춤 OpenMetadata 분류 이름", - "custom-favicon-url-path-message": "파비콘 아이콘의 URL 경로입니다.", - "custom-logo-configuration-message": "회사 로고, 모노그램 및 파비콘으로 OpenMetadata를 맞춤 설정하세요.", - "custom-logo-url-path-message": "로그인 페이지 로고의 URL 경로입니다.", - "custom-monogram-url-path-message": "네비게이션 바 로고의 URL 경로입니다.", - "custom-properties-description": "속성을 확장하여 데이터 자산을 풍부하게 하기 위해 맞춤 메타데이터를 캡처하세요.", - "custom-property-is-set-to-message": "{{fieldName}}이(가) 설정되었습니다.", - "custom-property-name-validation": "이름은 공백, 밑줄, 점 없이 소문자로 시작해야 합니다.", - "customize-landing-page-header": "페르소나 \"<0>{{persona}}\"를 위한 {{pageName}}을 맞춤 설정하세요.", - "customize-open-metadata-description": "조직과 팀의 필요에 맞게 OpenMetadata UX를 맞춤 설정하세요.", - "data-asset-has-been-action-type": "데이터 자산이 {{actionType}}되었습니다.", - "data-insight-alert-destination-description": "관리자 또는 팀에게 이메일 알림을 전송하세요.", - "data-insight-alert-trigger-description": "실시간으로 트리거하거나 일간, 주간, 월간으로 예약 실행하세요.", - "data-insight-message": "데이터 인사이트 파이프라인을 관리하세요.", - "data-insight-page-views": "데이터셋 유형이 조회된 횟수를 표시합니다.", - "data-insight-pipeline-description": "데이터 인사이트 파이프라인을 배포하여 데이터 사용량을 모니터링하고 KPI를 설정하세요. 자세한 내용은 <0>{{link}}를 참조하세요.", - "data-insight-report-send-failed-message": "데이터 인사이트 보고서 전송에 실패했습니다.", - "data-insight-report-send-success-message": "데이터 인사이트 보고서가 성공적으로 전송되었습니다.", - "data-insight-subtitle": "시간에 따른 모든 데이터 자산의 상태를 한눈에 볼 수 있습니다.", - "database-service-name-message": "계보 생성을 위해 데이터베이스 서비스 이름을 추가하세요.", - "dbt-catalog-file-extract-path": "dbt 카탈로그 파일 경로를 입력하여 dbt 모델과 해당 열 스키마를 추출합니다.", - "dbt-cloud-type": "dbt 클라우드 계정에 여러 개의 {{type}}이 있는 경우, dbt 실행 산출물을 추출할 {{type}}의 ID를 지정하세요.", - "dbt-ingestion-description": "메타데이터 수집 설정 후 dbt 워크플로우를 구성 및 배포할 수 있습니다. 동일한 데이터베이스 서비스에 대해 여러 dbt 파이프라인을 설정할 수 있으며, 이 파이프라인은 테이블 엔터티의 dbt 탭에 데이터를 공급하고, dbt 노드에서 계보를 생성하며, 테스트를 추가합니다. 시작하려면 dbt 파일의 소스 구성을 추가하세요.", - "dbt-manifest-file-path": "dbt 매니페스트 파일 경로를 입력하여 dbt 모델을 추출하고 테이블과 연관시키세요.", - "dbt-optional-config": "dbt에서 설명을 업데이트할지 선택하는 옵션 구성", - "dbt-result-file-path": "dbt 실행 결과 파일 경로를 입력하여 테스트 결과 정보를 추출하세요.", - "dbt-run-result-http-path-message": "dbt 실행 결과를 HTTP 경로에서 추출합니다.", - "deeply-understand-table-relations-message": "하나의 플랫폼에서 데이터의 생산자와 소비자를 파악하여 생산성을 높이세요. 여러 도구의 데이터를 중앙에서 함께 협업하면 더욱 효과적입니다.", - "define-custom-property-for-entity": "조직의 필요에 맞게 {{entity}}에 대한 맞춤 속성을 정의하세요.", - "delete-action-description": "이 {{entityType}}을(를) 삭제하면 OpenMetadata에서 해당 메타데이터가 영구적으로 제거됩니다.", - "delete-asset-from-entity-type": "이 작업을 삭제하면 해당 {{entityType}}이(가) 엔터티에서 제거됩니다.", - "delete-entity-permanently": "이 {{entityType}}을(를) 삭제하면 영구적으로 제거됩니다.", - "delete-entity-type-action-description": "이 {{entityType}}을(를) 삭제하면 OpenMetadata에서 메타데이터가 영구적으로 제거됩니다.", - "delete-message-question-mark": "메시지를 삭제하시겠습니까?", - "delete-team-message": "\"{{teamName}}\" 팀 하위의 모든 팀도 함께 {{deleteType}} 삭제됩니다.", - "delete-webhook-permanently": "웹훅 {{webhookName}}을(를) 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "derived-tag-warning": "이 태그는 자동으로 파생되며, 관련 용어집 용어를 삭제해야만 제거할 수 있습니다.", - "destination-selection-warning": "\"{{subscriptionCategory}}\"가 알림을 받을 수 있도록 \"{{subscriptionType}}\"이(가) 구성되었는지 확인하세요.", - "disable-app": "이 작업은 {{app}} 애플리케이션을 비활성화합니다.", - "disable-classification-description": "분류를 비활성화하면 어떤 엔터티에서도 해당 분류로 검색하거나 관련 태그를 할당할 수 없습니다.", - "disabled-classification-actions-message": "비활성화된 분류에서는 이 작업을 수행할 수 없습니다.", - "discover-your-data-and-unlock-the-value-of-data-assets": "코드 없이 데이터 품질을 손쉽게 테스트, 배포, 결과 수집할 수 있습니다. 즉각적인 테스트 실패 알림으로 신뢰할 수 있는 데이터를 유지하세요.", - "domain-does-not-have-assets": "도메인 {{name}}에는 데이터 제품에 추가할 자산이 없습니다.", - "domains-not-configured": "도메인이 구성되지 않았습니다.", - "downstream-depth-message": "다운스트림 깊이에 대한 값을 선택하세요.", - "downstream-depth-tooltip": "대상(하위 레벨)을 식별하기 위해 최대 3개의 다운스트림 노드를 표시합니다.", - "drag-and-drop-files-here": "파일을 여기에 드래그 앤 드롭하세요.", - "drag-and-drop-or-browse-csv-files-here": "드래그 앤 드롭하거나 <0>{{text}}를 클릭하여 CSV 파일을 선택하세요.", - "duration-in-iso-format": "ISO 8601 형식 ('PnYnMnDTnHnMnS')의 기간", - "edit-entity-style-description": "{{entity}}의 아이콘과 배지 색상을 변경하세요.", - "edit-glossary-display-name-help": "표시 이름 업데이트", - "edit-glossary-name-help": "이름을 변경하면 기존 태그가 제거되고 새 태그가 생성됩니다.", - "edit-service-entity-connection": "{{entity}} 서비스 연결 수정", - "elastic-search-message": "Elasticsearch 인덱스가 최신 상태인지 동기화하거나 재생성하여 확인하세요.", - "elastic-search-re-index-pipeline-description": "검색 인덱스 파이프라인은 Elasticsearch의 데이터를 재인덱스하는 데 사용됩니다. 자세한 내용은 <0>{{link}}를 참조하세요.", - "elasticsearch-setup": "Elasticsearch에 메타데이터를 수집하고 인덱싱하기 위한 설정 방법을 확인하려면 안내를 따르세요.", - "email-configuration-message": "이메일 전송을 위한 SMTP 설정을 구성하세요.", - "email-is-invalid": "유효하지 않은 이메일입니다.", - "email-verification-token-expired": "이메일 확인 토큰이 만료되었습니다.", - "enable-classification-description": "분류를 활성화하면 어떤 엔터티에서도 해당 분류로 검색하거나 관련 태그를 할당할 수 있습니다.", - "enable-column-profile": "열 프로파일 활성화", - "enable-debug-logging": "디버그 로깅 활성화", - "enables-end-to-end-metadata-management": "모든 데이터 자산을 한 곳에서 확인하고, 올바른 데이터를 통해 중요한 인사이트를 얻으세요. 이제 데이터 잠재력을 열어 현명한 비즈니스 결정을 내리세요!", - "endpoint-should-be-valid": "엔드포인트는 유효한 URL이어야 합니다.", - "ensure-airflow-set-up-correctly-before-heading-to-ingest-metadata": "메타데이터 수집을 시작하기 전에 Airflow가 올바르게 구성되었는지 확인하세요.", - "ensure-elasticsearch-is-up-and-running": "Elasticsearch Docker가 실행 중인지 확인하세요.", - "enter-a-field": "{{field}}을(를) 입력하세요.", - "enter-column-description": "열 설명을 입력하세요.", - "enter-comma-separated-field": "쉼표(,)로 구분된 {{field}}을(를) 입력하세요.", - "enter-display-name": "표시 이름을 입력하세요.", - "enter-feature-description": "기능 설명을 입력하세요.", - "enter-interval": "간격을 입력하세요.", - "enter-test-case-name": "테스트 케이스 이름을 입력하세요.", - "enter-test-suite-name": "테스트 스위트 이름을 입력하세요.", - "enter-your-registered-email": "비밀번호 재설정 링크를 받기 위해 등록된 이메일을 입력하세요.", - "entity-already-exists": "{{entity}}이(가) 이미 존재합니다.", - "entity-are-not-available": "{{entity}}이(가) 사용 가능하지 않습니다.", - "entity-delimiters-not-allowed": "구분 기호가 있는 이름은 허용되지 않습니다.", - "entity-does-not-have-followers": "{{entityName}}에는 아직 팔로워가 없습니다.", - "entity-enabled-success": "{{entity}}이(가) 성공적으로 활성화되었습니다.", - "entity-ingestion-added-successfully": "{{entity}} 수집이 성공적으로 추가되었습니다.", - "entity-is-not-valid": "{{entity}}이(가) 유효하지 않습니다.", - "entity-is-not-valid-url": "{{entity}}이(가) 유효한 URL이 아닙니다.", - "entity-maximum-size": "{{entity}}은(는) 최대 {{max}}자까지 가능합니다.", - "entity-name-validation": "이름은 문자, 숫자, 밑줄, 하이픈, 점, 괄호, 앰퍼샌드만 포함할 수 있습니다.", - "entity-not-contain-whitespace": "{{entity}}은(는) 공백을 포함해서는 안 됩니다.", - "entity-owned-by-name": "이 엔터티는 {{entityOwner}}이(가) 소유합니다.", - "entity-pattern-validation": "{{entity}}은(는) 공백과 함께 다음 특수 문자만 포함할 수 있습니다: {{pattern}}.", - "entity-restored-error": "{{entity}} 복구 중 오류가 발생했습니다.", - "entity-restored-success": "{{entity}}이(가) 성공적으로 복구되었습니다.", - "entity-saved-successfully": "{{entity}}이(가) 성공적으로 저장되었습니다.", - "entity-size-in-between": "{{entity}}의 크기는 {{min}}과(와) {{max}} 사이여야 합니다.", - "entity-size-must-be-between-2-and-64": "{{entity}}의 크기는 2에서 64 사이여야 합니다.", - "entity-transfer-message": "<0>{{from}}의 {{entity}}을(를) <0>{{to}}의 {{entity}} 아래로 이동하려면 확인을 클릭하세요.", - "error-team-transfer-message": "팀 유형이 {{dragTeam}}인 팀은 {{dropTeam}}의 하위 팀으로 이동할 수 없습니다.", - "error-while-fetching-access-token": "액세스 토큰을 가져오는 중 오류가 발생했습니다.", - "explore-our-guide-here": "여기에서 가이드를 확인해 보세요.", - "export-entity-help": "모든 {{entity}}을 CSV 파일로 다운로드하여 팀과 공유하세요.", - "failed-status-for-entity-deploy": "<0>{{entity}}이(가) {{entityStatus}}되었으나 배포에 실패했습니다.", - "feed-asset-action-header": "{{action}} <0>데이터 자산", - "feed-custom-property-header": "업데이트된 맞춤 속성:", - "feed-entity-action-header": "{{action}} <0>{{entity}}", - "feed-field-action-entity-header": "{{action}} <0>{{field}} 대상:", - "feed-filter-all": "내가 소유하고 팔로우하는 모든 데이터 자산의 피드", - "feed-filter-following": "내가 팔로우하는 모든 데이터 자산의 피드", - "feed-filter-owner": "내가 소유한 모든 데이터 자산의 피드", - "feed-test-case-header": "<0>데이터 품질 결과가 추가되었습니다", - "fetch-dbt-files": "dbt 카탈로그 및 매니페스트 파일을 가져올 수 있는 사용 가능한 소스들입니다.", - "fetch-pipeline-status-error": "파이프라인 상태를 가져오는 중 오류가 발생했습니다.", - "field-ca-certs-description": "구성에 인증서 경로를 추가해야 합니다. 경로는 수집 컨테이너 내의 로컬 경로여야 합니다.", - "field-data-is-not-available-for-deleted-entities": "삭제된 엔터티에 대해 {{field}} 데이터는 제공되지 않습니다.", - "field-insight": "유형별로 {{field}}가 있는 데이터 자산의 비율을 표시합니다.", - "field-region-name-description": "AWS 자격증명을 사용할 경우 리전 이름이 필요합니다.", - "field-text-is-invalid": "{{fieldText}}이(가) 유효하지 않습니다.", - "field-text-is-required": "{{fieldText}}이(가) 필요합니다.", - "field-timeout-description": "연결 타임아웃", - "field-use-aws-credentials-description": "AWS에서 OpenSearch에 연결할 때 AWS 자격증명을 사용할지 여부를 나타냅니다.", - "field-use-ssl-description": "Elasticsearch에 연결할 때 SSL 사용 여부를 나타냅니다. 기본적으로 SSL 설정은 무시됩니다.", - "field-verify-certs-description": "Elasticsearch에 SSL로 연결할 때 인증서를 검증할지 여부를 나타냅니다. 기본적으로 무시되며 true로 설정되어 있습니다. 'CA Certificates' 속성에 인증서를 제공해야 합니다.", - "filter-pattern-include-exclude-info": "쉼표로 구분된 정규식 목록을 추가하여 명시적으로 {{filterPattern}}을(를) {{activity}}합니다.", - "filter-pattern-info": "메타데이터 수집의 일부로서 {{filterPattern}}을 포함하거나 제외할지를 선택하세요.", - "filter-pattern-placeholder": "필터 패턴을 추가하려면 입력 후 Enter 키를 누르세요.", - "find-apps-to-improve-data": "데이터 개선을 위한 애플리케이션을 찾으세요.", - "find-in-table": "테이블에서 찾기", - "fosters-collaboration-among-producers-and-consumers": "조직의 목표와 KPI를 설정하여 데이터 문화를 촉진하고, 시기적절한 보고서로 데이터 상태를 모니터링하며 지속적인 개선 문화를 구축하세요.", - "get-started-with-open-metadata": "OpenMetadata 시작하기", - "glossary-tag-assignment-help-message": "이 자산들을 제거하거나 자산에서 충돌하는 태그를 제거한 후, 다시 태그를 추가할 수 있습니다.", - "glossary-tag-update-description": "이 작업은 용어집 용어와 연결된 모든 자산의 태그를 업데이트합니다.", - "glossary-tag-update-modal-title-failed": "다음 데이터 자산에 대한 검증에 실패했습니다.", - "glossary-tag-update-modal-title-validating": "데이터 자산 검증 중", - "glossary-term-description": "용어집의 모든 용어는 고유한 정의를 가집니다. 개념의 표준 용어를 정의하는 것 외에도, 동의어 및 관련 용어(예: 상위 및 하위 용어)를 지정할 수 있습니다. 용어와 관련된 자산에 대한 참조도 추가할 수 있으며, 새 용어를 추가하거나 기존 용어를 업데이트할 수 있습니다. 일부 사용자가 용어를 검토하여 승인 또는 거부할 수 있습니다.", - "glossary-term-status": "용어집 용어가 {{status}}되었습니다.", - "go-back-to-login-page": "로그인 페이지로 돌아가기", - "govern-url-size-message": "아이콘의 가로세로 비율은 1:1이어야 하며, 권장 크기는 64 x 64 px입니다.", - "group-team-type-change-message": "‘Group’ 팀 유형은 변경할 수 없습니다. 원하는 유형의 새 팀을 생성하세요.", - "group-type-team-not-allowed-to-have-sub-team": "‘Group’ 유형의 팀은 하위 팀을 가질 수 없습니다.", - "has-been-created-successfully": "성공적으로 생성되었습니다.", - "have-not-explored-yet": "아직 탐색하지 않으셨나요?", - "hex-code-placeholder": "HEX 색상 코드를 선택하거나 입력하세요.", - "hex-color-validation": "입력한 값이 유효한 HEX 코드가 아닙니다.", - "hi-user-welcome-to": "안녕하세요 {{user}}, OpenMetadata에 오신 것을 환영합니다!", - "import-entity-help": "여러 {{entity}}를 한 번에 CSV 파일로 업로드하여 시간과 노력을 절약하세요.", - "in-this-database": "이 데이터베이스에서", - "include-assets-message": "데이터 소스에서 {{assets}}을 추출하도록 활성화하세요.", - "include-database-filter-extra-information": "서비스 생성 시 추가된 데이터베이스입니다.", - "include-lineage-message": "파이프라인에서 계보를 가져오지 않도록 구성합니다.", - "ingest-sample-data-for-entity": "각 {{entity}}에서 샘플 데이터를 추출하세요.", - "ingestion-bot-cant-be-deleted": "수집 봇은 삭제할 수 없습니다.", - "ingestion-pipeline-name-message": "이 파이프라인 인스턴스를 고유하게 식별하는 이름입니다.", - "ingestion-pipeline-name-successfully-deployed-entity": "설정 완료! 이(가) 성공적으로 배포되었습니다. {{entity}}은(는) 스케줄에 따라 정기적으로 실행됩니다.", - "instance-identifier": "이 구성 인스턴스를 고유하게 식별하는 이름입니다.", - "integration-description": "생산성을 향상시키기 위해 애플리케이션 및 봇을 구성하세요.", - "invalid-object-key": "유효하지 않은 객체 키입니다. 문자, 밑줄 또는 달러 기호로 시작한 후 문자, 밑줄, 달러 기호 또는 숫자가 올 수 있습니다.", - "invalid-property-name": "유효하지 않은 속성 이름입니다.", - "jwt-token": "생성된 토큰을 사용하여 OpenMetadata API에 접근할 수 있습니다.", - "jwt-token-expiry-time-message": "JWT 토큰 만료 시간이 초 단위로 설정되어 있습니다.", - "kill-ingestion-warning": "이 수집 작업을 종료하면 실행 중 및 대기 중인 모든 워크플로우가 중지되고 실패로 표시됩니다.", - "kpi-subtitle": "데이터 자산의 건강 상태를 가장 잘 반영하는 핵심 성과 지표(KPI)를 파악하세요.", - "kpi-target-achieved": "목표 달성을 축하합니다!", - "kpi-target-achieved-before-time": "축하합니다! 팀이 설정된 KPI 목표를 훌륭하게 달성했습니다. 이제 이 KPI를 보관하거나 새 목표를 설정하여 데이터 문화를 더욱 강화할 수 있습니다.", - "kpi-target-overdue": "알림: 설명 KPI 목표에 아직 도달하지 못했으나, 시간이 남아 있습니다 – 조직에는 {{count}}일이 남았습니다. 진행 상황 유지를 위해 데이터 인사이트 보고서를 활성화하세요. 이를 통해 모든 팀에 주간 업데이트를 전송하여 조직의 KPI 달성을 촉진할 수 있습니다.", - "leave-the-team-team-name": "{{teamName}} 팀에서 나가세요.", - "length-validator-error": "최소 {{length}}개의 {{field}}가 필요합니다.", - "lineage-ingestion-description": "메타데이터 수집 설정 후 계보 수집을 구성 및 배포할 수 있습니다. 계보 수집 워크플로우는 쿼리 기록을 가져와 CREATE, INSERT, MERGE 등의 쿼리를 파싱하고 관련 엔터티 간의 계보를 생성합니다. 한 데이터베이스 서비스에 대해 계보 수집 파이프라인은 하나만 설정할 수 있습니다. 시작하려면 쿼리 로그 기간(일)과 결과 제한을 정의하세요.", - "link-copy-to-clipboard": "링크가 클립보드에 복사되었습니다.", - "list-of-strings-regex-patterns-csv": "쉼표로 구분된 문자열/정규식 패턴 목록을 입력하세요.", - "login-fail-attempt-message": "애플리케이션에 연속적으로 로그인 실패 시 허용되는 시도 횟수입니다.", - "logout-confirmation": "로그아웃하시겠습니까?", - "look-like-upgraded-om": "OpenMetadata가 업그레이드된 것 같습니다.", - "made-announcement": "공지사항을 작성했습니다.", - "make-an-announcement": "공지사항을 작성하세요!", - "manage-airflow-api": "OpenMetadata - 관리형 Airflow API", - "manage-airflow-api-failed": "OpenMetadata - 관리형 Airflow API를 찾는 데 실패했습니다.", - "mark-all-deleted-table-message": "이 옵션은 테이블의 소프트 삭제를 활성화하는 선택적 구성입니다. 활성화되면 소스에서 삭제된 테이블만 소프트 삭제되며, 데이터 소스의 모든 스키마에 적용됩니다. 관련된 테스트 스위트나 계보 정보 등도 함께 삭제됩니다. 여러 메타데이터 수집 파이프라인이 있는 경우 이 옵션을 사용하지 마세요. 이 기능을 사용하려면 markDeletedTables 옵션도 활성화되어 있어야 합니다.", - "mark-deleted-entity-message": "소스에서 삭제된 '{{entityPlural}}'이(가) 있을 경우 OpenMetadata에서 해당 '{{entity}}'을(를) 소프트 삭제하도록 설정하는 선택적 구성입니다. 삭제 후, 해당 '{{entity}}'과 관련된 모든 엔터티(예: 계보 등)도 삭제됩니다.", - "mark-deleted-table-message": "이 옵션은 테이블의 소프트 삭제를 활성화하는 선택적 구성입니다. 활성화되면 소스에서 삭제된 테이블만 소프트 삭제되며, 현재 파이프라인으로 수집되는 스키마에만 적용됩니다. 관련된 테스트 스위트나 계보 정보 등도 삭제됩니다.", - "markdown-editor-placeholder": "사용자를 태그하려면 @멘션, 데이터 자산을 태그하려면 #멘션을 사용하세요.", - "marketplace-verify-msg": "OpenMetadata는 발행인이 도메인을 제어하며 기타 요구 사항을 충족하는지 확인했습니다.", - "maximum-value-error": "최대값은 최소값보다 커야 합니다.", - "member-description": "OpenMetadata에서 사용자 및 팀에 대한 접근을 간소화하세요.", - "mentioned-you-on-the-lowercase": "에서 당신을 언급했습니다.", - "metadata-ingestion-description": "선택한 서비스 유형에 따라 스키마(또는 테이블), 토픽(메시징) 또는 대시보드의 필터 패턴 세부 정보를 입력하세요. 필터 패턴을 포함하거나 제외할 수 있으며, 보기 포함, 데이터 프로파일러 활성화/비활성화, 샘플 데이터 수집 여부를 선택할 수 있습니다.", - "minimum-value-error": "최소값은 최대값보다 작아야 합니다.", - "minute": "분", - "modify-hierarchy-entity-description": "상위 {{entity}}을 변경하여 계층 구조를 수정하세요.", - "most-active-users": "페이지 조회수를 기반으로 플랫폼에서 가장 활동적인 사용자를 표시합니다.", - "most-viewed-data-assets": "가장 많이 조회된 데이터 자산을 표시합니다.", - "mutually-exclusive-alert": "만약 {{entity}}에 대해 '상호 배타적' 옵션을 활성화하면, 사용자는 데이터 자산에 하나의 {{child-entity}}만 적용할 수 있습니다. 이 옵션이 활성화되면 비활성화할 수 없습니다.", - "name-of-the-bucket-dbt-files-stored": "dbt 파일이 저장된 버킷의 이름입니다.", - "new-conversation": "새로운 대화를 시작합니다.", - "new-to-the-platform": "플랫폼이 처음이신가요?", - "no-access-placeholder": "접근 권한이 없습니다. 관리자에게 문의하세요.", - "no-activity-feed": "현재 소유하거나 팔로우하는 데이터 자산에 업데이트가 없습니다. <0>{{explored}}를 확인하고, 관심 있는 데이터 자산의 소유권을 주장하거나 팔로우하세요.", - "no-announcement-message": "공지사항이 없습니다. 공지 추가 버튼을 클릭하여 추가하세요.", - "no-asset-available": "사용 가능한 자산이 없습니다.", - "no-closed-task": "닫힌 작업이 없습니다.", - "no-config-available": "사용 가능한 연결 구성이 없습니다.", - "no-config-plural": "구성이 없습니다.", - "no-custom-properties-table": "현재 테이블 데이터 자산에 정의된 사용자 지정 속성이 없습니다. 사용자 지정 속성을 추가하는 방법은 <0>{{docs}}를 참조하세요.", - "no-data": "데이터가 없습니다.", - "no-data-assets": "OpenMetadata에 오신 것을 환영합니다! 아직 데이터 자산이 추가되지 않은 것 같습니다. 시작하려면 <0>시작하기 가이드를 확인하세요.", - "no-data-available": "사용 가능한 데이터가 없습니다.", - "no-data-available-entity": "{{entity}}에 사용 가능한 데이터가 없습니다.", - "no-data-available-for-search": "데이터를 찾을 수 없습니다. 다른 텍스트로 검색해 보세요.", - "no-data-available-for-selected-filter": "데이터를 찾을 수 없습니다. 필터를 변경해 보세요.", - "no-data-quality-test-case": "이 테이블에 구성된 데이터 품질 테스트가 없는 것 같습니다. 데이터 품질 테스트 설정 방법은 <0>{{explore}}를 참조하세요.", - "no-domain-assigned-to-entity": "{{entity}}에 할당된 도메인이 없습니다.", - "no-domain-available": "구성할 도메인이 없습니다. 도메인을 추가하려면 <0>{{link}}를 클릭하세요.", - "no-entity-activity-message": "{{entity}}에 아직 활동이 없습니다. 대화를 시작하려면 클릭하세요.", - "no-entity-available-with-name": "이름이 일치하는 {{entity}}이(가) 없습니다.", - "no-entity-data-available": "{{entity}} 데이터가 없습니다.", - "no-entity-found-for-name": "{{name}}에 해당하는 {{entity}}을(를) 찾을 수 없습니다.", - "no-execution-runs-found": "파이프라인 실행 기록을 찾을 수 없습니다.", - "no-features-data-available": "사용 가능한 기능 데이터가 없습니다.", - "no-feed-available-for-selected-filter": "피드를 찾을 수 없습니다. 필터를 변경해 보세요.", - "no-glossary-term": "현재 정의된 용어집 용어가 없는 것 같습니다. 새 용어집 용어를 생성하려면 '용어 추가' 버튼을 사용하세요.", - "no-incident-found": "현재 데이터 품질에 영향을 주는 사건이 없습니다.", - "no-info-about-joined-tables": "조인된 테이블에 대한 정보가 없습니다.", - "no-ingestion-available": "사용 가능한 수집 데이터가 없습니다.", - "no-ingestion-description": "수집 데이터를 보려면 메타데이터 수집을 실행하세요. 스케줄 설정 방법은 <0>{{link}} 문서를 참조하세요.", - "no-ingestion-pipeline-found": "수집 파이프라인을 찾을 수 없습니다. 수집 파이프라인을 설정하려면 배포 버튼을 클릭하세요.", - "no-inherited-roles-found": "상속된 역할이 없습니다.", - "no-installed-applications-found": "현재 설치된 애플리케이션이 없습니다. '앱 추가' 버튼을 클릭하여 설치하세요.", - "no-kpi": "조직에 아직 설정된 핵심 성과 지표(KPI)가 없습니다! OpenMetadata에서 KPI를 설정하면 문서화, 효과적인 소유권, 효율적인 티어링을 위한 명확한 목표를 세울 수 있습니다.", - "no-kpi-available-add-new-one": "사용 가능한 KPI가 없습니다. KPI 추가 버튼을 클릭하여 추가하세요.", - "no-kpi-found": "{{name}} 이름의 KPI를 찾을 수 없습니다.", - "no-match-found": "일치하는 항목이 없습니다.", - "no-mentions": "현재 활동 내역에 멘션이 없습니다. 좋은 성과를 계속 이어가세요!", - "no-notification-found": "알림을 찾을 수 없습니다.", - "no-open-task": "열린 작업이 없습니다.", - "no-open-tasks": "좋은 소식입니다! 현재 열린 작업이 없습니다. 잠시 여유를 즐기세요!", - "no-owned-data": "데이터는 소유될 때 더 가치가 있습니다. 아직 귀하나 팀이 데이터 자산의 소유권을 주장하지 않은 것 같습니다. 데이터 자산을 소유하기 시작하려면 <0>탐색 옵션을 클릭하세요.", - "no-permission-for-action": "이 작업을 수행할 권한이 없습니다.", - "no-permission-to-view": "이 데이터를 볼 권한이 없습니다.", - "no-persona-assigned": "할당된 페르소나가 없습니다.", - "no-persona-message": "랜딩 페이지 맞춤 설정을 위해 페르소나가 필요합니다. <0>{{link}}에서 페르소나를 생성하세요.", - "no-profiler-enabled-summary-message": "이 테이블에 대해 프로파일러가 활성화되어 있지 않습니다.", - "no-profiler-message": "데이터 프로파일러는 수집 시 선택적 구성입니다. 문서를 참조하여 활성화하세요.", - "no-recently-viewed-date": "최근에 조회한 데이터 자산이 없습니다. 흥미로운 자산을 찾아보세요!", - "no-reference-available": "참조를 찾을 수 없습니다.", - "no-related-terms-available": "관련 용어가 없습니다.", - "no-roles-assigned": "할당된 역할이 없습니다.", - "no-rule-found": "규칙을 찾을 수 없습니다.", - "no-searched-terms": "검색된 용어가 없습니다.", - "no-selected-dbt": "dbt 구성을 위한 소스가 선택되지 않았습니다.", - "no-service-connection-details-message": "{{serviceName}}에 연결 세부 정보가 입력되지 않았습니다. 수집 작업 전에 세부 정보를 추가하세요.", - "no-synonyms-available": "사용 가능한 동의어가 없습니다.", - "no-table-pipeline": "정기적으로 데이터 품질 테스트를 자동화하기 위해 파이프라인을 추가하세요. 최적의 결과를 위해 테이블 로드 빈도에 맞게 스케줄을 설정하는 것이 좋습니다.", - "no-tags-description": "이 카테고리에는 정의된 태그가 없습니다. 새 태그를 생성하려면 '추가' 버튼을 클릭하세요.", - "no-tasks-assigned": "현재 귀하에게 할당된 대기 작업이 없습니다.", - "no-team-found": "팀을 찾을 수 없습니다.", - "no-terms-found": "용어를 찾을 수 없습니다.", - "no-terms-found-for-search-text": "\"{{searchText}}\"에 해당하는 용어를 찾을 수 없습니다.", - "no-test-result-for-days": "{{days}} 동안의 테스트 결과를 찾을 수 없습니다.", - "no-test-suite-table-pipeline": "파이프라인을 통합하면 데이터 품질 테스트를 정기적으로 자동화할 수 있습니다. 파이프라인 실행 전에 테스트를 구성하세요.", - "no-token-available": "사용 가능한 토큰이 없습니다.", - "no-total-data-assets": "데이터 인사이트를 통해 조직 내 데이터 자산의 총 수, 신규 자산 추가 속도 등 데이터 전반의 흐름을 파악할 수 있습니다. 자세한 내용은 <0>{{setup}}를 참조하세요.", - "no-user-available": "사용 가능한 사용자가 없습니다.", - "no-username-available": "\"<0>{{user}}\" 이름의 사용자를 찾을 수 없습니다.", - "no-users": "{{text}}에 해당하는 사용자가 없습니다.", - "no-version-type-available": "사용 가능한 {{type}} 버전이 없습니다.", - "no-widgets-to-add": "추가할 새로운 위젯이 없습니다.", - "nodes-per-layer-message": "레이어당 노드 수를 입력하세요.", - "nodes-per-layer-tooltip": "레이어당 표시할 노드 수를 선택하세요. 기존 노드 수가 설정된 수보다 많으면 페이지네이션이 표시됩니다.", - "not-followed-anything": "탐색을 시작하고 관심 있는 데이터 자산을 팔로우하세요.", - "notification-description": "실시간 업데이트와 시기적절한 알림을 받을 수 있도록 알림을 설정하세요.", - "om-description": "중앙 집중식 메타데이터 저장소로, 데이터를 발견하고 협업하며 올바른 데이터를 관리할 수 있습니다.", - "onboarding-claim-ownership-description": "데이터는 소유될 때 더 효과적입니다. 귀하가 소유한 데이터 자산을 확인하고 소유권을 주장하세요.", - "onboarding-explore-data-description": "조직 내 인기 데이터 자산을 살펴보세요.", - "onboarding-stay-up-to-date-description": "자주 사용하는 데이터셋을 팔로우하여 최신 상태를 유지하세요.", - "only-reviewers-can-approve-or-reject": "검토자만 승인하거나 거부할 수 있습니다.", - "optional-configuration-update-description-dbt": "dbt에서 설명을 업데이트할지 선택하는 옵션 구성", - "page-is-not-available": "찾으시는 페이지를 찾을 수 없습니다.", - "page-sub-header-for-activity-feed": "데이터 변경 이벤트의 요약을 볼 수 있는 활동 피드입니다.", - "page-sub-header-for-admins": "조직 내 다른 관리자 및 각 팀과 역할의 세부 정보를 확인하세요.", - "page-sub-header-for-advanced-search": "and/or 조건을 사용한 구문 편집기로 올바른 데이터 자산을 빠르게 찾아보세요.", - "page-sub-header-for-apis": "가장 인기 있는 API 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-bots": "범위가 제한된 접근 권한을 가진 잘 정의된 봇을 생성하세요.", - "page-sub-header-for-column-profile": "프로파일러를 통해 열 구조를 모니터링하고 이해하세요.", - "page-sub-header-for-customize-landing-page": "특정 사용자 페르소나와 경험에 맞게 OpenMetadata 랜딩 페이지를 맞춤 설정하세요.", - "page-sub-header-for-dashboards": "가장 인기 있는 대시보드 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-data-observability": "UI를 통해 테스트 스위트 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-data-quality": "데이터 품질 테스트를 구축하여 신뢰할 수 있는 데이터 제품을 만드세요.", - "page-sub-header-for-databases": "가장 인기 있는 데이터베이스 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-login-configuration": "로그인 실패 시 처리 및 JWT 토큰 만료 시간을 정의하세요.", - "page-sub-header-for-messagings": "가장 많이 사용되는 메시징 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-metadata": "UI를 통해 메타데이터 서비스를 구성하세요.", - "page-sub-header-for-ml-models": "UI를 통해 ML 모델 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-om-health-configuration": "데이터베이스 접근, Elasticsearch 상태, 파이프라인 서비스 클라이언트, jwks 구성 및 마이그레이션을 확인하세요.", - "page-sub-header-for-persona": "페르소나를 통해 사용자 경험을 향상하고 맞춤 설정하세요.", - "page-sub-header-for-pipelines": "가장 많이 사용되는 파이프라인 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-policies": "세분화된 접근 제어를 위한 규칙 집합으로 정책을 정의하세요.", - "page-sub-header-for-profiler-configuration": "열 데이터 유형에 따라 계산할 메트릭을 설정하여 프로파일러 동작을 전역적으로 맞춤 설정하세요.", - "page-sub-header-for-roles": "사용자 또는 팀에 대해 포괄적인 역할 기반 접근을 할당하세요.", - "page-sub-header-for-search": "가장 인기 있는 검색 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-setting": "귀하의 필요에 맞게 OpenMetadata 애플리케이션을 구성할 수 있습니다.", - "page-sub-header-for-storages": "가장 인기 있는 스토리지 서비스로부터 메타데이터를 수집하세요.", - "page-sub-header-for-table-profile": "프로파일러를 통해 테이블 구조를 모니터링하고 이해하세요.", - "page-sub-header-for-teams": "계층적 팀으로 조직 전체 구조를 표현하세요.", - "page-sub-header-for-users": "계층적 팀으로 조직 전체 구조를 표현하세요.", - "paid-addon-description": "<0>{{app}}은(는) Collate 고객을 위한 유료 애드온입니다.", - "password-error-message": "비밀번호는 최소 8자, 최대 56자여야 하며, 하나 이상의 대문자(A-Z), 소문자(a-z), 숫자, 그리고 하나의 특수 문자(예: !, %, @, # 등)를 포함해야 합니다.", - "password-pattern-error": "비밀번호는 최소 8자, 최대 56자이며, 하나의 특수 문자, 대문자, 소문자를 포함해야 합니다.", - "path-of-the-dbt-files-stored": "dbt 파일이 저장된 폴더의 경로", - "permanently-delete-metadata": "이 <0>{{entityName}}을(를) 영구적으로 삭제하면 OpenMetadata에서 해당 메타데이터가 제거되어 복구할 수 없습니다.", - "permanently-delete-metadata-and-dependents": "이 {{entityName}}을(를) 영구적으로 삭제하면 해당 메타데이터와 함께 관련 {{dependents}}의 메타데이터도 OpenMetadata에서 영구적으로 제거됩니다.", - "personal-access-token": "개인 접근 토큰", - "pipeline-action-failed-message": "파이프라인 {{action}}에 실패했습니다!", - "pipeline-action-success-message": "파이프라인이 성공적으로 {{action}}되었습니다!", - "pipeline-description-message": "파이프라인에 대한 설명입니다.", - "pipeline-disabled-ingestion-deployment": "수집 데이터를 보려면, 아래 <0>{{link}}를 참고하여 자체 환경에서 메타데이터 수집을 실행하세요.", - "pipeline-killed-successfully": "{{pipelineName}}의 실행 중인 워크플로우가 성공적으로 종료되었습니다.", - "pipeline-not-deployed": "파이프라인이 배포되지 않았습니다.", - "pipeline-scheduler-message": "수집 스케줄러가 응답하지 않습니다. 자세한 사항은 Collate 지원팀에 문의하세요. 감사합니다.", - "pipeline-will-trigger-manually": "파이프라인은 수동으로만 트리거됩니다.", - "pipeline-will-triggered-manually": "파이프라인은 수동으로만 트리거됩니다.", - "please-contact-us": "자세한 내용은 support@getcollate.io로 문의하세요.", - "please-enter-to-find-data-assets": "데이터 자산을 찾으려면 Enter 키를 누르세요: <0>{{keyword}}", - "please-refresh-the-page": "변경 사항을 확인하려면 페이지를 새로고침하세요.", - "please-type-text-to-confirm": "확인을 위해 {{text}}를 입력하세요.", - "popup-block-message": "로그인 팝업이 브라우저에 의해 차단되었습니다. <0>활성화한 후 다시 시도하세요.", - "process-pii-sensitive-column-message": "열 이름을 확인하여 PII 민감/비민감 열에 자동으로 태그를 지정하세요.", - "process-pii-sensitive-column-message-profiler": "활성화되면 샘플 데이터를 분석하여 각 열에 적절한 PII 태그를 결정합니다.", - "profile-sample-percentage-message": "프로파일러 값을 백분율로 설정하세요.", - "profile-sample-row-count-message": "프로파일러 값을 행 수로 설정하세요.", - "profiler-ingestion-description": "메타데이터 수집 설정 후 프로파일러 워크플로우를 구성 및 배포할 수 있습니다. 동일한 데이터베이스 서비스에 대해 여러 프로파일러 파이프라인을 설정할 수 있으며, 이 파이프라인은 테이블 엔터티의 프로파일러 탭에 데이터를 공급하고, 해당 엔터티에 구성된 테스트를 실행합니다. 시작하려면 이름, FQN, 그리고 필터 패턴을 정의하세요.", - "profiler-timeout-seconds-message": "프로파일러의 타임아웃 시간을 초 단위로 입력하세요 (선택 사항). 프로파일러는 대기 중인 쿼리가 실행되도록 기다리거나 타임아웃에 도달하면 해당 쿼리를 종료합니다.", - "queries-result-test": "1개 이상의 행을 반환하는 쿼리는 테스트 실패로 처리됩니다.", - "query-log-duration-message": "쿼리 로그에서 사용 데이터를 처리하기 위해 조회할 기간을 조정하는 구성입니다.", - "query-used-by-other-tables": "다른 테이블에서 사용된 쿼리", - "reacted-with-emoji": "{{type}} 이모지로 반응했습니다.", - "redirect-message": "리다이렉트 중입니다. 잠시만 기다리세요.", - "redirecting-to-home-page": "홈페이지로 리다이렉트 중입니다.", - "refer-to-our-doc": "더 도움이 필요하신가요? 자세한 내용은 <0>{{doc}}를 참조하세요.", - "remove-edge-between-source-and-target": "정말로 \"{{sourceDisplayName}}와 {{targetDisplayName}}\" 사이의 연결을 제거하시겠습니까?", - "remove-lineage-edge": "계보 연결 제거", - "rename-entity": "{{entity}}의 이름과 표시 이름을 변경하세요.", - "request-approval-message": "승인 요청:", - "request-description": "요청 설명", - "request-description-message": "요청 설명:", - "request-tags-message": "요청 태그:", - "request-test-case-failure-resolution-message": "테스트 케이스 실패 해결 요청:", - "request-update-description": "업데이트 요청 설명", - "reset-layout-confirmation": "정말로 \"기본 레이아웃\"을 적용하시겠습니까?", - "reset-link-has-been-sent": "비밀번호 재설정 링크가 귀하의 이메일로 전송되었습니다.", - "restore-action-description": "이 {{entityType}}을(를) 복구하면 OpenMetadata에서 해당 메타데이터가 복원됩니다.", - "restore-deleted-team": "팀을 복구하면 모든 메타데이터가 OpenMetadata에 다시 추가됩니다.", - "restore-entities-error": "{{entity}} 복구 중 오류가 발생했습니다.", - "restore-entities-success": "{{entity}}이(가) 성공적으로 복구되었습니다.", - "result-limit-message": "쿼리 로그의 제한을 설정하는 구성입니다.", - "retention-period-description": "보존 기간은 데이터가 삭제 또는 보관 대상으로 간주되기 전까지 유지되는 기간을 의미합니다. 예: 30일, 6개월, 1년 또는 UTC 기준의 ISO 8601 형식(P23DT23H) 등이 유효합니다.", - "run-sample-data-to-ingest-sample-data": "샘플 데이터를 실행하여 OpenMetadata에 샘플 데이터 자산을 수집하세요.", - "run-status-at-timestamp": "{{timestamp}}에 {{status}} 상태로 실행됨", - "schedule-for-ingestion-description": "스케줄은 시간별, 일별 또는 주별로 설정할 수 있습니다. 타임존은 UTC입니다.", - "scheduled-run-every": "매번 실행되도록 예약됨", - "scopes-comma-separated": "쉼표로 구분된 스코프 값을 추가하세요.", - "search-for-edge": "파이프라인, 저장 프로시저 검색", - "search-for-entity-types": "테이블, 토픽, 대시보드, 파이프라인, ML 모델, 용어집, 태그 등을 검색하세요.", - "search-for-ingestion": "수집 검색", - "select-alert-type": "경고 유형을 선택하세요.", - "select-column-name": "열 이름을 선택하세요.", - "select-gcs-config-type": "GCS 구성 유형을 선택하세요.", - "select-interval-type": "간격 유형을 선택하세요.", - "select-interval-unit": "간격 단위를 선택하세요.", - "select-team": "팀 유형을 선택하세요.", - "select-test-case": "예약 간격 실행을 위한 테스트 케이스를 선택하세요. 선택하지 않으면 모든 테스트 케이스가 기본으로 실행됩니다.", - "select-token-expiration": "토큰 만료 기간을 선택하세요.", - "service-created-entity-description": "이(가) 성공적으로 생성되었습니다. 새로 생성된 서비스를 방문하여 세부 정보를 확인하세요. {{entity}}", - "service-description": "다양한 소스로부터 메타데이터를 수집하기 위해 커넥터를 설정하세요.", - "service-name-length": "서비스 이름은 1자 이상 128자 이하여야 합니다.", - "service-requirements-description": "각 서비스에는 연결을 위한 기본 요구 사항이 있으며, 아래에 필요한 기본 사항이 표시됩니다.", - "service-with-delimiters-not-allowed": "구분 기호가 있는 서비스 이름은 허용되지 않습니다.", - "service-with-space-not-allowed": "공백이 포함된 서비스 이름은 허용되지 않습니다.", - "session-expired": "세션이 만료되었습니다! OpenMetadata에 접근하려면 다시 로그인하세요.", - "setup-custom-property": "OpenMetadata는 테이블 엔터티에 맞춤 속성을 지원합니다. 고유한 속성 이름을 추가하여 맞춤 속성을 생성하세요. 이름은 카멜케이스 형식에 따라 소문자로 시작해야 하며, 대문자와 숫자는 포함될 수 있으나 공백, 밑줄, 점은 허용되지 않습니다. 제공된 옵션 중에서 원하는 속성 유형을 선택하고, 팀에 추가 정보를 제공할 수 있도록 설명을 작성하세요.", - "setup-data-insights": "데이터 인사이트 설정 방법", - "size-evolution-description": "조직 내 자산의 크기 변화를 보여줍니다.", - "soft-delete-message-for-entity": "소프트 삭제 시 {{entity}}이(가) 비활성화되며, 해당 {{entity}}에 대한 검색, 읽기 또는 쓰기 작업이 중지됩니다.", - "something-went-wrong": "문제가 발생했습니다.", - "source-aligned-domain-type-description": "다양한 도메인의 데이터를 결합하여 사용자에게 제공하는, 데이터 기반 의사결정을 지원하는 사용자 친화적 도메인입니다.", - "special-character-not-allowed": "특수 문자는 허용되지 않습니다.", - "sql-query-tooltip": "1개 이상의 행을 반환하는 쿼리는 테스트 실패로 처리됩니다.", - "sso-provider-not-supported": "SSO 공급자 {{provider}}은(는) 지원되지 않습니다.", - "stage-file-location-message": "쿼리 로그를 처리하기 전에 저장할 임시 파일 이름입니다. 절대 경로를 입력하세요.", - "star-on-github-description": "개발자 여러분, OpenMetadata의 오픈 소스 활동을 강화해 봅시다! 🚀 여러분의 별 표시는 OpenMetadata를 최고의 메타데이터 플랫폼으로 도약시키는 데 큰 도움이 됩니다. 소문을 퍼뜨리고, 함께해 주세요! 🌟", - "still-running-into-issue": "문제가 계속된다면 슬랙으로 문의해 주세요.", - "success-status-for-entity-deploy": "<0>{{entity}}이(가) {{entityStatus}}되었으며 성공적으로 배포되었습니다.", - "successfully-completed-the-tour": "투어를 성공적으로 완료했습니다.", - "synonym-placeholder": "동의어를 추가하려면 입력 후 Enter 키를 누르세요.", - "system-tag-delete-disable-message": "시스템에서 생성된 태그는 삭제할 수 없습니다. 대신 태그를 비활성화해 보세요.", - "tag-update-confirmation": "태그 업데이트를 진행하시겠습니까?", - "take-quick-product-tour": "시작하려면 제품 투어를 진행해 보세요!", - "team-moved-success": "팀이 성공적으로 이동되었습니다!", - "team-no-asset": "귀하의 팀에는 자산이 없습니다.", - "test-case-schedule-description": "데이터 품질 테스트는 원하는 빈도로 예약 실행할 수 있습니다. 타임존은 UTC입니다.", - "test-connection-cannot-be-triggered": "연결 테스트를 실행할 수 없습니다.", - "test-connection-taking-too-long": "연결 테스트에 너무 오랜 시간이 소요되고 있습니다. 다시 시도하세요.", - "test-your-connection-before-creating-service": "서비스 생성 전에 연결 테스트를 진행하세요.", - "testing-your-connection-may-take-two-minutes": "연결 테스트에는 최대 2분이 소요될 수 있습니다.", - "this-action-is-not-allowed-for-deleted-entities": "삭제된 엔터티에 대해서는 이 작업을 수행할 수 없습니다.", - "this-will-pick-in-next-run": "다음 실행 시 반영됩니다.", - "thread-count-message": "메트릭 계산 시 사용할 스레드 수를 설정하세요. 입력하지 않으면 기본값 5가 사용됩니다.", - "to-add-new-line": "새 줄을 추가하려면", - "token-has-no-expiry": "이 토큰은 만료 날짜가 없습니다.", - "token-security-description": "JWT 토큰을 가진 사람은 OpenMetadata 서버에 REST API 요청을 보낼 수 있습니다. JWT 토큰을 애플리케이션 코드에 노출하거나 GitHub 등 공개 장소에 공유하지 마세요.", - "total-entity-insight": "유형별 최신 데이터 자산 수를 표시합니다.", - "tour-follow-step": "데이터 자산을 팔로우하여 해당 자산의 변경 사항을 실시간으로 확인하세요. 활동 피드에 팔로우 중인 자산의 모든 변경 사항이 표시되며, 데이터 품질 문제 발생 시 알림도 받게 됩니다.", - "tour-high-level-assets-information-step": "데이터 자산 상세 페이지에서 자산의 제목, 설명, 소유자, 티어, 사용량, 위치 등 모든 맥락을 360도 뷰로 확인하여 최적의 활용 방안을 모색하세요.", - "tour-owner-step": "여기서 데이터 자산의 소유자를 팀 또는 개인으로 설정할 수 있습니다. 데이터 소유자와 협업하며, 데이터를 이해하고 누락된 설명을 요청하거나 변경을 제안할 수 있습니다.", - "tour-step-activity-feed": "<0>{{text}}를 통해 조직 내 데이터 변화 현황을 파악하세요.", - "tour-step-click-on-entity-tab": "<0>\"{{text}}\" 탭을 클릭하세요.", - "tour-step-click-on-link-to-view-more": "자세한 정보를 보려면 <0>자산의 제목을 클릭하세요.", - "tour-step-discover-all-assets-at-one-place": "<0>{{text}}와 같은 중앙 집중식 메타데이터 저장소를 통해 모든 데이터 자산을 한 곳에서 확인하고, 팀과 협업하여 조직의 전체 데이터를 파악하세요.", - "tour-step-discover-data-assets-with-data-profile": "<0>{{text}}를 통해 자산을 확인하고, 테이블 사용 통계, 널 값, 중복, 열 데이터 분포 등을 파악하세요.", - "tour-step-explore-summary-asset": "<0>\"{{text}}\" 페이지에서 각 자산의 제목, 설명, 소유자, 티어(중요도), 사용량, 위치 등의 요약 정보를 확인하세요.", - "tour-step-get-to-know-table-schema": "테이블 <0>스키마를 확인하여 열 이름, 데이터 유형, 열 설명 및 태그를 파악하고, 구조체와 같은 복잡한 유형의 메타데이터도 확인하세요.", - "tour-step-look-at-sample-data": "<0>{{text}}를 살펴보며 테이블에 포함된 데이터를 확인하고 활용 방안을 모색하세요.", - "tour-step-search-for-matching-dataset": "검색 상자에 '이름', '설명', '열 이름' 등을 입력하여 일치하는 데이터 자산을 찾으세요: <0>{{text}}", - "tour-step-trace-path-across-tables": "<0>{{text}}를 사용하여 테이블, 파이프라인, 대시보드 간의 데이터 경로를 추적하세요.", - "tour-step-type-search-term": "검색 상자에 <0>\"{{text}}\"를 입력하고 <0>{{enterText}}를 누르세요.", - "try-adjusting-filter": "원하는 항목을 찾기 위해 검색어나 필터를 조정해 보세요.", - "try-different-time-period-filtering": "결과가 없습니다. 다른 기간으로 필터링해 보세요.", - "try-extending-time-frame": "결과를 확인하려면 시간 범위를 확장해 보세요.", - "type-delete-to-confirm": "확인을 위해 <0>DELETE를 입력하세요.", - "unable-to-connect-to-your-dbt-cloud-instance": "dbt 클라우드 인스턴스에 연결할 URL입니다. 예: \n https://cloud.getdbt.com 또는 https://emea.dbt.com/", - "unable-to-error-elasticsearch": "엔터티 인덱스를 위해 Elasticsearch에서 {{error}}할 수 없습니다.", - "uninstall-app": "이 {{app}} 애플리케이션을 제거하면 OpenMetadata에서 삭제됩니다.", - "unix-epoch-time-in-ms": "{{prefix}} 밀리초 단위의 Unix 에포크 시간", - "update-description-message": "설명을 업데이트하라는 요청:", - "update-displayName-entity": "{{entity}}의 표시 이름을 업데이트하세요.", - "update-profiler-settings": "프로파일러 설정을 업데이트하세요.", - "update-tag-message": "태그 업데이트 요청:", - "updating-existing-not-possible-can-add-new-values": "기존 값을 업데이트하는 것은 불가능하며, 새로운 값 추가만 허용됩니다.", - "upload-file": "파일 업로드", - "upstream-depth-message": "업스트림 깊이에 대한 값을 선택하세요.", - "upstream-depth-tooltip": "출처(상위 레벨)를 식별하기 위해 최대 3개의 업스트림 노드를 표시합니다.", - "usage-ingestion-description": "메타데이터 수집 설정 후 사용량 수집을 구성 및 배포할 수 있습니다. 사용량 수집 워크플로우는 기본 데이터베이스에서 쿼리 로그와 테이블 생성 세부 정보를 가져와 OpenMetadata로 전송합니다. 한 데이터베이스 서비스에 대해 메타데이터 및 사용량 수집 파이프라인은 하나만 설정할 수 있습니다. 시작하려면 쿼리 로그 기간(일), 스테이지 파일 위치, 결과 제한을 정의하세요.", - "use-fqn-for-filtering-message": "정규식은 원시 이름(예: table_name) 대신 전체 이름(예: service_name.db_name.schema_name.table_name)에 적용됩니다.", - "user-assign-new-task": "{{user}}이(가) 귀하에게 새 작업을 할당했습니다.", - "user-mentioned-in-comment": "{{user}}이(가) 댓글에서 귀하를 언급했습니다.", - "user-verified-successfully": "사용자 인증이 성공적으로 완료되었습니다.", - "valid-url-endpoint": "엔드포인트는 유효한 URL이어야 합니다.", - "validation-error-assets": "추가 중인 모든 자산을 확인하세요.", - "value-should-equal-to-value": "값은 {{value}}와 같아야 합니다.", - "value-should-not-equal-to-value": "값은 {{value}}와 같으면 안 됩니다.", - "version-released-try-now": "{{version}} 버전이 출시되었습니다. <0>새 소식을 확인하세요!", - "view-deleted-entity": "이 {{parent}}에 속한 모든 삭제된 {{entity}}을(를) 확인하세요.", - "view-sample-data-entity": "샘플 데이터를 보려면 {{entity}}을(를) 실행하세요. 스케줄 설정 방법은 <0>{{entity}} 문서를 참조하세요.", - "view-test-suite": "테스트 스위트 보기", - "viewing-older-version": "이전 버전을 보고 있습니다.\n최신 버전으로 돌아가 세부 정보를 업데이트하세요.", - "webhook-listing-message": "웹훅을 사용하면 API를 통해 조직 내 메타데이터 변경 이벤트에 대해 외부 서비스에 알림을 보낼 수 있습니다. 웹훅 통합으로 콜백 URL을 등록하여 메타데이터 이벤트 알림을 받으세요. 웹훅을 추가, 목록 확인, 업데이트 및 삭제할 수 있습니다.", - "webhook-type-listing-message": "{{webhookType}} 알림을 통해 메타데이터 생산자와 소비자에게 시기적절한 업데이트를 제공하세요. {{webhookType}} 웹훅을 사용하여 조직 내 메타데이터 변경 이벤트에 대해 알림을 전송할 수 있습니다. 이러한 웹훅을 추가, 목록 확인, 업데이트 및 삭제할 수 있습니다.", - "welcome-screen-message": "모든 데이터를 한 곳에서 확인하고, 신뢰할 수 있는 데이터를 바탕으로 팀과 원활하게 협업하세요.", - "welcome-to-om": "OpenMetadata에 오신 것을 환영합니다!", - "welcome-to-open-metadata": "OpenMetadata에 오신 것을 환영합니다!", - "would-like-to-start-adding-some": "데이터를 추가해 보시겠습니까?", - "write-your-announcement-lowercase": "공지사항을 작성하세요", - "write-your-description": "설명을 작성하세요", - "write-your-text": "{{text}}을(를) 작성하세요", - "you-can-also-set-up-the-metadata-ingestion": "메타데이터 수집도 설정할 수 있습니다." - }, - "server": { - "account-verify-success": "이메일 인증이 성공적으로 완료되었습니다", - "add-entity-error": "{{entity}} 추가 중 오류 발생!", - "auth-provider-not-supported-renewing": "토큰 갱신을 위한 인증 공급자 {{provider}}은(는) 지원되지 않습니다.", - "can-not-renew-token-authentication-not-present": "id 토큰을 갱신할 수 없습니다. 인증 공급자가 없습니다.", - "column-fetch-error": "열 테스트 케이스를 가져오는 중 오류 발생!", - "connection-tested-successfully": "연결 테스트가 성공적으로 완료되었습니다", - "create-entity-error": "{{entity}} 생성 중 오류 발생!", - "create-entity-success": "{{entity}}이(가) 성공적으로 생성되었습니다.", - "delete-entity-error": "\"{{entity}}\" 삭제 중 오류 발생.", - "deploy-entity-error": "{{entity}} 배포 중 오류 발생!", - "email-already-exist": "\"{{name}}\" 이름의 {{entity}}이(가) 이미 존재합니다. 다른 이메일을 선택하세요.", - "email-confirmation": "이메일을 확인해 주세요. 확인 메일이 전송되었습니다.", - "email-found": "해당 이메일 주소를 가진 사용자가 이미 존재합니다!", - "email-not-found": "해당 이메일 주소를 가진 사용자가 존재하지 않습니다!", - "email-verification-error": "이메일 인증 메일 전송 중 문제가 발생했습니다. 관리자에게 문의하세요.", - "entity-already-exist": "\"{{name}}\" 이름의 {{entity}}이(가) 이미 존재합니다. 중복 {{entityPlural}}은 허용되지 않습니다.", - "entity-already-exist-message-without-name": "입력한 정보로 {{entity}}이(가) 이미 존재합니다. 중복 {{entityPlural}}은 허용되지 않습니다.", - "entity-creation-error": "{{entity}} 생성 중 오류 발생", - "entity-deleted-successfully": "\"{{entity}}\"이(가) 성공적으로 삭제되었습니다!", - "entity-details-fetch-error": "{{entityType}} {{entityName}} 상세 정보 가져오는 중 오류 발생", - "entity-feed-fetch-error": "엔터티 피드 수를 가져오는 중 오류 발생!", - "entity-fetch-error": "{{entity}} 가져오는 중 오류 발생", - "entity-fetch-version-error": "{{version}} 버전의 {{entity}} 가져오는 중 오류 발생", - "entity-follow-error": "{{entity}} 팔로우 중 오류 발생", - "entity-limit-reached": "{{entity}} 제한에 도달했습니다", - "entity-removing-error": "{{entity}} 제거 중 오류 발생", - "entity-unfollow-error": "{{entity}} 언팔로우 중 오류 발생", - "entity-updating-error": "{{entity}} 업데이트 중 오류 발생", - "error-selected-node-name-details": "{{selectedNodeName}} 상세 정보 가져오는 중 오류 발생", - "error-while-renewing-id-token-with-message": "Auth0 SSO에서 id 토큰 갱신 중 오류 발생: {{message}}", - "feed-post-error": "메시지 게시 중 오류 발생!", - "fetch-entity-permissions-error": "{{entity}}에 대한 권한을 가져올 수 없습니다.", - "fetch-re-index-data-error": "재인덱스 데이터를 가져오는 중 오류 발생!", - "fetch-table-profiler-config-error": "테이블 프로파일러 구성을 가져오는 중 오류 발생!", - "fetch-updated-conversation-error": "업데이트된 대화 가져오는 중 오류 발생!", - "forgot-password-email-error": "이메일 전송 중 문제가 발생했습니다. 관리자에게 문의하세요.", - "indexing-error": "인덱싱 중 오류 발생", - "ingestion-workflow-operation-error": "{{displayName}} 수집 워크플로우 {{operation}} 중 오류 발생", - "invalid-username-or-password": "잘못된 사용자명 또는 비밀번호입니다.", - "join-team-error": "팀 가입 중 오류 발생!", - "join-team-success": "팀에 성공적으로 가입했습니다!", - "leave-team-error": "팀 탈퇴 중 오류 발생!", - "leave-team-success": "팀에서 성공적으로 탈퇴했습니다!", - "no-owned-entities": "아직 소유한 항목이 없습니다.", - "no-query-available": "사용 가능한 쿼리가 없습니다.", - "no-task-available": "작업 데이터가 없습니다.", - "no-task-creation-without-assignee": "담당자 없이 작업을 생성할 수 없습니다", - "page-layout-operation-error": "페이지 레이아웃 {{operation}} 중 오류 발생.", - "page-layout-operation-success": "페이지 레이아웃이 {{operation}}되었습니다.", - "please-add-description": "빈 설명은 허용되지 않습니다. 설명을 추가하세요.", - "please-add-tags": "빈 태그 목록은 허용되지 않습니다. 태그를 추가하세요.", - "re-indexing-error": "재인덱싱 중 오류 발생!", - "re-indexing-started": "재인덱싱 시작됨", - "re-indexing-stopped": "재인덱싱 중지됨", - "reset-password-success": "비밀번호가 성공적으로 재설정되었습니다!", - "stop-re-indexing-error": "재인덱싱 중지 시 오류가 발생했습니다!", - "task-closed-successfully": "작업이 성공적으로 닫혔습니다.", - "task-closed-without-comment": "댓글 없이 작업을 닫을 수 없습니다.", - "task-resolved-successfully": "작업이 성공적으로 해결되었습니다", - "team-moved-error": "팀 이동 중 오류 발생", - "test-connection-error": "연결 테스트 중 오류 발생!", - "unauthorized-user": "권한이 없는 사용자입니다! 이메일 또는 비밀번호를 확인하세요.", - "unexpected-error": "예기치 않은 오류가 발생했습니다.", - "unexpected-response": "서버로부터 예기치 않은 응답이 있었습니다.", - "update-entity-success": "{{entity}}이(가) 성공적으로 업데이트되었습니다.", - "you-have-not-action-anything-yet": "아직 아무 것도 {{action}}하지 않았습니다." - } + "label": { + "aborted": "중단됨", + "accept": "수락", + "accept-all": "전체 수락", + "accept-suggestion": "제안 수락", + "access": "접근", + "access-block-time": "접근 차단 시간", + "access-control": "접근 제어", + "access-token": "접근 토큰", + "accessed": "접근됨", + "account": "계정", + "account-email": "계정 이메일", + "account-name": "계정 이름", + "acknowledged": "인정됨", + "action": "작업", + "action-plural": "작업들", + "action-required": "Action Required", + "active": "활성", + "active-uppercase": "ACTIVE", + "active-user": "활성 사용자", + "active-with-error": "오류와 함께 활성", + "activity": "활동", + "activity-feed": "활동 피드", + "activity-feed-and-task-plural": "활동 피드 및 작업", + "activity-feed-plural": "활동 피드들", + "activity-lowercase": "활동", + "activity-lowercase-plural": "활동들", + "add": "추가", + "add-a-new-service": "새 서비스 추가", + "add-an-image": "이미지 추가", + "add-column": "Add Column", + "add-custom-entity-property": "맞춤형 {{entity}} 속성 추가", + "add-deploy": "추가 및 배포", + "add-entity": "{{entity}} 추가", + "add-entity-metric": "{{entity}} 메트릭 추가", + "add-entity-test": "{{entity}} 테스트 추가", + "add-new-entity": "새로운 {{entity}} 추가", + "add-row": "Add Row", + "add-suggestion": "제안 추가", + "add-workflow-ingestion": "{{workflow}} 수집 추가", + "added": "추가됨", + "added-entity": "추가된 {{entity}}", + "added-lowercase": "추가됨", + "added-yet-lowercase": "아직 추가되지 않음.", + "adding-new-classification": "새 분류 추가 중", + "adding-new-tag": "{{categoryName}}에 새 태그 추가 중", + "address": "주소", + "admin": "관리자", + "admin-plural": "관리자들", + "admin-profile": "관리자 프로필", + "advanced": "고급", + "advanced-config": "고급 구성", + "advanced-configuration": "고급 설정", + "advanced-entity": "고급 {{entity}}", + "advanced-search": "고급 검색", + "aggregate": "집계", + "airflow-config-plural": "airflow 구성들", + "alert": "경고", + "alert-detail-plural": "Alert Details", + "alert-lowercase": "경고", + "alert-lowercase-plural": "경고들", + "alert-plural": "경고들", + "alert-type": "경고 유형", + "algorithm": "알고리즘", + "all": "전체", + "all-activity": "모든 활동", + "all-data-asset-plural": "모든 데이터 자산", + "all-domain-plural": "모든 도메인", + "all-entity": "모든 {{entity}}", + "all-lowercase": "전체", + "all-threads": "모든 스레드", + "and-lowercase": "그리고", + "announcement": "공지", + "announcement-lowercase": "공지", + "announcement-on-entity": "{{entity}}에 대한 공지", + "announcement-plural": "공지사항", + "announcement-title": "공지 제목", + "api-collection": "API 컬렉션", + "api-collection-plural": "API 컬렉션들", + "api-endpoint": "API 엔드포인트", + "api-endpoint-plural": "API 엔드포인트들", + "api-uppercase": "API", + "api-uppercase-plural": "API들", + "app-analytic-plural": "앱 분석들", + "app-lowercase": "앱", + "app-plural": "앱들", + "application": "애플리케이션", + "application-by-developer": "<0>{{dev}}의 {{app}}", + "application-plural": "애플리케이션들", + "applied-advanced-search": "적용된 고급 검색", + "apply": "적용", + "approve": "승인", + "approved": "승인됨", + "april": "4월", + "argument-plural": "인수들", + "as-lowercase": "처럼", + "asset": "자산", + "asset-lowercase": "자산", + "asset-plural": "자산들", + "assigned": "할당됨", + "assigned-entity": "할당된 {{entity}}", + "assigned-to-me": "나에게 할당됨", + "assignee": "담당자", + "assignee-plural": "담당자들", + "assume-role-arn": "Assume Role의 Role Arn", + "assume-role-session-name": "Assume Role의 Role Session Name", + "assume-role-source-identity": "Assume Role의 Source Identity", + "attention": "주의", + "audience": "청중", + "august": "8월", + "auth-config-lowercase-plural": "인증 구성들", + "auth-mechanism": "인증 메커니즘", + "auth-x509-certificate-url": "인증 공급자 x509 인증서 URL", + "auth0": "Auth0", + "authentication-uri": "인증 URI", + "authority": "권한", + "authorize-app": "{{app}} 승인", + "auto-classification": "Auto Classification", + "auto-pii-confidence-score": "자동 PII 신뢰도 점수", + "auto-tag-pii-uppercase": "자동 PII 태그", + "automatically-generate": "자동 생성", + "average-daily-active-users-on-the-platform": "Average Daily Active Users on the Platform", + "average-session": "평균 세션 시간", + "awaiting-status": "대기 중 상태", + "aws-access-key-id": "AWS 접근 키 ID", + "aws-region": "AWS 리전", + "aws-secret-access-key": "AWS 비밀 접근 키", + "aws-session-token": "AWS 세션 토큰", + "azure": "Azure", + "azure-config-source": "Azure 구성 소스", + "back": "뒤로", + "back-to-login-lowercase": "로그인 페이지로 돌아가기", + "basic-configuration": "기본 구성", + "batch-size": "배치 크기", + "before-number-of-day-plural": "{{numberOfDays}}일 전", + "beta": "베타", + "bot": "봇", + "bot-detail": "봇 상세 정보", + "bot-lowercase": "봇", + "bot-plural": "봇들", + "broker-plural": "브로커들", + "browse": "찾아보기", + "browse-app-plural": "앱 찾아보기", + "browse-csv-file": "CSV 파일 찾아보기", + "by-entity": "{{entity}} 기준", + "by-lowercase": "의", + "ca-certs": "CA 인증서", + "cancel": "취소", + "category": "카테고리", + "category-plural": "카테고리들", + "certification": "Certification", + "change-entity": "{{entity}} 변경", + "change-log-plural": "Change Logs", + "change-parent-entity": "상위 {{entity}} 변경", + "change-password": "Change Password", + "chart": "차트", + "chart-entity": "{{entity}} 차트", + "chart-plural": "차트들", + "chart-type": "차트 유형", + "check-active-data-quality-incident-plural": "Check active data quality incidents", + "check-status": "상태 확인", + "check-upstream-failure": "Check Upstream Failure", + "children": "자식", + "children-lowercase": "자식", + "claim-ownership": "소유권 주장", + "classification": "분류", + "classification-lowercase": "분류", + "classification-lowercase-plural": "분류들", + "classification-plural": "분류들", + "clean-up-policy-plural": "CleanUp Policies", + "clean-up-policy-plural-lowercase": "정리 정책들", + "clear": "명확히", + "clear-entity": "{{entity}} 제거", + "click-here": "여기를 클릭", + "client-email": "클라이언트 이메일", + "client-id": "클라이언트 ID", + "client-secret": "클라이언트 비밀", + "client-x509-certificate-url": "클라이언트 x509 인증서 URL", + "close": "닫기", + "close-with-comment": "댓글과 함께 닫기", + "closed": "닫힘", + "closed-lowercase": "닫힘", + "closed-task-plural": "닫힌 작업들", + "closed-this-task-lowercase": "이 작업 닫음", + "cloud-config-source": "클라우드 구성 소스", + "cluster": "Cluster", + "code": "코드", + "collapse": "접기", + "collapse-all": "전체 접기", + "collection": "컬렉션", + "collection-plural": "컬렉션들", + "color": "색상", + "column": "열", + "column-description": "Column Description", + "column-entity": "{{entity}} 열", + "column-level-lineage": "Column Level Lineage", + "column-lowercase": "열", + "column-lowercase-plural": "열들", + "column-plural": "열들", + "column-profile": "열 프로파일", + "comment": "댓글", + "comment-lowercase": "댓글", + "comment-plural": "Comments", + "complete": "완료", + "completed": "완료됨", + "completed-entity": "{{entity}} 완료됨", + "compute-row-count": "행 수 계산", + "condition": "조건", + "config": "구성", + "configuration": "설정", + "configure": "구성", + "configure-a-service": "서비스 구성", + "configure-dbt-model": "dbt 모델 구성", + "configure-entity": "{{entity}} 구성", + "confirm": "확인", + "confirm-lowercase": "확인", + "confirm-new-password": "새 비밀번호 확인", + "confirm-password": "비밀번호 확인", + "connection": "연결", + "connection-details": "연결 세부 정보", + "connection-entity": "{{entity}} 연결", + "connection-status": "연결 상태", + "connection-timeout": "연결 시간 초과", + "connection-timeout-plural": "연결 시간 초과", + "connector": "커넥터", + "constraint": "제약", + "constraint-plural": "Constraints", + "constraint-type": "Constraint Type", + "consumer-aligned": "소비자 맞춤", + "container": "컨테이너", + "container-column": "컨테이너 열", + "container-plural": "컨테이너들", + "conversation": "대화", + "conversation-lowercase": "대화", + "conversation-plural": "대화들", + "copied": "복사됨", + "copy": "복사", + "cost-analysis": "비용 분석", + "count": "수", + "covered": "Covered", + "create": "생성", + "create-entity": "{{entity}} 생성", + "create-new-test-suite": "새 테스트 스위트 생성", + "created-a-task-lowercase": "작업 생성됨", + "created-by": "생성자", + "created-by-me": "내가 생성함", + "created-date": "생성 날짜", + "created-lowercase": "생성됨", + "creating-account": "계정 생성 중", + "creating-lowercase": "생성 중", + "credentials-type": "자격 증명 유형", + "criteria": "기준", + "cron": "크론", + "current-version": "Current Version", + "custom": "맞춤", + "custom-attribute-plural": "맞춤 속성들", + "custom-entity": "맞춤 {{entity}}", + "custom-logo": "맞춤 로고", + "custom-logo-configuration": "맞춤 로고 구성", + "custom-metric": "맞춤 메트릭", + "custom-oidc": "CustomOidc", + "custom-property": "맞춤 속성", + "custom-property-plural": "맞춤 속성들", + "custom-range": "맞춤 범위", + "custom-theme": "맞춤 테마", + "customise": "맞춤 설정", + "customize-entity": "{{entity}} 맞춤 설정", + "customize-ui": "Customize UI", + "dag": "Dag", + "dag-view": "DAG 보기", + "daily-active-users-on-the-platform": "플랫폼의 일일 활성 사용자 수", + "dashboard": "대시보드", + "dashboard-data-model-plural": "대시보드 데이터 모델들", + "dashboard-detail-plural-lowercase": "대시보드 상세 정보들", + "dashboard-lowercase": "대시보드", + "dashboard-lowercase-plural": "대시보드들", + "dashboard-name": "대시보드 이름", + "dashboard-plural": "대시보드들", + "data-aggregate": "데이터 집계", + "data-aggregation": "데이터 집계", + "data-asset": "데이터 자산", + "data-asset-name": "데이터 자산 이름", + "data-asset-plural": "데이터 자산들", + "data-asset-plural-coverage": "Data Assets Coverage", + "data-asset-plural-with-field": "{{field}}가 있는 데이터 자산들", + "data-asset-type": "데이터 자산 유형", + "data-assets-report": "데이터 자산 보고서", + "data-assets-with-tier-plural": "티어가 있는 데이터 자산들", + "data-collaboration": "데이터 협업", + "data-contract-plural": "데이터 계약들", + "data-count-plural": "데이터 수", + "data-discovery": "데이터 검색", + "data-distribution": "데이터 분포", + "data-entity": "데이터 {{entity}}", + "data-insight": "데이터 인사이트", + "data-insight-active-user-summary": "가장 활동적인 사용자", + "data-insight-chart": "데이터 인사이트 차트", + "data-insight-description-summary-type": "{{type}} 설명이 있는 비율", + "data-insight-ingestion": "데이터 인사이트 수집", + "data-insight-owner-summary-type": "{{type}} 소유자 비율", + "data-insight-plural": "데이터 인사이트들", + "data-insight-report": "데이터 인사이트 보고서", + "data-insight-report-alert": "데이터 인사이트 보고서 경고", + "data-insight-summary": "{{organization}} 한 눈에 본 상태", + "data-insight-tier-summary": "티어별 총 데이터 자산", + "data-insight-top-viewed-entity-summary": "가장 많이 본 데이터 자산", + "data-insight-total-entity-summary": "총 데이터 자산", + "data-model": "데이터 모델", + "data-model-column": "데이터 모델 열", + "data-model-plural": "데이터 모델들", + "data-model-type": "데이터 모델 유형", + "data-observability": "데이터 관측성", + "data-product": "데이터 제품", + "data-product-lowercase": "데이터 제품", + "data-product-plural": "데이터 제품들", + "data-profiler-metrics": "데이터 프로파일러 메트릭", + "data-proportion-plural": "데이터 비율들", + "data-quality": "데이터 품질", + "data-quality-test": "데이터 품질 테스트", + "data-quartile-plural": "데이터 사분위수들", + "data-range": "데이터 범위", + "data-type": "데이터 유형", + "data-volume": "데이터 볼륨", + "database": "데이터베이스", + "database-lowercase": "데이터베이스", + "database-name": "데이터베이스 이름", + "database-plural": "데이터베이스들", + "database-schema": "데이터베이스 스키마", + "database-schema-plural": "데이터베이스 스키마들", + "database-service-name": "데이터베이스 서비스 이름", + "date": "날짜", + "date-and-time": "날짜 & 시간", + "date-filter": "날짜 필터", + "day": "일", + "day-left": "{{day}}일 남음", + "days-change-lowercase": "{{days}}일 변화", + "dbt-bucket-name": "dbt 버킷 이름", + "dbt-catalog-file-path": "dbt 카탈로그 파일 경로", + "dbt-catalog-http-path": "dbt 카탈로그 HTTP 경로", + "dbt-classification-name": "dbt 분류 이름", + "dbt-cloud-account-auth-token": "dbt 클라우드 계정 인증 토큰", + "dbt-cloud-account-id": "dbt 클라우드 계정 ID", + "dbt-cloud-job-id": "dbt 클라우드 작업 ID", + "dbt-cloud-project-id": "dbt 클라우드 프로젝트 ID", + "dbt-cloud-url": "dbt 클라우드 URL", + "dbt-configuration-source": "dbt 구성 소스", + "dbt-configuration-source-type": "dbt 구성 소스 유형", + "dbt-ingestion": "dbt 수집", + "dbt-lowercase": "dbt", + "dbt-manifest-file-path": "dbt 매니페스트 파일 경로", + "dbt-object-prefix": "dbt 객체 접두사", + "dbt-run-result-file-path": "dbt 실행 결과 파일 경로", + "dbt-run-result-http-path": "dbt 실행 결과 HTTP 경로", + "dbt-source": "dbt 소스", + "deactivated": "비활성화됨", + "december": "12월", + "default": "기본", + "default-persona": "기본 페르소나", + "delete": "삭제", + "delete-entity": "{{entity}} 삭제", + "delete-profile": "Delete Profile", + "delete-property-name": "{{propertyName}} 속성 삭제", + "delete-tag-classification": "{{isCategory}} 태그 삭제", + "delete-uppercase": "삭제", + "deleted": "삭제됨", + "deleted-entity": "삭제된 {{entity}}", + "deleted-lowercase": "삭제됨", + "deleting-lowercase": "삭제 중", + "deploy": "배포", + "deployed": "배포됨", + "deployed-lowercase": "배포됨", + "deploying-lowercase": "배포 중", + "description": "설명", + "description-kpi": "KPI 설명", + "description-lowercase": "설명", + "description-plural": "설명들", + "destination": "대상", + "destination-plural": "Destinations", + "detail-plural": "상세 정보들", + "developed-by-developer": "{{developer}}가 개발함", + "dimension": "Dimension", + "disable": "비활성화", + "disable-lowercase": "비활성화", + "disable-tag": "태그 비활성화", + "disabled": "비활성화됨", + "discover": "발견", + "display-name": "표시 이름", + "display-name-lowercase": "표시 이름", + "dist": "Dist", + "distinct": "고유", + "doc-plural": "문서들", + "doc-plural-lowercase": "문서들", + "document": "문서", + "documentation": "문서화", + "documentation-lowercase": "문서", + "domain": "도메인", + "domain-lowercase": "도메인", + "domain-lowercase-plural": "도메인들", + "domain-plural": "도메인들", + "domain-type": "도메인 유형", + "down-vote": "다운 보트", + "downstream-depth": "다운스트림 깊이", + "duplicate": "중복", + "duration": "기간", + "dynamic-assertion": "동적 단언", + "edge": "엣지", + "edge-information": "엣지 정보", + "edge-lowercase": "엣지", + "edit": "수정", + "edit-amp-accept-suggestion": "수정 및 제안 수락", + "edit-an-announcement": "공지 수정", + "edit-chart-name": "차트 수정: \"{{name}}\"", + "edit-description-for": "{{entityName}}의 설명 수정", + "edit-entity": "수정 {{entity}}", + "edit-entity-name": "{{entityType}} 수정: \"{{entityName}}\"", + "edit-glossary-display-name": "용어집 표시 이름 수정", + "edit-glossary-name": "용어집 이름 수정", + "edit-profile": "Edit Profile", + "edit-suggestion": "Edit Suggestion", + "edit-workflow-ingestion": "{{workflow}} 수집 수정", + "edited": "수정됨", + "effect": "효과", + "elastic-search-re-index": "Elasticsearch 재인덱스", + "elasticsearch": "Elasticsearch", + "email": "이메일", + "email-configuration": "이메일 구성", + "email-configuration-lowercase": "이메일 구성", + "email-lowercase": "이메일", + "email-plural": "이메일들", + "emailing-entity": "이메일 발송 {{entity}}", + "embed-image": "이미지 삽입", + "embed-link": "링크 삽입", + "enable": "활성화", + "enable-debug-log": "디버그 로그 활성화", + "enable-lowercase": "활성화", + "enable-partition": "파티션 활성화", + "enable-roles-polices-in-search": "Enable Roles & Policies in Search", + "enable-smtp-server": "SMTP 서버 활성화", + "enabled": "활성화됨", + "end-date": "종료 날짜", + "end-date-time-zone": "종료 날짜: ({{timeZone}})", + "end-entity": "{{entity}} 종료", + "endpoint": "엔드포인트", + "endpoint-plural": "엔드포인트들", + "endpoint-url": "엔드포인트 URL", + "endpoint-url-for-aws": "AWS용 엔드포인트 URL", + "enter": "입력", + "enter-entity": "{{entity}} 입력", + "enter-entity-name": "{{entity}} 이름 입력", + "enter-entity-value": "{{entity}} 값 입력", + "enter-field-description": "{{field}} 설명 입력", + "enter-property-value": "속성 값 입력", + "enter-type-password": "{{type}} 비밀번호 입력", + "entity": "엔터티", + "entity-configuration": "{{entity}} Configuration", + "entity-count": "{{entity}} 수", + "entity-detail-plural": "{{entity}} 상세 정보들", + "entity-feed-plural": "엔터티 피드들", + "entity-hyphen-value": "{{entity}} - {{value}}", + "entity-id": "{{entity}} ID", + "entity-id-match": "ID로 일치", + "entity-index": "{{entity}} 인덱스", + "entity-key": "{{entity}} key", + "entity-key-plural": "{{entity}} keys", + "entity-lineage": "Entity Lineage", + "entity-list": "{{entity}} 목록", + "entity-name": "{{entity}} 이름", + "entity-plural": "엔터티들", + "entity-proportion": "{{entity}} 비율", + "entity-record-plural": "{{entity}} 레코드들", + "entity-reference": "엔터티 참조", + "entity-reference-plural": "엔터티 참조들", + "entity-reference-types": "엔터티 참조 유형들", + "entity-service": "{{entity}} 서비스", + "entity-type-plural": "{{entity}} 유형", + "entity-version": "{{entity}} version", + "entity-version-detail-plural": "{{entity}} 버전 상세 정보들", + "enum-value-plural": "열거형 값들", + "equation": "방정식", + "error": "오류", + "error-plural": "오류들", + "event-plural": "Events", + "event-publisher-plural": "이벤트 발행자들", + "event-type": "이벤트 유형", + "event-type-lowercase": "이벤트 유형", + "every": "모든", + "exclude": "제외", + "execution-date": "실행 날짜", + "execution-plural": "실행들", + "execution-time": "실행 시간", + "exit-fit-to-screen": "화면 맞춤 종료", + "exit-version-history": "버전 기록 종료", + "expand": "확장", + "expand-all": "전체 확장", + "expert-lowercase": "전문가", + "expert-plural": "전문가들", + "explore": "탐색", + "explore-asset-plural-with-type": "{{type}} 자산 탐색", + "explore-data": "데이터 탐색", + "explore-now": "지금 탐색", + "export": "내보내기", + "export-entity": "{{entity}} 내보내기", + "expression": "Expression", + "extend-open-meta-data": "OpenMetadata 확장", + "extension": "확장", + "external": "외부", + "failed": "실패함", + "failed-entity": "Failed {{entity}}", + "failing-subscription-id": "Failing Subscription Id", + "failure-comment": "실패 댓글", + "failure-context": "실패 상황", + "failure-plural": "실패들", + "failure-reason": "실패 이유", + "favicon-url": "파비콘 URL", + "feature": "기능", + "feature-lowercase": "기능", + "feature-plural": "기능들", + "feature-plural-used": "사용된 기능들", + "february": "2월", + "feed-filter-plural": "피드 필터들", + "feed-lowercase": "피드", + "feed-plural": "피드들", + "field": "필드", + "field-change": "필드 변경", + "field-entity": "{{field}} {{entity}}", + "field-invalid": "{{field}}이(가) 유효하지 않음", + "field-plural": "필드들", + "field-required": "{{field}}이(가) 필요함", + "field-required-plural": "{{field}}들이 필요함", + "file": "파일", + "filter": "필터", + "filter-pattern": "필터 패턴", + "filter-plural": "필터들", + "filtered": "필터됨", + "filtering-condition": "필터링 조건", + "first": "첫 번째", + "first-lowercase": "첫 번째", + "first-quartile": "첫 사분위", + "fit-to-screen": "화면 맞춤", + "flush-interval-secs": "플러시 간격(초)", + "follow": "팔로우", + "followed-lowercase": "팔로우됨", + "follower-plural": "팔로워들", + "followers-of-entity-name": "{{entityName}}의 팔로워들", + "following": "팔로잉", + "for-lowercase": "위한", + "foreign": "Foreign", + "foreign-key": "외래 키", + "forgot-password": "비밀번호 찾기", + "format": "형식", + "fqn-uppercase": "FQN", + "frequently-joined-column-plural": "자주 조인된 열들", + "frequently-joined-table-plural": "자주 조인된 테이블들", + "friday": "금요일", + "from-lowercase": "부터", + "full-name": "전체 이름", + "full-screen": "전체 화면", + "function": "함수", + "g-chat": "G 채팅", + "gcs-config": "GCS 구성", + "gcs-config-source": "GCS 구성 소스", + "gcs-credential-path": "GCS 자격증명 경로", + "gcs-credential-value": "GCS 자격증명 값들", + "generate": "생성", + "generate-new-token": "새 토큰 생성", + "generated-by": "생성됨", + "get-app-support": "앱 지원 받기", + "glossary": "용어집", + "glossary-lowercase": "용어집", + "glossary-lowercase-plural": "용어집들", + "glossary-name": "용어집 이름", + "glossary-plural": "용어집들", + "glossary-term": "용어", + "glossary-term-lowercase": "용어", + "glossary-term-lowercase-plural": "용어들", + "glossary-term-plural": "용어들", + "go-back": "돌아가기", + "go-to-home-page": "홈페이지로 이동", + "google": "구글", + "google-account-service-type": "Google Cloud 서비스 계정 유형.", + "google-client-id": "Google Cloud 클라이언트 ID.", + "google-cloud-auth-provider": "Google Cloud 인증 공급자 인증서.", + "google-cloud-auth-uri": "Google Cloud 인증 URI.", + "google-cloud-client-certificate-uri": "Google Cloud 클라이언트 인증서 URI.", + "google-cloud-email": "Google Cloud 이메일.", + "google-cloud-private-key": "Google Cloud 개인 키.", + "google-cloud-private-key-id": "Google Cloud 개인 키 ID.", + "google-cloud-project-id": "Google Cloud 프로젝트 ID.", + "google-cloud-token-uri": "Google Cloud 토큰 URI.", + "govern": "거버넌스", + "governance": "거버넌스", + "granularity": "Granularity", + "group": "그룹", + "has-been-action-type-lowercase": "이미 {{actionType}}됨", + "header-plural": "Headers", + "health-check": "헬스 체크", + "health-lowercase": "health", + "healthy": "Healthy", + "healthy-data-asset-plural": "Healthy Data Assets", + "help": "도움말", + "here-lowercase": "여기", + "hide": "숨기기", + "hide-deleted-entity": "삭제된 {{entity}} 숨기기", + "history": "히스토리", + "home": "홈", + "homepage": "Homepage", + "hour": "시", + "http-config-source": "HTTP 구성 소스", + "http-method": "HTTP Method", + "hyper-parameter-plural": "하이퍼 파라미터들", + "icon-url": "아이콘 URL", + "image-repository": "이미지 저장소", + "import": "가져오기", + "import-entity": "{{entity}} 가져오기", + "in-lowercase": "안에", + "in-open-metadata": "OpenMetadata 내에서", + "in-review": "검토 중", + "inactive-announcement-plural": "비활성 공지사항들", + "incident": "인시던트", + "incident-manager": "인시던트 관리자", + "incident-plural": "Incidents", + "incident-status": "인시던트 상태", + "include": "포함", + "include-entity": "{{entity}} 포함", + "include-owner": "소유자 포함", + "incomplete": "불완전함", + "index-states": "인덱스 상태", + "ingest-sample-data": "샘플 데이터 수집", + "ingestion": "수집", + "ingestion-lowercase": "수집", + "ingestion-pipeline": "수집 파이프라인", + "ingestion-pipeline-name": "수집 파이프라인 이름", + "ingestion-plural": "수집", + "ingestion-workflow-lowercase": "수집 워크플로우", + "inherited-entity": "상속된 {{entity}}", + "inherited-role-plural": "상속된 역할들", + "insert": "삽입", + "insight-plural": "인사이트들", + "install": "설치", + "install-airflow-api": "Airflow 관리 API 설치", + "install-service-connectors": "서비스 커넥터 설치", + "installed": "설치됨", + "installed-lowercase": "설치됨", + "instance-lowercase": "인스턴스", + "integration-plural": "통합", + "inter-quartile-range": "사분위 범위", + "internal": "내부", + "interval": "간격", + "interval-type": "간격 유형", + "interval-unit": "간격 단위", + "invalid-condition": "유효하지 않은 조건", + "invalid-name": "유효하지 않은 이름", + "is": "Is", + "is-not": "Is Not", + "is-not-set": "Is Not Set", + "is-ready-for-preview": "미리보기를 위한 준비 완료", + "is-set": "Is Set", + "issue": "Issue", + "issue-plural": "이슈들", + "items-selected-lowercase": "선택된 항목들", + "january": "1월", + "job-lowercase": "작업", + "join": "가입", + "join-entity": "가입 <0>{{entity}}", + "join-team": "팀 가입", + "joinable": "가입 가능", + "json-data": "JSON 데이터", + "july": "7월", + "jump-to-end": "끝으로 이동", + "june": "6월", + "jwt-token-expiry-time": "JWT 토큰 만료 시간", + "jwt-uppercase": "JWT", + "key": "Key", + "keyword-lowercase-plural": "키워드들", + "kill": "종료", + "kpi-display-name": "KPI 표시 이름", + "kpi-list": "KPI 목록", + "kpi-name": "KPI 이름", + "kpi-title": "핵심 성과 지표 (KPI)", + "kpi-uppercase": "KPI", + "kpi-uppercase-plural": "KPIs", + "landing-page": "랜딩 페이지", + "language": "언어", + "large": "대형", + "last": "마지막", + "last-error": "마지막 오류", + "last-failed-at": "마지막 실패 시간", + "last-lowercase": "마지막", + "last-name-lowercase": "성", + "last-no-of-day-plural": "최근 {{day}}일", + "last-number-of-days": "최근 {{numberOfDays}}일", + "last-run": "마지막 실행", + "last-run-result": "마지막 실행 결과", + "last-updated": "최근 업데이트", + "latest": "최신", + "layer": "Layer", + "layer-plural": "레이어들", + "learn-more": "자세히 알아보기", + "learn-more-and-support": "자세히 알아보기 및 지원", + "leave-team": "팀 나가기", + "less": "더 적게", + "less-lowercase": "더 적게", + "line": "선", + "line-plural": "선들", + "lineage": "계보", + "lineage-config": "계보 구성", + "lineage-data-lowercase": "계보 데이터", + "lineage-ingestion": "계보 수집", + "lineage-layer": "Lineage Layer", + "lineage-lowercase": "계보", + "lineage-node-lowercase": "계보 노드", + "lineage-source": "계보 출처", + "link": "링크", + "list": "목록", + "list-entity": "{{entity}} 목록", + "live": "실시간", + "load-more": "더 불러오기", + "loading": "불러오는 중", + "local-config-source": "로컬 구성 소스", + "location": "위치", + "log-plural": "로그들", + "log-viewer": "로그 뷰어", + "logged-in-user-lowercase": "로그인한 사용자", + "login": "로그인", + "login-configuration": "로그인 구성", + "logo-url": "로고 URL", + "logout": "로그아웃", + "machine-learning": "머신 러닝", + "major": "주요", + "manage-entity": "관리 {{entity}}", + "manage-rule": "규칙 관리", + "mandatory": "필수", + "many-to-many": "Many to Many", + "many-to-one": "Many to One", + "march": "3월", + "mark-all-deleted-table-plural": "모든 삭제된 테이블 표시", + "mark-deleted-entity": "삭제된 {{entity}} 표시", + "mark-deleted-table-plural": "삭제된 테이블 표시", + "markdown-guide": "Markdown 가이드", + "market-place": "마켓플레이스", + "matches": "일치", + "matrix": "매트릭스", + "max": "최대", + "max-login-fail-attempt-plural": "최대 로그인 실패 시도", + "max-message-size": "Max Message Size", + "maximum-size-lowercase": "최대 크기", + "may": "5월", + "mean": "평균", + "median": "중앙값", + "medium": "중간", + "member-plural": "멤버들", + "mention-plural": "멘션들", + "message-lowercase": "메시지", + "message-lowercase-plural": "메시지들", + "message-plural-lowercase": "메시지들", + "message-schema": "메시지 스키마", + "messaging": "메시징", + "messaging-lowercase": "메시징", + "messaging-plural": "메시징들", + "metadata": "메타데이터", + "metadata-ingestion": "메타데이터 수집", + "metadata-lowercase": "메타데이터", + "metadata-plural": "메타데이터들", + "metadata-to-es-config-optional": "메타데이터 to ES 구성 (옵션)", + "metapilot": "메타파일럿", + "metapilot-suggested-description": "메타파일럿 제안 설명", + "metric": "Metric", + "metric-configuration": "메트릭 구성", + "metric-plural": "Metrics", + "metric-type": "메트릭 유형", + "metric-value": "메트릭 값", + "metrics-summary": "메트릭 요약", + "middot-symbol": "·", + "min": "최소", + "minor": "사소한", + "minute": "분", + "minute-lowercase": "분", + "minute-plural": "분", + "ml-feature-plural": "ML 기능들", + "ml-model": "ML 모델", + "ml-model-lowercase": "ML 모델", + "ml-model-lowercase-plural": "ML 모델들", + "ml-model-plural": "ML 모델들", + "mode": "모드", + "model": "모델", + "model-name": "모델 이름", + "model-plural": "모델들", + "model-store": "모델 저장소", + "monday": "월요일", + "monogram-url": "모노그램 URL", + "month": "월", + "more": "더 보기", + "more-help": "추가 도움말", + "more-lowercase": "더", + "most-active-user": "가장 활동적인 사용자", + "most-recent-session": "가장 최근 세션", + "move-the-entity": "{{entity}} 이동", + "ms": "밀리초", + "ms-team-plural": "MS 팀들", + "multi-select": "다중 선택", + "mutually-exclusive": "상호 배타적", + "my-data": "내 데이터", + "my-task-plural": "My Tasks", + "name": "이름", + "name-lowercase": "이름", + "navigation": "Navigation", + "need-help": "도움 필요", + "new": "새로운", + "new-password": "새 비밀번호", + "new-tab": "New Tab", + "new-term": "새 용어", + "new-test-suite": "새 테스트 스위트", + "next": "다음", + "no": "아니오", + "no-comma-cancel": "아니오, 취소", + "no-data-asset-found-for": "{{entity}}에 대한 데이터 자산을 찾을 수 없음", + "no-data-found": "데이터를 찾을 수 없음", + "no-description": "설명이 없음", + "no-diff-available": "차이가 없음", + "no-entity": "해당 {{entity}}이(가) 없음", + "no-entity-available": "No {{entity}} are available", + "no-entity-selected": "선택된 {{entity}}이(가) 없음", + "no-matching-data-asset": "일치하는 데이터 자산을 찾을 수 없음", + "no-of-test": "테스트 수", + "no-owner": "소유자 없음", + "no-parameter-available": "사용 가능한 파라미터가 없음", + "no-result-found": "결과를 찾을 수 없음.", + "no-reviewer": "검토자 없음", + "no-tags-added": "추가된 태그가 없음", + "nodes-per-layer": "레이어당 노드 수", + "non-partitioned": "비파티션", + "none": "없음", + "not-covered": "Not Covered", + "not-found-lowercase": "찾을 수 없음", + "not-null": "널이 아님", + "not-used": "사용되지 않음", + "notification": "알림", + "notification-alert": "알림 경고", + "notification-plural": "알림들", + "november": "11월", + "null": "널", + "number": "숫자", + "number-of-object-plural": "객체 수", + "number-of-retries": "재시도 횟수", + "number-of-rows": "행 수", + "number-reply-plural": "{{number}}개의 답글", + "object-plural": "객체들", + "observability": "모니터링", + "observability-alert": "모니터링 경고", + "october": "10월", + "of-lowercase": "의", + "ok": "확인", + "okta": "Okta", + "okta-service-account-email": "Okta 서비스 계정 이메일", + "old": "이전", + "old-password": "이전 비밀번호", + "older-reply-lowercase": "이전 답글", + "older-reply-plural-lowercase": "이전 답글들", + "om-jwt-token": "OpenMetadata JWT 토큰", + "on-demand": "온디맨드", + "on-lowercase": "에", + "one-reply": "답글 1개", + "one-to-many": "One to Many", + "one-to-one": "One to One", + "open": "열기", + "open-incident-plural": "Open Incidents", + "open-lowercase": "열림", + "open-metadata": "OpenMetadata", + "open-metadata-logo": "OpenMetadata 로고", + "open-metadata-updated": "OpenMetadata 업데이트됨!", + "open-metadata-url": "OpenMetadata URL", + "open-task-plural": "열린 작업들", + "operation-plural": "작업들", + "option": "옵션", + "optional": "Optional", + "or-lowercase": "또는", + "ordinal-position": "서수 위치", + "org-url": "OrgUrl", + "overview": "개요", + "owned-lowercase": "소유됨", + "owner": "소유자", + "owner-kpi": "소유자 KPI", + "owner-lowercase": "소유자", + "owner-lowercase-plural": "owners", + "owner-plural": "소유자들", + "page": "Page", + "page-not-found": "페이지를 찾을 수 없음", + "page-views-by-data-asset-plural": "데이터 자산별 페이지 조회수", + "parameter": "파라미터", + "parameter-plural": "파라미터들", + "parent": "상위", + "parsing-timeout-limit": "쿼리 파싱 시간 초과 제한", + "partition-lowercase-plural": "파티션들", + "partition-plural": "파티션들", + "partitioned": "파티션화됨", + "passed": "통과", + "password": "비밀번호", + "password-lowercase": "비밀번호", + "password-not-match": "비밀번호가 일치하지 않음", + "password-type": "{{type}} 비밀번호", + "path": "경로", + "pause": "일시정지", + "paused-uppercase": "PAUSED", + "pctile-lowercase": "백분위", + "pending-entity": "Pending {{entity}}", + "pending-task": "대기 중인 작업", + "pending-task-plural": "대기 중인 작업들", + "percentage": "백분율", + "permanently-delete": "영구 삭제", + "permanently-lowercase": "영구적으로", + "permission-plural": "권한들", + "persona": "페르소나", + "persona-plural": "페르소나들", + "personal-user-data": "개인 사용자 데이터", + "pipe-symbol": "|", + "pipeline": "파이프라인", + "pipeline-detail-plural": "파이프라인 상세 정보들", + "pipeline-detail-plural-lowercase": "파이프라인 상세 정보들", + "pipeline-lowercase": "파이프라인", + "pipeline-lowercase-plural": "파이프라인들", + "pipeline-name": "파이프라인 이름", + "pipeline-plural": "파이프라인들", + "pipeline-state": "파이프라인 상태", + "platform": "플랫폼", + "play": "재생", + "please-enter-value": "{{name}} 값을 입력해 주세요", + "please-password-type-first": "먼저 비밀번호를 입력해 주세요", + "please-select": "선택해 주세요", + "please-select-entity": "{{entity}}을(를) 선택해 주세요", + "plus-count-more": "+ {{count}}개 더", + "plus-symbol": "+", + "policy": "정책", + "policy-lowercase": "정책", + "policy-lowercase-plural": "정책들", + "policy-name": "정책 이름", + "policy-plural": "정책들", + "popularity": "인기도", + "posted-on-lowercase": "게시됨", + "precision": "정밀도", + "preference-plural": "환경 설정들", + "press": "누르세요", + "preview": "미리보기", + "preview-and-edit": "Preview & Edit", + "preview-data": "데이터 미리보기", + "previous": "이전", + "previous-version": "Previous Version", + "pricing": "가격", + "primary": "Primary", + "primary-key": "기본 키", + "primary-key-plural": "Primary Keys", + "privacy-policy": "개인정보 보호 정책", + "private-key": "개인 키", + "private-key-id": "개인 키 ID", + "profile": "프로필", + "profile-config": "프로필 구성", + "profile-lowercase": "프로필", + "profile-name": "프로필 이름", + "profile-sample-type": "프로필 샘플 {{type}}", + "profiler": "프로파일러", + "profiler-amp-data-quality": "프로파일러 & 데이터 품질", + "profiler-configuration": "프로파일러 구성", + "profiler-ingestion": "프로파일러 수집", + "profiler-lowercase": "프로파일러", + "profiler-setting-plural": "프로파일러 설정들", + "profiler-timeout-second-plural-label": "타임아웃(초)", + "project": "프로젝트", + "project-id": "프로젝트 ID", + "project-lowercase": "프로젝트", + "property": "속성", + "public-team": "공개 팀", + "quality": "품질", + "query": "쿼리", + "query-log-duration": "쿼리 로그 기간", + "query-lowercase": "쿼리", + "query-lowercase-plural": "쿼리들", + "query-plural": "쿼리들", + "query-used-in": "쿼리 사용 위치", + "queued": "Queued", + "range": "범위", + "re-assign": "재할당", + "re-deploy": "재배포", + "re-enter-new-password": "새 비밀번호 재입력", + "re-index-all": "모두 재인덱스", + "re-index-elasticsearch": "Elasticsearch 재인덱스", + "re-verify": "재검증", + "reaction-lowercase-plural": "반응들", + "read-type": "{{type}} 읽기", + "reason": "이유", + "receiver-plural": "수신자들", + "recent-announcement-plural": "최근 공지사항들", + "recent-event-plural": "Recent Events", + "recent-run-plural": "최근 실행들", + "recent-search-term-plural": "최근 검색어들", + "recent-views": "최근 조회", + "recently-viewed": "최근 조회됨", + "record-plural": "레코드들", + "recreate-index-plural": "인덱스 재생성들", + "reference-plural": "참조들", + "refresh": "새로고침", + "refresh-log": "로그 새로고침", + "regenerate-registration-token": "등록 토큰 재생성", + "region-name": "리전 이름", + "registry": "레지스트리", + "regular-expression": "Regular Expression", + "reject": "거부", + "reject-all": "모두 거부", + "rejected": "거부됨", + "related-column": "Related Column", + "related-metric-plural": "Related Metrics", + "related-term-plural": "관련 용어들", + "relationship": "Relationship", + "relationship-type": "Relationship Type", + "relevance": "관련성", + "remove": "제거", + "remove-entity": "{{entity}} 제거", + "remove-entity-lowercase": "remove {{entity}}", + "remove-lowercase": "제거", + "removed": "제거됨", + "removed-entity": "제거된 {{entity}}", + "removing-user": "사용자 제거 중", + "rename": "이름 변경", + "rename-entity": "{{entity}} 이름 변경", + "replication-factor": "복제 계수", + "reply": "답글", + "reply-in-conversation": "대화 내에서 답글", + "reply-lowercase": "답글", + "reply-lowercase-plural": "답글들", + "request": "요청", + "request-method": "요청 메서드", + "request-schema-field": "요청 스키마 필드", + "request-tag-plural": "요청 태그들", + "requirement-plural": "요구 사항들", + "reset": "재설정", + "reset-default-layout": "기본 레이아웃 재설정", + "reset-your-password": "비밀번호 재설정", + "resolution": "해결", + "resolution-time": "Resolution Time", + "resolve": "해결", + "resolved": "해결됨", + "resolved-by": "해결자", + "resolved-incident-plural": "Resolved Incidents", + "resource-permission-lowercase": "리소스 권한", + "resource-plural": "리소스들", + "response": "응답", + "response-schema-field": "응답 스키마 필드", + "response-time": "Response Time", + "restore": "복구", + "restore-entity": "{{entity}} 복구", + "restored-lowercase": "복구됨", + "result-limit": "결과 제한", + "result-plural": "결과들", + "resume": "재개", + "retention-period": "보존 기간", + "retention-size": "보존 크기", + "retention-size-lowercase": "보존 크기", + "return": "반환", + "review-data-entity": "Review data {{entity}}", + "reviewer": "검토자", + "reviewer-plural": "검토자들", + "revoke-token": "토큰 취소", + "role": "역할", + "role-lowercase": "역할", + "role-lowercase-plural": "역할들", + "role-name": "역할 이름", + "role-plural": "역할들", + "row": "행", + "row-count": "Row Count", + "row-count-lowercase": "행 수", + "row-plural": "행들", + "rtl-ltr-direction": "RTL/LTR 방향", + "rule": "규칙", + "rule-effect": "규칙 효과", + "rule-lowercase": "규칙", + "rule-lowercase-plural": "규칙들", + "rule-name": "규칙 이름", + "rule-plural": "규칙들", + "run": "실행", + "run-at": "실행 시간", + "run-now": "지금 실행", + "run-type": "실행 유형", + "running": "실행 중", + "runs-for": "대상 실행", + "s3-config-source": "S3 구성 소스", + "sample": "샘플", + "sample-data": "샘플 데이터", + "sample-data-count": "샘플 데이터 수", + "sample-data-count-lowercase": "샘플 데이터 수", + "saturday": "토요일", + "save": "저장", + "scale": "크기 조정", + "schedule": "일정", + "schedule-for-entity": "{{entity}}용 스케줄러", + "schedule-info": "스케줄 정보", + "schedule-interval": "스케줄 간격", + "schedule-to-run-every": "매번 실행될 스케줄", + "schedule-type": "스케줄 유형", + "schema": "스키마", + "schema-definition": "스키마 정의", + "schema-field": "스키마 필드", + "schema-field-plural": "스키마 필드들", + "schema-name": "스키마 이름", + "schema-plural": "스키마들", + "schema-text": "스키마 텍스트", + "schema-type": "Schema Type", + "scope-plural": "스코프들", + "search": "검색", + "search-by-type": "{{type}}로 검색", + "search-entity": "{{entity}} 검색", + "search-for-type": "{{type}} 검색", + "search-index": "검색 인덱스", + "search-index-ingestion": "검색 인덱스 수집", + "search-index-plural": "검색 인덱스들", + "search-index-setting-plural": "검색 인덱스 설정들", + "search-rbac": "Search RBAC", + "second-plural": "초", + "secret-key": "비밀 키", + "select": "선택", + "select-a-chart": "차트 선택", + "select-a-metric-type": "메트릭 유형 선택", + "select-a-policy": "정책 선택", + "select-add-test-suite": "테스트 스위트 선택/추가", + "select-all-entity": "모든 {{entity}} 선택", + "select-column-plural-to-exclude": "제외할 열들 선택", + "select-column-plural-to-include": "포함할 열들 선택", + "select-entity": "{{entity}} 선택", + "select-field": "{{field}} 선택", + "select-to-search": "검색할 항목 선택", + "select-type": "유형 선택", + "selected-entity": "선택된 {{entity}}", + "selected-lowercase": "선택됨", + "send": "전송", + "send-now": "지금 전송", + "send-to": "전송 대상", + "sender-email": "발신자 이메일", + "september": "9월", + "server": "서버", + "server-endpoint": "서버 엔드포인트", + "server-port": "서버 포트", + "service": "서비스", + "service-account-email": "서비스 계정 이메일", + "service-configuration-lowercase": "서비스 구성", + "service-created-successfully": "서비스가 성공적으로 생성됨", + "service-detail-lowercase-plural": "서비스 상세 정보들", + "service-lowercase": "서비스", + "service-lowercase-plural": "서비스들", + "service-name": "서비스 이름", + "service-plural": "서비스들", + "service-sso": "{{serviceType}} SSO", + "service-type": "서비스 유형", + "session-plural": "세션들", + "setting-plural": "설정들", + "setup-guide": "설정 가이드", + "severity": "심각도", + "shift": "변경", + "show": "보이기", + "show-deleted": "삭제된 항목 보이기", + "show-deleted-entity": "삭제된 {{entity}} 보이기", + "show-less": "더 적게 보이기", + "show-log-plural": "로그 보이기", + "show-more": "Show More", + "show-more-entity": "{{entity}} 더 보기", + "show-or-hide-advanced-config": "{{showAdv}} 고급 구성", + "sign-in-with-sso": "{{sso}}로 로그인", + "size": "크기", + "size-evolution-graph": "크기 변화 그래프", + "skew": "스큐", + "skipped": "건너뜀", + "slack": "슬랙", + "slack-support": "슬랙 지원", + "slash-symbol": "/", + "small": "소형", + "soft-delete": "소프트 삭제", + "soft-deleted-lowercase": "소프트 삭제됨", + "soft-lowercase": "소프트", + "sort": "Sort", + "source": "출처", + "source-aligned": "출처 맞춤", + "source-column": "출처 열", + "source-match": "이벤트 출처로 일치", + "source-plural": "출처들", + "source-url": "출처 URL", + "specific-data-asset-plural": "특정 데이터 자산들", + "sql-uppercase": "SQL", + "sql-uppercase-query": "SQL 쿼리", + "sso-uppercase": "SSO", + "stage-file-location": "스테이지 파일 위치", + "star": "별", + "star-open-metadata": "OpenMetadata에 별 표시", + "star-us-on-github": "GitHub에서 별 주세요", + "start-date-time-zone": "시작 날짜: ({{timeZone}})", + "start-elasticsearch-docker": "Elasticsearch Docker 시작", + "start-entity": "{{entity}} 시작", + "started": "시작됨", + "started-following": "팔로우 시작", + "status": "상태", + "stay-up-to-date": "최신 정보 유지", + "step": "단계", + "stop": "Stop", + "stop-re-index-all": "모두 재인덱스 중지", + "stopped": "중지됨", + "storage": "스토리지", + "storage-plural": "스토리지들", + "stored-procedure": "저장 프로시저", + "stored-procedure-plural": "저장 프로시저들", + "style": "스타일", + "sub-domain": "서브 도메인", + "sub-domain-lowercase": "서브 도메인", + "sub-domain-lowercase-plural": "서브 도메인들", + "sub-domain-plural": "서브 도메인들", + "sub-team-plural": "서브 팀들", + "submit": "제출", + "subscription": "구독", + "success": "성공", + "successful": "Successful", + "successfully-lowercase": "성공적으로", + "successfully-uploaded": "성공적으로 업로드됨", + "suggest": "제안", + "suggest-entity": "{{entity}} 제안", + "suggested-by": "제안자", + "suggested-description": "제안된 설명", + "suggested-description-plural": "제안된 설명들", + "suggestion": "제안", + "suggestion-lowercase-plural": "제안들", + "suggestion-pending": "제안 대기 중", + "suite": "스위트", + "sum": "합계", + "summary": "요약", + "sunday": "일요일", + "support": "지원", + "support-url": "지원 URL", + "supported-language-plural": "지원 언어들", + "synonym-lowercase-plural": "동의어들", + "synonym-plural": "동의어들", + "table": "테이블", + "table-constraint-plural": "테이블 제약들", + "table-constraints": "테이블 제약들", + "table-entity-text": "테이블 {{entityText}}", + "table-lowercase": "테이블", + "table-lowercase-plural": "테이블들", + "table-partition-plural": "테이블 파티션들", + "table-plural": "테이블들", + "table-profile": "테이블 프로파일", + "table-tests-summary": "테이블 테스트 요약", + "table-type": "테이블 유형", + "table-update-plural": "테이블 업데이트들", + "tag": "태그", + "tag-category-lowercase": "태그 카테고리", + "tag-lowercase": "태그", + "tag-lowercase-plural": "태그들", + "tag-plural": "태그들", + "target": "대상", + "target-column": "대상 열", + "task": "작업", + "task-entity": "{{entity}} 작업", + "task-lowercase": "작업", + "task-plural": "작업들", + "task-title": "작업 #{{title}}", + "team": "팀", + "team-asset-plural": "팀 자산들", + "team-lowercase": "팀", + "team-plural": "팀들", + "team-plural-lowercase": "팀들", + "team-type": "팀 유형", + "team-user-management": "팀 및 사용자 관리", + "tenant-id": "테넌트 ID", + "term": "용어", + "term-lowercase": "용어", + "term-plural": "용어들", + "test": "테스트", + "test-case": "테스트 케이스", + "test-case-lowercase": "테스트 케이스", + "test-case-lowercase-plural": "테스트 케이스들", + "test-case-name": "테스트 케이스 이름", + "test-case-plural": "테스트 케이스들", + "test-case-result": "테스트 케이스 결과", + "test-email": "테스트 이메일", + "test-email-connection": "이메일 연결 테스트", + "test-entity": "{{entity}} 테스트", + "test-plural": "테스트들", + "test-suite": "테스트 스위트", + "test-suite-ingestion": "테스트 스위트 수집", + "test-suite-lowercase": "테스트 스위트", + "test-suite-lowercase-plural": "테스트 스위트들", + "test-suite-plural": "테스트 스위트들", + "test-suite-status": "테스트 스위트 상태", + "test-suite-summary": "테스트 스위트 요약", + "test-type": "테스트 유형", + "testing-connection": "연결 테스트 중", + "tests-summary": "테스트 요약", + "text": "텍스트", + "theme": "테마", + "third-quartile": "세 번째 사분위", + "thread": "스레드", + "thread-plural-lowercase": "스레드들", + "three-dash-symbol": "---", + "three-dots-symbol": "•••", + "thursday": "목요일", + "tier": "티어", + "tier-number": "티어{{tier}}", + "tier-plural-lowercase": "티어들", + "time": "시간", + "timeout": "타임아웃", + "timezone": "타임존", + "title": "제목", + "to-lowercase": "에게", + "token-end-point": "토큰 엔드포인트", + "token-expiration": "토큰 만료", + "token-expired": "토큰 만료됨", + "token-security": "토큰 보안", + "token-uri": "토큰 URI", + "topic": "토픽", + "topic-lowercase": "토픽", + "topic-lowercase-plural": "토픽들", + "topic-name": "토픽 이름", + "topic-plural": "토픽들", + "total": "총계", + "total-entity": "총 {{entity}}", + "total-index-sent": "총 전송된 인덱스", + "total-user-plural": "Total Users", + "tour": "투어", + "tracking": "추적", + "transportation-strategy": "운송 전략", + "tree": "트리", + "trigger": "트리거", + "trigger-type": "트리거 유형", + "triggered-lowercase": "트리거됨", + "triggering-lowercase": "트리거 중", + "try-again": "다시 시도", + "tuesday": "화요일", + "type": "유형", + "type-entities": "{{type}} 엔터티들", + "type-filed-name": "{{fieldName}} 유형", + "type-lowercase": "유형", + "type-to-confirm": "확인을 위해 <0>{{text}} 입력", + "un-follow": "언팔로우", + "unhealthy": "Unhealthy", + "uninstall": "제거", + "uninstall-lowercase": "제거", + "uninstalled-lowercase": "제거됨", + "unique": "고유", + "unit-of-measurement": "Unit of Measurement", + "unpause": "일시정지 해제", + "unprocessed": "Unprocessed", + "up-vote": "업 보트", + "update": "업데이트", + "update-description": "설명 업데이트", + "update-entity": "{{entity}} 업데이트", + "update-image": "이미지 업데이트", + "update-request-tag-plural": "요청 태그 업데이트들", + "updated": "업데이트됨", + "updated-by": "업데이트한 사람", + "updated-lowercase": "업데이트됨", + "updated-on": "업데이트 시간", + "updating-lowercase": "업데이트 중", + "upload": "업로드", + "upload-csv-uppercase-file": "CSV 파일 업로드", + "upload-image": "이미지 업로드", + "upstream-depth": "업스트림 깊이", + "url-lowercase": "url", + "url-uppercase": "URL", + "usage": "사용", + "usage-ingestion": "사용 수집", + "usage-lowercase": "사용", + "use-aws-credential-plural": "AWS 자격증명 사용", + "use-fqn-for-filtering": "FQN 사용하여 필터링", + "use-ssl-uppercase": "SSL 사용", + "used-by": "사용됨", + "user": "사용자", + "user-account": "사용자 계정", + "user-analytics-report": "사용자 분석 보고서", + "user-lowercase": "사용자", + "user-name": "User Name", + "user-permission-plural": "사용자 권한들", + "user-plural": "사용자들", + "username": "사용자명", + "username-or-email": "사용자명 또는 이메일", + "valid-condition": "유효한 조건", + "validating-condition": "조건 검사 중...", + "validation-error-plural": "유효성 검사 오류들!", + "value": "값", + "value-count": "값 수", + "value-plural": "값들", + "verify-cert-plural": "인증서 검증", + "version": "버전", + "version-plural": "버전들", + "version-plural-history": "버전 기록들", + "view": "보기", + "view-all": "전체 보기", + "view-definition": "정의 보기", + "view-entity": "{{entity}} 보기", + "view-less": "View less", + "view-more": "더 보기", + "view-new-count": "{{count}}개 새 항목 보기", + "view-parsing-timeout-limit": "정의 파싱 타임아웃 제한 보기", + "view-plural": "보기들", + "visit-developer-website": "개발자 웹사이트 방문", + "volume-change": "볼륨 변화", + "wants-to-access-your-account": "{{username}} 계정 접근을 원함", + "warning": "경고", + "warning-plural": "경고들", + "web-analytics-report": "웹 분석 보고서", + "webhook": "웹훅", + "webhook-display-text": "웹훅 {{displayText}}", + "wednesday": "수요일", + "week": "주", + "weekly-usage": "주간 사용량", + "whats-new": "새 소식", + "whats-new-version": "새 소식 ({{version}})", + "widget": "위젯", + "widget-lowercase": "위젯", + "workflow-plural": "워크플로우들", + "yes": "예", + "yes-comma-confirm": "예, 확인", + "yesterday": "어제", + "your-entity": "당신의 {{entity}}" + }, + "message": { + "access-block-time-message": "최대 로그인 실패 시도 후 몇 밀리초 동안 접근이 차단됩니다.", + "access-control-description": "조직의 계층 구조에 맞게 역할과 정책을 사용하여 팀 접근을 구성하세요.", + "access-to-collaborate": "누구나 팀에 참여하고 데이터를 열람하며 협업할 수 있도록 접근을 허용하세요.", + "action-has-been-done-but-deploy-successfully": " {{action}}되었으며 성공적으로 배포되었습니다", + "action-has-been-done-but-failed-to-deploy": " {{action}}되었으나 배포에 실패했습니다", + "active-users": "활성 사용자 수를 표시합니다.", + "add-data-asset-domain": "{{domain}}에 서비스나 데이터 자산을 추가하여 시작하세요.", + "add-kpi-message": "데이터 자산의 건강 상태를 가장 잘 반영하는 핵심 성과 지표(KPI)를 파악하세요. 데이터 자산을 설명, 소유권, 티어 기준으로 검토하고, 목표 메트릭을 절대값이나 백분율로 정의하여 진행 상황을 추적하세요. 마지막으로 데이터 목표 달성을 위한 시작일과 종료일을 설정하세요.", + "add-new-service-description": "OpenMetadata가 통합하는 다양한 서비스 중에서 선택하세요. 새 서비스를 추가하려면 먼저 서비스 카테고리(데이터베이스, 메시징, 대시보드 또는 파이프라인)를 선택한 후, 사용 가능한 서비스 목록에서 원하는 서비스를 선택하세요.", + "add-policy-message": "정책은 팀에 할당됩니다. OpenMetadata에서 정책은 특정 조건에 따라 접근을 정의하는 규칙 모음입니다. 우리는 풍부한 SpEL(Spring Expression Language) 기반 조건을 지원합니다. 엔터티에서 지원되는 모든 작업이 게시되므로, 이 세분화된 작업들을 사용하여 각 정책의 조건부 규칙을 정의하고, 이를 통해 강력한 접근 제어 역할을 구축하세요.", + "add-query-helper-message": "데이터베이스에서 실행할 SQL 쿼리를 추가하세요. 동일한 쿼리는 '쿼리 사용 위치' 옵션에서 여러 테이블에 추가할 수 있습니다. 향후 참고할 수 있도록 쿼리에 대한 설명도 추가하세요.", + "add-role-message": "역할은 사용자에게 할당됩니다. OpenMetadata에서 역할은 정책의 집합이며, 각 역할에는 최소 하나의 정책이 연결되어야 합니다. 역할은 일대다 관계로 여러 정책을 지원하므로, 새 역할 생성 전에 필요한 정책들이 미리 생성되어 있는지 확인하고, 조건부 규칙에 기반한 잘 정의된 정책으로 강력한 접근 제어 역할을 구축하세요.", + "adding-new-asset-to-team": "자산 추가 버튼을 클릭하면 탐색 페이지로 이동하며, 그곳에서 자산의 소유자로 팀을 할당할 수 있습니다.", + "adding-new-entity-is-easy-just-give-it-a-spin": "새로운 {{entity}} 추가는 쉽습니다. 한번 시도해 보세요!", + "adding-new-tag": "{{categoryName}}에 새 태그를 추가 중입니다.", + "adding-new-user-to-entity": "{{entity}}에 새로운 사용자를 추가 중입니다.", + "admin-only-action": "이 작업은 관리자만 수행할 수 있습니다.", + "advanced-search-message": "and/or 조건을 사용한 구문 편집기로 올바른 데이터 자산을 빠르게 찾아보세요.", + "aggregate-domain-type-description": "이벤트와 트랜잭션 데이터를 포함하는 온라인 서비스 및 트랜잭션 데이터에 가까운 도메인입니다.", + "airflow-guide-message": "OpenMetadata는 수집 커넥터 실행을 위해 Airflow를 사용합니다. 우리는 수집 커넥터 배포를 위한 관리 API를 개발했습니다. OpenMetadata Airflow 인스턴스를 사용하거나, 아래 가이드를 참고하여 Airflow에 관리 API를 설치하세요.", + "airflow-host-ip-address": "OpenMetadata는 IP <0>{{hostIp}}에서 귀하의 리소스에 연결합니다. 네트워크 보안 설정에서 인바운드 트래픽을 허용했는지 확인하세요.", + "alert-recent-events-description": "List of recent alert events triggered for the alert {{alertName}}.", + "alerts-description": "웹훅을 사용하여 시기적절한 알림으로 최신 상태를 유지하세요.", + "alerts-destination-description": "Slack, MS Teams, 이메일 또는 웹훅으로 알림을 전송하세요.", + "alerts-filter-description": "알림의 범위를 좁히기 위해 변경 이벤트를 지정하세요.", + "alerts-source-description": "경고를 활성화할 출처를 지정하세요.", + "alerts-trigger-description": "'스키마 변경' 또는 '테스트 실패'와 같은 중요한 트리거 이벤트를 선택하여 알림을 생성하세요.", + "all-charts-are-mapped": "모든 차트가 기존 KPI와 매핑되었습니다.", + "already-a-user": "이미 사용자이신가요?", + "and-followed-owned-by-name": "그리고 당신이 팔로우 중인 팀은 {{userName}}이(가) 소유합니다.", + "announcement-action-description": "다가오는 유지보수, 업데이트 및 삭제에 대해 팀에 알리기 위해 배너를 설정하세요.", + "announcement-created-successfully": "공지사항이 성공적으로 생성되었습니다!", + "announcement-invalid-start-time": "공지 시작 시간은 종료 시간보다 앞서야 합니다.", + "app-already-installed": "애플리케이션이 이미 설치되었습니다", + "app-disabled-successfully": "애플리케이션이 성공적으로 비활성화되었습니다", + "app-installed-successfully": "애플리케이션이 성공적으로 설치되었습니다", + "app-uninstalled-successfully": "애플리케이션이 성공적으로 제거되었습니다", + "appearance-configuration-message": "회사 로고, 모노그램, 파비콘 및 브랜드 색상으로 OpenMetadata를 맞춤 설정하세요.", + "application-action-successfully": "애플리케이션이 {{action}}되었습니다.", + "application-disabled-message": "애플리케이션이 현재 비활성화되어 있습니다. 헤더의 점 3개 메뉴를 클릭하여 활성화하세요.", + "application-stop": "Application stop is in progresss", + "application-to-improve-data": "MetaPilot, Data Insights, 검색 인덱싱 애플리케이션을 사용하여 데이터를 개선하세요.", + "are-you-sure": "정말 확실합니까?", + "are-you-sure-action-property": "정말로 {{propertyName}}을(를) {{action}}하시겠습니까?", + "are-you-sure-delete-entity": "정말로 {{entity}} 속성을 삭제하시겠습니까?", + "are-you-sure-delete-property": "정말로 {{propertyName}} 속성을 삭제하시겠습니까?", + "are-you-sure-delete-tag": "정말로 {{type}} \"{{tagName}}\"을(를) 삭제하시겠습니까?", + "are-you-sure-to-revoke-access": "JWT 토큰의 접근 권한을 취소하시겠습니까?", + "are-you-sure-to-revoke-access-personal-access": "개인 접근 토큰의 접근 권한을 취소하시겠습니까?", + "are-you-sure-want-to-enable": "정말로 {{entity}}을(를) 활성화하시겠습니까?", + "are-you-sure-want-to-text": "정말로 {{text}}하시겠습니까?", + "are-you-sure-you-want-to-remove-child-from-parent": "정말로 {{parent}}에서 {{child}}를 제거하시겠습니까?", + "are-you-want-to-restore": "정말로 {{entity}}을(를) 복구하시겠습니까?", + "assess-data-reliability-with-data-profiler-lineage": "올바른 데이터 거버넌스 접근을 통해 데이터 기반 세상에서 경쟁 우위를 확보하세요. 데이터를 안전하게 보호하고, 비즈니스 혁신과 성장을 지원하세요.", + "assigned-you-a-new-task-lowercase": "새로운 작업이 귀하에게 할당되었습니다.", + "assigning-team-entity-description": "{{name}}에 {{entity}}을(를) 추가하세요. 이 {{entity}}은(는) {{name}} 팀 및 그 하위 팀의 모든 사용자에게 상속됩니다.", + "at-least-one-policy": "최소 하나의 정책을 입력하세요.", + "auth-configuration-missing": "인증 구성이 누락되었습니다.", + "authProvider-is-not-basic": "AuthProvider가 Basic이 아닙니다.", + "bot-email-confirmation": "{{botName}} 봇의 {{email}} 확인", + "can-not-add-widget": "크기 제한으로 인해 이 섹션에 위젯을 추가할 수 없습니다.", + "can-you-add-a-description": "설명을 추가해 주실 수 있나요?", + "checkout-service-connectors-doc": "여기에는 서비스 데이터를 인덱싱하기 위한 다양한 커넥터들이 있습니다. 커넥터를 확인해 보세요.", + "click-here-to-view-assets-on-explore": "(Click to view the filtered assets on Explore page.)", + "click-text-to-view-details": "<0>{{text}}를 클릭하여 세부 정보를 확인하세요.", + "closed-this-task": "이 작업을 닫았습니다.", + "collaborate-with-other-user": "다른 사용자와 협업하기 위해", + "compute-row-count-helper-text": "테스트 케이스의 통과 및 실패한 행 수를 계산하세요.", + "confidence-percentage-message": "NLP 모델이 열에 PII 데이터가 포함되었는지 판단할 때 사용할 신뢰 수준을 설정하세요.", + "configure-a-service-description": "고유한 서비스 이름을 입력하세요. 서비스 이름은 동일 카테고리 내에서 유일해야 합니다. 예를 들어, 데이터베이스 서비스에서는 MySQL과 Snowflake가 동일한 이름(예: customer_data)을 사용할 수 없지만, 다른 서비스 카테고리(대시보드, 파이프라인)에서는 동일한 이름을 사용할 수 있습니다. 서비스 이름에는 공백이 허용되지 않으며, '-'와 '_'는 사용 가능합니다. 또한 설명을 추가하세요.", + "configure-airflow": "UI를 통해 메타데이터 추출을 설정하려면 먼저 Airflow를 구성하고 연결해야 합니다. 자세한 내용은 <0>{{text}}를 참조하세요.", + "configure-dbt-model-description": "dbt 모델은 원시 데이터로부터 테이블을 생성하는 변환 로직을 제공합니다. 계보는 테이블 간 데이터 경로를 추적하지만, dbt 모델은 구체적인 정보를 제공합니다. 필요한 dbt 소스 공급자를 선택하고 필수 필드를 채우세요. OpenMetadata와 dbt를 통합하여 테이블 생성에 사용된 모델을 확인하세요.", + "configure-glossary-term-description": "용어집의 모든 용어는 고유한 정의를 가집니다. 개념의 표준 용어를 정의하는 것 외에도, 동의어 및 관련 용어(예: 상위 및 하위 용어)를 지정할 수 있습니다. 용어와 관련된 자산에 대한 참조도 추가할 수 있으며, 새 용어를 추가하거나 기존 용어를 업데이트할 수 있습니다. 일부 사용자가 용어를 검토하여 승인하거나 거부할 수 있습니다.", + "configure-search-re-index": "<0>{{settings}}로 이동하여 지금 실행 버튼을 클릭해 데이터를 재인덱스하세요.", + "configure-webhook-message": "OpenMetadata는 등록된 웹훅으로 이벤트 알림을 자동 전송하도록 구성할 수 있습니다. 웹훅 이름과 HTTP 콜백을 받을 엔드포인트 URL을 입력하세요. 관심 있는 이벤트(예: 엔터티 생성, 업데이트, 삭제)에 기반하여 알림을 받으려면 이벤트 필터를 사용하고, 웹훅의 용도와 사용 사례를 이해할 수 있도록 설명을 추가하세요. 고급 구성을 통해 HMAC 서명을 이용한 웹훅 이벤트 검증을 위한 공유 비밀 키를 설정할 수도 있습니다.", + "configure-webhook-name-message": "OpenMetadata는 등록된 {{webhookType}} 웹훅을 통해 이벤트 알림을 자동 전송하도록 구성할 수 있습니다. {{webhookType}} 웹훅 이름과 HTTP 콜백을 받을 엔드포인트 URL을 입력하세요. 필요한 엔터티에 대해서만 알림을 받도록 이벤트 필터를 사용하고, 웹훅의 사용 사례를 기록하기 위해 설명을 추가하세요. 또한, 고급 구성을 통해 HMAC 서명을 이용한 {{webhookType}} 웹훅 이벤트 검증을 위한 공유 비밀 키를 설정할 수 있습니다.", + "configured-sso-provider-is-not-supported": "구성된 SSO 공급자 \"{{provider}}\"은(는) 지원되지 않습니다. 서버의 인증 구성을 확인하세요.", + "confirm-delete-message": "이 메시지를 영구적으로 삭제하시겠습니까?", + "connection-details-description": "각 서비스에는 연결을 위한 기본 요구 사항이 있으며, 이 요구 사항은 해당 서비스의 JSON 스키마에서 생성됩니다. 필수 필드는 별표(*)로 표시됩니다.", + "connection-test-failed": "연결 테스트에 실패했습니다. 연결 및 실패한 단계의 권한을 확인하세요.", + "connection-test-successful": "연결 테스트가 성공했습니다.", + "connection-test-warning": "연결 테스트가 일부 성공했습니다. 일부 단계에서 실패가 발생하여 부분적인 메타데이터만 수집됩니다.", + "consumer-aligned-domain-type-description": "여러 소스로부터 데이터를 수집 및 정제하여, 고객 360, 고객 세션 등과 같은 집계 데이터와 데이터 제품을 다른 도메인이 사용할 수 있도록 제공하는 도메인입니다.", + "copied-to-clipboard": "클립보드에 복사됨", + "copy-to-clipboard": "클립보드에 복사", + "create-new-domain-guide": "데이터 메시(Mesh)는 도메인 지향 설계 개념에 따라 특정 비즈니스 도메인별로 데이터를 조직하는 분산 데이터 아키텍처입니다. 팀은 해당 도메인의 운영 및 분석 데이터를 소유하며, 데이터 계약에 따라 소비자에게 제품으로 데이터를 제공합니다. 이는 도메인에 구애받지 않는 셀프 서비스 데이터 인프라에 의해 지원되며, 컨설팅을 위한 지원 팀이 존재합니다.", + "create-new-glossary-guide": "용어집은 조직 내 개념과 용어를 정의하기 위해 사용되는 통제된 어휘집입니다. 용어집은 특정 도메인(예: 비즈니스, 기술)에 국한될 수 있으며, 표준 용어와 개념, 동의어 및 관련 용어를 정의할 수 있습니다. 또한 누가 어떻게 용어를 추가할지에 대한 제어가 가능합니다.", + "create-or-update-email-account-for-bot": "계정 이메일을 변경하면 봇 사용자가 업데이트되거나 새로 생성됩니다.", + "created-this-task-lowercase": "이 작업을 생성했습니다", + "cron-dow-validation-failure": "DOW part must be >= 0 and <= 6", + "cron-less-than-hour-message": "Cron schedule too frequent. Please choose at least 1-hour intervals.", + "custom-classification-name-dbt-tags": "dbt 태그를 위한 맞춤 OpenMetadata 분류 이름", + "custom-favicon-url-path-message": "파비콘 아이콘의 URL 경로입니다.", + "custom-logo-configuration-message": "회사 로고, 모노그램 및 파비콘으로 OpenMetadata를 맞춤 설정하세요.", + "custom-logo-url-path-message": "로그인 페이지 로고의 URL 경로입니다.", + "custom-monogram-url-path-message": "네비게이션 바 로고의 URL 경로입니다.", + "custom-properties-description": "속성을 확장하여 데이터 자산을 풍부하게 하기 위해 맞춤 메타데이터를 캡처하세요.", + "custom-property-is-set-to-message": "{{fieldName}}이(가) 설정되었습니다.", + "custom-property-name-validation": "이름은 공백, 밑줄, 점 없이 소문자로 시작해야 합니다.", + "custom-property-update": "Custom property '{{propertyName}}' update in {{entityName}} is {{status}}", + "customize-landing-page-header": "페르소나 \"<0>{{persona}}\"를 위한 {{pageName}}을 맞춤 설정하세요.", + "customize-open-metadata-description": "조직과 팀의 필요에 맞게 OpenMetadata UX를 맞춤 설정하세요.", + "data-asset-has-been-action-type": "데이터 자산이 {{actionType}}되었습니다.", + "data-insight-alert-destination-description": "관리자 또는 팀에게 이메일 알림을 전송하세요.", + "data-insight-alert-trigger-description": "실시간으로 트리거하거나 일간, 주간, 월간으로 예약 실행하세요.", + "data-insight-message": "데이터 인사이트 파이프라인을 관리하세요.", + "data-insight-page-views": "데이터셋 유형이 조회된 횟수를 표시합니다.", + "data-insight-pipeline-description": "데이터 인사이트 파이프라인을 배포하여 데이터 사용량을 모니터링하고 KPI를 설정하세요. 자세한 내용은 <0>{{link}}를 참조하세요.", + "data-insight-report-send-failed-message": "데이터 인사이트 보고서 전송에 실패했습니다.", + "data-insight-report-send-success-message": "데이터 인사이트 보고서가 성공적으로 전송되었습니다.", + "data-insight-subtitle": "시간에 따른 모든 데이터 자산의 상태를 한눈에 볼 수 있습니다.", + "database-service-name-message": "계보 생성을 위해 데이터베이스 서비스 이름을 추가하세요.", + "dbt-catalog-file-extract-path": "dbt 카탈로그 파일 경로를 입력하여 dbt 모델과 해당 열 스키마를 추출합니다.", + "dbt-cloud-type": "dbt 클라우드 계정에 여러 개의 {{type}}이 있는 경우, dbt 실행 산출물을 추출할 {{type}}의 ID를 지정하세요.", + "dbt-ingestion-description": "메타데이터 수집 설정 후 dbt 워크플로우를 구성 및 배포할 수 있습니다. 동일한 데이터베이스 서비스에 대해 여러 dbt 파이프라인을 설정할 수 있으며, 이 파이프라인은 테이블 엔터티의 dbt 탭에 데이터를 공급하고, dbt 노드에서 계보를 생성하며, 테스트를 추가합니다. 시작하려면 dbt 파일의 소스 구성을 추가하세요.", + "dbt-manifest-file-path": "dbt 매니페스트 파일 경로를 입력하여 dbt 모델을 추출하고 테이블과 연관시키세요.", + "dbt-optional-config": "dbt에서 설명을 업데이트할지 선택하는 옵션 구성", + "dbt-result-file-path": "dbt 실행 결과 파일 경로를 입력하여 테스트 결과 정보를 추출하세요.", + "dbt-run-result-http-path-message": "dbt 실행 결과를 HTTP 경로에서 추출합니다.", + "deeply-understand-table-relations-message": "하나의 플랫폼에서 데이터의 생산자와 소비자를 파악하여 생산성을 높이세요. 여러 도구의 데이터를 중앙에서 함께 협업하면 더욱 효과적입니다.", + "define-custom-property-for-entity": "조직의 필요에 맞게 {{entity}}에 대한 맞춤 속성을 정의하세요.", + "delete-action-description": "이 {{entityType}}을(를) 삭제하면 OpenMetadata에서 해당 메타데이터가 영구적으로 제거됩니다.", + "delete-asset-from-entity-type": "이 작업을 삭제하면 해당 {{entityType}}이(가) 엔터티에서 제거됩니다.", + "delete-entity-permanently": "이 {{entityType}}을(를) 삭제하면 영구적으로 제거됩니다.", + "delete-entity-type-action-description": "이 {{entityType}}을(를) 삭제하면 OpenMetadata에서 메타데이터가 영구적으로 제거됩니다.", + "delete-message-question-mark": "메시지를 삭제하시겠습니까?", + "delete-team-message": "\"{{teamName}}\" 팀 하위의 모든 팀도 함께 {{deleteType}} 삭제됩니다.", + "delete-webhook-permanently": "웹훅 {{webhookName}}을(를) 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "derived-tag-warning": "이 태그는 자동으로 파생되며, 관련 용어집 용어를 삭제해야만 제거할 수 있습니다.", + "destination-selection-warning": "\"{{subscriptionCategory}}\"가 알림을 받을 수 있도록 \"{{subscriptionType}}\"이(가) 구성되었는지 확인하세요.", + "disable-app": "이 작업은 {{app}} 애플리케이션을 비활성화합니다.", + "disable-classification-description": "분류를 비활성화하면 어떤 엔터티에서도 해당 분류로 검색하거나 관련 태그를 할당할 수 없습니다.", + "disabled-classification-actions-message": "비활성화된 분류에서는 이 작업을 수행할 수 없습니다.", + "discover-your-data-and-unlock-the-value-of-data-assets": "코드 없이 데이터 품질을 손쉽게 테스트, 배포, 결과 수집할 수 있습니다. 즉각적인 테스트 실패 알림으로 신뢰할 수 있는 데이터를 유지하세요.", + "domain-does-not-have-assets": "도메인 {{name}}에는 데이터 제품에 추가할 자산이 없습니다.", + "domain-type-guide": "There are three types of domains: Aggregate, Consumer-aligned and Source-aligned.", + "domains-not-configured": "도메인이 구성되지 않았습니다.", + "downstream-depth-message": "다운스트림 깊이에 대한 값을 선택하세요.", + "downstream-depth-tooltip": "대상(하위 레벨)을 식별하기 위해 최대 3개의 다운스트림 노드를 표시합니다.", + "drag-and-drop-files-here": "파일을 여기에 드래그 앤 드롭하세요.", + "drag-and-drop-or-browse-csv-files-here": "드래그 앤 드롭하거나 <0>{{text}}를 클릭하여 CSV 파일을 선택하세요.", + "duration-in-iso-format": "ISO 8601 형식 ('PnYnMnDTnHnMnS')의 기간", + "edit-entity-style-description": "{{entity}}의 아이콘과 배지 색상을 변경하세요.", + "edit-glossary-display-name-help": "표시 이름 업데이트", + "edit-glossary-name-help": "이름을 변경하면 기존 태그가 제거되고 새 태그가 생성됩니다.", + "edit-service-entity-connection": "{{entity}} 서비스 연결 수정", + "elastic-search-message": "Elasticsearch 인덱스가 최신 상태인지 동기화하거나 재생성하여 확인하세요.", + "elastic-search-re-index-pipeline-description": "검색 인덱스 파이프라인은 Elasticsearch의 데이터를 재인덱스하는 데 사용됩니다. 자세한 내용은 <0>{{link}}를 참조하세요.", + "elasticsearch-setup": "Elasticsearch에 메타데이터를 수집하고 인덱싱하기 위한 설정 방법을 확인하려면 안내를 따르세요.", + "email-configuration-message": "이메일 전송을 위한 SMTP 설정을 구성하세요.", + "email-is-invalid": "유효하지 않은 이메일입니다.", + "email-verification-token-expired": "이메일 확인 토큰이 만료되었습니다.", + "enable-access-control-description": "Enable to imply access control to search results. Disable to allow all users to view search results.", + "enable-classification-description": "분류를 활성화하면 어떤 엔터티에서도 해당 분류로 검색하거나 관련 태그를 할당할 수 있습니다.", + "enable-column-profile": "열 프로파일 활성화", + "enable-debug-logging": "디버그 로깅 활성화", + "enables-end-to-end-metadata-management": "모든 데이터 자산을 한 곳에서 확인하고, 올바른 데이터를 통해 중요한 인사이트를 얻으세요. 이제 데이터 잠재력을 열어 현명한 비즈니스 결정을 내리세요!", + "endpoint-should-be-valid": "엔드포인트는 유효한 URL이어야 합니다.", + "ensure-airflow-set-up-correctly-before-heading-to-ingest-metadata": "메타데이터 수집을 시작하기 전에 Airflow가 올바르게 구성되었는지 확인하세요.", + "ensure-elasticsearch-is-up-and-running": "Elasticsearch Docker가 실행 중인지 확인하세요.", + "enter-a-field": "{{field}}을(를) 입력하세요.", + "enter-column-description": "열 설명을 입력하세요.", + "enter-comma-separated-field": "쉼표(,)로 구분된 {{field}}을(를) 입력하세요.", + "enter-display-name": "표시 이름을 입력하세요.", + "enter-feature-description": "기능 설명을 입력하세요.", + "enter-interval": "간격을 입력하세요.", + "enter-test-case-name": "테스트 케이스 이름을 입력하세요.", + "enter-test-suite-name": "테스트 스위트 이름을 입력하세요.", + "enter-your-registered-email": "비밀번호 재설정 링크를 받기 위해 등록된 이메일을 입력하세요.", + "entity-already-exists": "{{entity}}이(가) 이미 존재합니다.", + "entity-are-not-available": "{{entity}}이(가) 사용 가능하지 않습니다.", + "entity-delimiters-not-allowed": "구분 기호가 있는 이름은 허용되지 않습니다.", + "entity-details-updated": "{{entityType}} {{fqn}} details updated successfully", + "entity-does-not-have-followers": "{{entityName}}에는 아직 팔로워가 없습니다.", + "entity-enabled-success": "{{entity}}이(가) 성공적으로 활성화되었습니다.", + "entity-ingestion-added-successfully": "{{entity}} 수집이 성공적으로 추가되었습니다.", + "entity-is-not-valid": "{{entity}}이(가) 유효하지 않습니다.", + "entity-is-not-valid-url": "{{entity}}이(가) 유효한 URL이 아닙니다.", + "entity-maximum-size": "{{entity}}은(는) 최대 {{max}}자까지 가능합니다.", + "entity-name-validation": "이름은 문자, 숫자, 밑줄, 하이픈, 점, 괄호, 앰퍼샌드만 포함할 수 있습니다.", + "entity-not-contain-whitespace": "{{entity}}은(는) 공백을 포함해서는 안 됩니다.", + "entity-owned-by-name": "이 엔터티는 {{entityOwner}}이(가) 소유합니다.", + "entity-pattern-validation": "{{entity}}은(는) 공백과 함께 다음 특수 문자만 포함할 수 있습니다: {{pattern}}.", + "entity-restored-error": "{{entity}} 복구 중 오류가 발생했습니다.", + "entity-restored-success": "{{entity}}이(가) 성공적으로 복구되었습니다.", + "entity-saved-successfully": "{{entity}}이(가) 성공적으로 저장되었습니다.", + "entity-size-in-between": "{{entity}}의 크기는 {{min}}과(와) {{max}} 사이여야 합니다.", + "entity-size-must-be-between-2-and-64": "{{entity}}의 크기는 2에서 64 사이여야 합니다.", + "entity-transfer-message": "<0>{{from}}의 {{entity}}을(를) <0>{{to}}의 {{entity}} 아래로 이동하려면 확인을 클릭하세요.", + "enum-property-update-message": "Enum values update started. You will be notified once it's done.", + "enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.", + "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", + "error-team-transfer-message": "팀 유형이 {{dragTeam}}인 팀은 {{dropTeam}}의 하위 팀으로 이동할 수 없습니다.", + "error-while-fetching-access-token": "액세스 토큰을 가져오는 중 오류가 발생했습니다.", + "explore-our-guide-here": "여기에서 가이드를 확인해 보세요.", + "export-entity-help": "모든 {{entity}}을 CSV 파일로 다운로드하여 팀과 공유하세요.", + "external-destination-selection": "Only external destinations can be tested.", + "failed-status-for-entity-deploy": "<0>{{entity}}이(가) {{entityStatus}}되었으나 배포에 실패했습니다.", + "feed-asset-action-header": "{{action}} <0>데이터 자산", + "feed-custom-property-header": "업데이트된 맞춤 속성:", + "feed-entity-action-header": "{{action}} <0>{{entity}}", + "feed-field-action-entity-header": "{{action}} <0>{{field}} 대상:", + "feed-filter-all": "내가 소유하고 팔로우하는 모든 데이터 자산의 피드", + "feed-filter-following": "내가 팔로우하는 모든 데이터 자산의 피드", + "feed-filter-owner": "내가 소유한 모든 데이터 자산의 피드", + "feed-test-case-header": "<0>데이터 품질 결과가 추가되었습니다", + "fetch-dbt-files": "dbt 카탈로그 및 매니페스트 파일을 가져올 수 있는 사용 가능한 소스들입니다.", + "fetch-pipeline-status-error": "파이프라인 상태를 가져오는 중 오류가 발생했습니다.", + "field-ca-certs-description": "구성에 인증서 경로를 추가해야 합니다. 경로는 수집 컨테이너 내의 로컬 경로여야 합니다.", + "field-data-is-not-available-for-deleted-entities": "삭제된 엔터티에 대해 {{field}} 데이터는 제공되지 않습니다.", + "field-insight": "유형별로 {{field}}가 있는 데이터 자산의 비율을 표시합니다.", + "field-region-name-description": "AWS 자격증명을 사용할 경우 리전 이름이 필요합니다.", + "field-text-is-invalid": "{{fieldText}}이(가) 유효하지 않습니다.", + "field-text-is-required": "{{fieldText}}이(가) 필요합니다.", + "field-timeout-description": "연결 타임아웃", + "field-use-aws-credentials-description": "AWS에서 OpenSearch에 연결할 때 AWS 자격증명을 사용할지 여부를 나타냅니다.", + "field-use-ssl-description": "Elasticsearch에 연결할 때 SSL 사용 여부를 나타냅니다. 기본적으로 SSL 설정은 무시됩니다.", + "field-verify-certs-description": "Elasticsearch에 SSL로 연결할 때 인증서를 검증할지 여부를 나타냅니다. 기본적으로 무시되며 true로 설정되어 있습니다. 'CA Certificates' 속성에 인증서를 제공해야 합니다.", + "filter-pattern-include-exclude-info": "쉼표로 구분된 정규식 목록을 추가하여 명시적으로 {{filterPattern}}을(를) {{activity}}합니다.", + "filter-pattern-info": "메타데이터 수집의 일부로서 {{filterPattern}}을 포함하거나 제외할지를 선택하세요.", + "filter-pattern-placeholder": "필터 패턴을 추가하려면 입력 후 Enter 키를 누르세요.", + "find-apps-to-improve-data": "데이터 개선을 위한 애플리케이션을 찾으세요.", + "find-in-table": "테이블에서 찾기", + "fosters-collaboration-among-producers-and-consumers": "조직의 목표와 KPI를 설정하여 데이터 문화를 촉진하고, 시기적절한 보고서로 데이터 상태를 모니터링하며 지속적인 개선 문화를 구축하세요.", + "get-started-with-open-metadata": "OpenMetadata 시작하기", + "glossary-tag-assignment-help-message": "이 자산들을 제거하거나 자산에서 충돌하는 태그를 제거한 후, 다시 태그를 추가할 수 있습니다.", + "glossary-tag-update-description": "이 작업은 용어집 용어와 연결된 모든 자산의 태그를 업데이트합니다.", + "glossary-tag-update-modal-title-failed": "다음 데이터 자산에 대한 검증에 실패했습니다.", + "glossary-tag-update-modal-title-validating": "데이터 자산 검증 중", + "glossary-term-description": "용어집의 모든 용어는 고유한 정의를 가집니다. 개념의 표준 용어를 정의하는 것 외에도, 동의어 및 관련 용어(예: 상위 및 하위 용어)를 지정할 수 있습니다. 용어와 관련된 자산에 대한 참조도 추가할 수 있으며, 새 용어를 추가하거나 기존 용어를 업데이트할 수 있습니다. 일부 사용자가 용어를 검토하여 승인 또는 거부할 수 있습니다.", + "glossary-term-status": "용어집 용어가 {{status}}되었습니다.", + "go-back-to-login-page": "로그인 페이지로 돌아가기", + "govern-url-size-message": "아이콘의 가로세로 비율은 1:1이어야 하며, 권장 크기는 64 x 64 px입니다.", + "group-team-type-change-message": "‘Group’ 팀 유형은 변경할 수 없습니다. 원하는 유형의 새 팀을 생성하세요.", + "group-type-team-not-allowed-to-have-sub-team": "‘Group’ 유형의 팀은 하위 팀을 가질 수 없습니다.", + "has-been-created-successfully": "성공적으로 생성되었습니다.", + "have-not-explored-yet": "아직 탐색하지 않으셨나요?", + "hex-code-placeholder": "HEX 색상 코드를 선택하거나 입력하세요.", + "hex-color-validation": "입력한 값이 유효한 HEX 코드가 아닙니다.", + "hi-user-welcome-to": "안녕하세요 {{user}}, OpenMetadata에 오신 것을 환영합니다!", + "image-upload-error": "Image upload is not supported. Please use Markdown syntax for images available via URL.", + "import-entity-help": "여러 {{entity}}를 한 번에 CSV 파일로 업로드하여 시간과 노력을 절약하세요.", + "in-this-database": "이 데이터베이스에서", + "include-assets-message": "데이터 소스에서 {{assets}}을 추출하도록 활성화하세요.", + "include-database-filter-extra-information": "서비스 생성 시 추가된 데이터베이스입니다.", + "include-lineage-message": "파이프라인에서 계보를 가져오지 않도록 구성합니다.", + "ingest-sample-data-for-entity": "각 {{entity}}에서 샘플 데이터를 추출하세요.", + "ingestion-bot-cant-be-deleted": "수집 봇은 삭제할 수 없습니다.", + "ingestion-pipeline-name-message": "이 파이프라인 인스턴스를 고유하게 식별하는 이름입니다.", + "ingestion-pipeline-name-successfully-deployed-entity": "설정 완료! 이(가) 성공적으로 배포되었습니다. {{entity}}은(는) 스케줄에 따라 정기적으로 실행됩니다.", + "input-placeholder": "Use @mention to tag and comment...", + "instance-identifier": "이 구성 인스턴스를 고유하게 식별하는 이름입니다.", + "integration-description": "생산성을 향상시키기 위해 애플리케이션 및 봇을 구성하세요.", + "invalid-object-key": "유효하지 않은 객체 키입니다. 문자, 밑줄 또는 달러 기호로 시작한 후 문자, 밑줄, 달러 기호 또는 숫자가 올 수 있습니다.", + "invalid-property-name": "유효하지 않은 속성 이름입니다.", + "invalid-unix-epoch-time-milliseconds": "Invalid Unix epoch time in milliseconds", + "jwt-token": "생성된 토큰을 사용하여 OpenMetadata API에 접근할 수 있습니다.", + "jwt-token-expiry-time-message": "JWT 토큰 만료 시간이 초 단위로 설정되어 있습니다.", + "kill-ingestion-warning": "이 수집 작업을 종료하면 실행 중 및 대기 중인 모든 워크플로우가 중지되고 실패로 표시됩니다.", + "kpi-subtitle": "데이터 자산의 건강 상태를 가장 잘 반영하는 핵심 성과 지표(KPI)를 파악하세요.", + "kpi-target-achieved": "목표 달성을 축하합니다!", + "kpi-target-achieved-before-time": "축하합니다! 팀이 설정된 KPI 목표를 훌륭하게 달성했습니다. 이제 이 KPI를 보관하거나 새 목표를 설정하여 데이터 문화를 더욱 강화할 수 있습니다.", + "kpi-target-overdue": "알림: 설명 KPI 목표에 아직 도달하지 못했으나, 시간이 남아 있습니다 – 조직에는 {{count}}일이 남았습니다. 진행 상황 유지를 위해 데이터 인사이트 보고서를 활성화하세요. 이를 통해 모든 팀에 주간 업데이트를 전송하여 조직의 KPI 달성을 촉진할 수 있습니다.", + "leave-the-team-team-name": "{{teamName}} 팀에서 나가세요.", + "length-validator-error": "최소 {{length}}개의 {{field}}가 필요합니다.", + "lineage-ingestion-description": "메타데이터 수집 설정 후 계보 수집을 구성 및 배포할 수 있습니다. 계보 수집 워크플로우는 쿼리 기록을 가져와 CREATE, INSERT, MERGE 등의 쿼리를 파싱하고 관련 엔터티 간의 계보를 생성합니다. 한 데이터베이스 서비스에 대해 계보 수집 파이프라인은 하나만 설정할 수 있습니다. 시작하려면 쿼리 로그 기간(일)과 결과 제한을 정의하세요.", + "link-copy-to-clipboard": "링크가 클립보드에 복사되었습니다.", + "list-of-strings-regex-patterns-csv": "쉼표로 구분된 문자열/정규식 패턴 목록을 입력하세요.", + "login-fail-attempt-message": "애플리케이션에 연속적으로 로그인 실패 시 허용되는 시도 횟수입니다.", + "logout-confirmation": "로그아웃하시겠습니까?", + "look-like-upgraded-om": "OpenMetadata가 업그레이드된 것 같습니다.", + "made-announcement": "공지사항을 작성했습니다.", + "make-an-announcement": "공지사항을 작성하세요!", + "manage-airflow-api": "OpenMetadata - 관리형 Airflow API", + "manage-airflow-api-failed": "OpenMetadata - 관리형 Airflow API를 찾는 데 실패했습니다.", + "mark-all-deleted-table-message": "이 옵션은 테이블의 소프트 삭제를 활성화하는 선택적 구성입니다. 활성화되면 소스에서 삭제된 테이블만 소프트 삭제되며, 데이터 소스의 모든 스키마에 적용됩니다. 관련된 테스트 스위트나 계보 정보 등도 함께 삭제됩니다. 여러 메타데이터 수집 파이프라인이 있는 경우 이 옵션을 사용하지 마세요. 이 기능을 사용하려면 markDeletedTables 옵션도 활성화되어 있어야 합니다.", + "mark-deleted-entity-message": "소스에서 삭제된 '{{entityPlural}}'이(가) 있을 경우 OpenMetadata에서 해당 '{{entity}}'을(를) 소프트 삭제하도록 설정하는 선택적 구성입니다. 삭제 후, 해당 '{{entity}}'과 관련된 모든 엔터티(예: 계보 등)도 삭제됩니다.", + "mark-deleted-table-message": "이 옵션은 테이블의 소프트 삭제를 활성화하는 선택적 구성입니다. 활성화되면 소스에서 삭제된 테이블만 소프트 삭제되며, 현재 파이프라인으로 수집되는 스키마에만 적용됩니다. 관련된 테스트 스위트나 계보 정보 등도 삭제됩니다.", + "markdown-editor-placeholder": "사용자를 태그하려면 @멘션, 데이터 자산을 태그하려면 #멘션을 사용하세요.", + "marketplace-verify-msg": "OpenMetadata는 발행인이 도메인을 제어하며 기타 요구 사항을 충족하는지 확인했습니다.", + "maximum-count-allowed": "Maximum {{count}} {{label}} are allowed", + "maximum-value-error": "최대값은 최소값보다 커야 합니다.", + "member-description": "OpenMetadata에서 사용자 및 팀에 대한 접근을 간소화하세요.", + "mentioned-you-on-the-lowercase": "에서 당신을 언급했습니다.", + "metadata-ingestion-description": "선택한 서비스 유형에 따라 스키마(또는 테이블), 토픽(메시징) 또는 대시보드의 필터 패턴 세부 정보를 입력하세요. 필터 패턴을 포함하거나 제외할 수 있으며, 보기 포함, 데이터 프로파일러 활성화/비활성화, 샘플 데이터 수집 여부를 선택할 수 있습니다.", + "metric-description": "Track the health of your data assets with metrics.", + "minimum-value-error": "최소값은 최대값보다 작아야 합니다.", + "minute": "분", + "modify-hierarchy-entity-description": "상위 {{entity}}을 변경하여 계층 구조를 수정하세요.", + "most-active-users": "페이지 조회수를 기반으로 플랫폼에서 가장 활동적인 사용자를 표시합니다.", + "most-viewed-data-assets": "가장 많이 조회된 데이터 자산을 표시합니다.", + "mutually-exclusive-alert": "만약 {{entity}}에 대해 '상호 배타적' 옵션을 활성화하면, 사용자는 데이터 자산에 하나의 {{child-entity}}만 적용할 수 있습니다. 이 옵션이 활성화되면 비활성화할 수 없습니다.", + "name-of-the-bucket-dbt-files-stored": "dbt 파일이 저장된 버킷의 이름입니다.", + "new-conversation": "새로운 대화를 시작합니다.", + "new-to-the-platform": "플랫폼이 처음이신가요?", + "no-access-placeholder": "접근 권한이 없습니다. 관리자에게 문의하세요.", + "no-activity-feed": "현재 소유하거나 팔로우하는 데이터 자산에 업데이트가 없습니다. <0>{{explored}}를 확인하고, 관심 있는 데이터 자산의 소유권을 주장하거나 팔로우하세요.", + "no-announcement-message": "공지사항이 없습니다. 공지 추가 버튼을 클릭하여 추가하세요.", + "no-asset-available": "사용 가능한 자산이 없습니다.", + "no-closed-task": "닫힌 작업이 없습니다.", + "no-config-available": "사용 가능한 연결 구성이 없습니다.", + "no-config-plural": "구성이 없습니다.", + "no-custom-properties-entity": "There are currently no Custom Properties defined for the {{entity}} Data Asset. To discover how to add Custom Properties, please refer to <0>{{docs}}", + "no-customization-available": "No customization available for this tab", + "no-data": "데이터가 없습니다.", + "no-data-assets": "OpenMetadata에 오신 것을 환영합니다! 아직 데이터 자산이 추가되지 않은 것 같습니다. 시작하려면 <0>시작하기 가이드를 확인하세요.", + "no-data-available": "사용 가능한 데이터가 없습니다.", + "no-data-available-entity": "{{entity}}에 사용 가능한 데이터가 없습니다.", + "no-data-available-for-search": "데이터를 찾을 수 없습니다. 다른 텍스트로 검색해 보세요.", + "no-data-available-for-selected-filter": "데이터를 찾을 수 없습니다. 필터를 변경해 보세요.", + "no-data-quality-test-case": "이 테이블에 구성된 데이터 품질 테스트가 없는 것 같습니다. 데이터 품질 테스트 설정 방법은 <0>{{explore}}를 참조하세요.", + "no-default-persona": "No default persona", + "no-domain-assigned-to-entity": "{{entity}}에 할당된 도메인이 없습니다.", + "no-domain-available": "구성할 도메인이 없습니다. 도메인을 추가하려면 <0>{{link}}를 클릭하세요.", + "no-entity-activity-message": "{{entity}}에 아직 활동이 없습니다. 대화를 시작하려면 클릭하세요.", + "no-entity-available-with-name": "이름이 일치하는 {{entity}}이(가) 없습니다.", + "no-entity-data-available": "{{entity}} 데이터가 없습니다.", + "no-entity-found-for-name": "{{name}}에 해당하는 {{entity}}을(를) 찾을 수 없습니다.", + "no-execution-runs-found": "파이프라인 실행 기록을 찾을 수 없습니다.", + "no-features-data-available": "사용 가능한 기능 데이터가 없습니다.", + "no-feed-available-for-selected-filter": "피드를 찾을 수 없습니다. 필터를 변경해 보세요.", + "no-glossary-term": "현재 정의된 용어집 용어가 없는 것 같습니다. 새 용어집 용어를 생성하려면 '용어 추가' 버튼을 사용하세요.", + "no-incident-found": "현재 데이터 품질에 영향을 주는 사건이 없습니다.", + "no-info-about-joined-tables": "조인된 테이블에 대한 정보가 없습니다.", + "no-ingestion-available": "사용 가능한 수집 데이터가 없습니다.", + "no-ingestion-description": "수집 데이터를 보려면 메타데이터 수집을 실행하세요. 스케줄 설정 방법은 <0>{{link}} 문서를 참조하세요.", + "no-ingestion-pipeline-found": "수집 파이프라인을 찾을 수 없습니다. 수집 파이프라인을 설정하려면 배포 버튼을 클릭하세요.", + "no-inherited-roles-found": "상속된 역할이 없습니다.", + "no-installed-applications-found": "현재 설치된 애플리케이션이 없습니다. '앱 추가' 버튼을 클릭하여 설치하세요.", + "no-kpi": "조직에 아직 설정된 핵심 성과 지표(KPI)가 없습니다! OpenMetadata에서 KPI를 설정하면 문서화, 효과적인 소유권, 효율적인 티어링을 위한 명확한 목표를 세울 수 있습니다.", + "no-kpi-available-add-new-one": "사용 가능한 KPI가 없습니다. KPI 추가 버튼을 클릭하여 추가하세요.", + "no-kpi-found": "{{name}} 이름의 KPI를 찾을 수 없습니다.", + "no-match-found": "일치하는 항목이 없습니다.", + "no-mentions": "현재 활동 내역에 멘션이 없습니다. 좋은 성과를 계속 이어가세요!", + "no-notification-found": "알림을 찾을 수 없습니다.", + "no-open-task": "열린 작업이 없습니다.", + "no-open-tasks": "좋은 소식입니다! 현재 열린 작업이 없습니다. 잠시 여유를 즐기세요!", + "no-owned-data": "데이터는 소유될 때 더 가치가 있습니다. 아직 귀하나 팀이 데이터 자산의 소유권을 주장하지 않은 것 같습니다. 데이터 자산을 소유하기 시작하려면 <0>탐색 옵션을 클릭하세요.", + "no-permission-for-action": "이 작업을 수행할 권한이 없습니다.", + "no-permission-to-view": "이 데이터를 볼 권한이 없습니다.", + "no-persona-assigned": "할당된 페르소나가 없습니다.", + "no-persona-message": "랜딩 페이지 맞춤 설정을 위해 페르소나가 필요합니다. <0>{{link}}에서 페르소나를 생성하세요.", + "no-profiler-enabled-summary-message": "이 테이블에 대해 프로파일러가 활성화되어 있지 않습니다.", + "no-profiler-message": "데이터 프로파일러는 수집 시 선택적 구성입니다. 문서를 참조하여 활성화하세요.", + "no-recently-viewed-date": "최근에 조회한 데이터 자산이 없습니다. 흥미로운 자산을 찾아보세요!", + "no-reference-available": "참조를 찾을 수 없습니다.", + "no-related-terms-available": "관련 용어가 없습니다.", + "no-roles-assigned": "할당된 역할이 없습니다.", + "no-rule-found": "규칙을 찾을 수 없습니다.", + "no-searched-terms": "검색된 용어가 없습니다.", + "no-selected-dbt": "dbt 구성을 위한 소스가 선택되지 않았습니다.", + "no-service-connection-details-message": "{{serviceName}}에 연결 세부 정보가 입력되지 않았습니다. 수집 작업 전에 세부 정보를 추가하세요.", + "no-synonyms-available": "사용 가능한 동의어가 없습니다.", + "no-table-pipeline": "정기적으로 데이터 품질 테스트를 자동화하기 위해 파이프라인을 추가하세요. 최적의 결과를 위해 테이블 로드 빈도에 맞게 스케줄을 설정하는 것이 좋습니다.", + "no-tags-description": "이 카테고리에는 정의된 태그가 없습니다. 새 태그를 생성하려면 '추가' 버튼을 클릭하세요.", + "no-tasks-assigned": "현재 귀하에게 할당된 대기 작업이 없습니다.", + "no-team-found": "팀을 찾을 수 없습니다.", + "no-terms-found": "용어를 찾을 수 없습니다.", + "no-terms-found-for-search-text": "\"{{searchText}}\"에 해당하는 용어를 찾을 수 없습니다.", + "no-test-result-for-days": "{{days}} 동안의 테스트 결과를 찾을 수 없습니다.", + "no-test-suite-table-pipeline": "파이프라인을 통합하면 데이터 품질 테스트를 정기적으로 자동화할 수 있습니다. 파이프라인 실행 전에 테스트를 구성하세요.", + "no-token-available": "사용 가능한 토큰이 없습니다.", + "no-total-data-assets": "데이터 인사이트를 통해 조직 내 데이터 자산의 총 수, 신규 자산 추가 속도 등 데이터 전반의 흐름을 파악할 수 있습니다. 자세한 내용은 <0>{{setup}}를 참조하세요.", + "no-user-available": "사용 가능한 사용자가 없습니다.", + "no-user-part-of-team": "No users is part of this {{team}} Team", + "no-username-available": "\"<0>{{user}}\" 이름의 사용자를 찾을 수 없습니다.", + "no-users": "{{text}}에 해당하는 사용자가 없습니다.", + "no-version-type-available": "사용 가능한 {{type}} 버전이 없습니다.", + "no-widgets-to-add": "추가할 새로운 위젯이 없습니다.", + "nodes-per-layer-message": "레이어당 노드 수를 입력하세요.", + "nodes-per-layer-tooltip": "레이어당 표시할 노드 수를 선택하세요. 기존 노드 수가 설정된 수보다 많으면 페이지네이션이 표시됩니다.", + "not-followed-anything": "탐색을 시작하고 관심 있는 데이터 자산을 팔로우하세요.", + "notification-description": "실시간 업데이트와 시기적절한 알림을 받을 수 있도록 알림을 설정하세요.", + "om-description": "중앙 집중식 메타데이터 저장소로, 데이터를 발견하고 협업하며 올바른 데이터를 관리할 수 있습니다.", + "om-url-configuration-message": "Configure the OpenMetadata URL Settings.", + "on-demand-description": "Run the ingestion manually.", + "onboarding-claim-ownership-description": "데이터는 소유될 때 더 효과적입니다. 귀하가 소유한 데이터 자산을 확인하고 소유권을 주장하세요.", + "onboarding-explore-data-description": "조직 내 인기 데이터 자산을 살펴보세요.", + "onboarding-stay-up-to-date-description": "자주 사용하는 데이터셋을 팔로우하여 최신 상태를 유지하세요.", + "only-reviewers-can-approve-or-reject": "검토자만 승인하거나 거부할 수 있습니다.", + "optional-configuration-update-description-dbt": "dbt에서 설명을 업데이트할지 선택하는 옵션 구성", + "page-is-not-available": "찾으시는 페이지를 찾을 수 없습니다.", + "page-sub-header-for-activity-feed": "데이터 변경 이벤트의 요약을 볼 수 있는 활동 피드입니다.", + "page-sub-header-for-admins": "조직 내 다른 관리자 및 각 팀과 역할의 세부 정보를 확인하세요.", + "page-sub-header-for-advanced-search": "and/or 조건을 사용한 구문 편집기로 올바른 데이터 자산을 빠르게 찾아보세요.", + "page-sub-header-for-apis": "가장 인기 있는 API 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-bots": "범위가 제한된 접근 권한을 가진 잘 정의된 봇을 생성하세요.", + "page-sub-header-for-column-profile": "프로파일러를 통해 열 구조를 모니터링하고 이해하세요.", + "page-sub-header-for-customize-landing-page": "특정 사용자 페르소나와 경험에 맞게 OpenMetadata 랜딩 페이지를 맞춤 설정하세요.", + "page-sub-header-for-dashboards": "가장 인기 있는 대시보드 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-data-observability": "UI를 통해 테스트 스위트 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-data-quality": "데이터 품질 테스트를 구축하여 신뢰할 수 있는 데이터 제품을 만드세요.", + "page-sub-header-for-databases": "가장 인기 있는 데이터베이스 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-lineage-config-setting": "Configure the lineage view settings.", + "page-sub-header-for-login-configuration": "로그인 실패 시 처리 및 JWT 토큰 만료 시간을 정의하세요.", + "page-sub-header-for-messagings": "가장 많이 사용되는 메시징 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-metadata": "UI를 통해 메타데이터 서비스를 구성하세요.", + "page-sub-header-for-ml-models": "UI를 통해 ML 모델 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-om-health-configuration": "데이터베이스 접근, Elasticsearch 상태, 파이프라인 서비스 클라이언트, jwks 구성 및 마이그레이션을 확인하세요.", + "page-sub-header-for-persona": "페르소나를 통해 사용자 경험을 향상하고 맞춤 설정하세요.", + "page-sub-header-for-pipelines": "가장 많이 사용되는 파이프라인 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-policies": "세분화된 접근 제어를 위한 규칙 집합으로 정책을 정의하세요.", + "page-sub-header-for-profiler-configuration": "열 데이터 유형에 따라 계산할 메트릭을 설정하여 프로파일러 동작을 전역적으로 맞춤 설정하세요.", + "page-sub-header-for-roles": "사용자 또는 팀에 대해 포괄적인 역할 기반 접근을 할당하세요.", + "page-sub-header-for-search": "가장 인기 있는 검색 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-search-setting": "Ability to configure the search settings to suit your needs.", + "page-sub-header-for-setting": "귀하의 필요에 맞게 OpenMetadata 애플리케이션을 구성할 수 있습니다.", + "page-sub-header-for-storages": "가장 인기 있는 스토리지 서비스로부터 메타데이터를 수집하세요.", + "page-sub-header-for-table-profile": "프로파일러를 통해 테이블 구조를 모니터링하고 이해하세요.", + "page-sub-header-for-teams": "계층적 팀으로 조직 전체 구조를 표현하세요.", + "page-sub-header-for-users": "계층적 팀으로 조직 전체 구조를 표현하세요.", + "paid-addon-description": "<0>{{app}}은(는) Collate 고객을 위한 유료 애드온입니다.", + "password-error-message": "비밀번호는 최소 8자, 최대 56자여야 하며, 하나 이상의 대문자(A-Z), 소문자(a-z), 숫자, 그리고 하나의 특수 문자(예: !, %, @, # 등)를 포함해야 합니다.", + "password-pattern-error": "비밀번호는 최소 8자, 최대 56자이며, 하나의 특수 문자, 대문자, 소문자를 포함해야 합니다.", + "path-of-the-dbt-files-stored": "dbt 파일이 저장된 폴더의 경로", + "permanently-delete-ingestion-pipeline": "Permanently deleting this <0>{{entityName}} will result in loss of pipeline configuration, and it cannot be recovered.", + "permanently-delete-metadata": "이 <0>{{entityName}}을(를) 영구적으로 삭제하면 OpenMetadata에서 해당 메타데이터가 제거되어 복구할 수 없습니다.", + "permanently-delete-metadata-and-dependents": "이 {{entityName}}을(를) 영구적으로 삭제하면 해당 메타데이터와 함께 관련 {{dependents}}의 메타데이터도 OpenMetadata에서 영구적으로 제거됩니다.", + "personal-access-token": "개인 접근 토큰", + "pipeline-action-failed-message": "파이프라인 {{action}}에 실패했습니다!", + "pipeline-action-success-message": "파이프라인이 성공적으로 {{action}}되었습니다!", + "pipeline-description-message": "파이프라인에 대한 설명입니다.", + "pipeline-disabled-ingestion-deployment": "수집 데이터를 보려면, 아래 <0>{{link}}를 참고하여 자체 환경에서 메타데이터 수집을 실행하세요.", + "pipeline-killed-successfully": "{{pipelineName}}의 실행 중인 워크플로우가 성공적으로 종료되었습니다.", + "pipeline-not-deployed": "파이프라인이 배포되지 않았습니다.", + "pipeline-scheduler-message": "수집 스케줄러가 응답하지 않습니다. 자세한 사항은 Collate 지원팀에 문의하세요. 감사합니다.", + "pipeline-will-trigger-manually": "파이프라인은 수동으로만 트리거됩니다.", + "pipeline-will-triggered-manually": "파이프라인은 수동으로만 트리거됩니다.", + "please-contact-us": "자세한 내용은 support@getcollate.io로 문의하세요.", + "please-enter-to-find-data-assets": "데이터 자산을 찾으려면 Enter 키를 누르세요: <0>{{keyword}}", + "please-refresh-the-page": "변경 사항을 확인하려면 페이지를 새로고침하세요.", + "please-type-text-to-confirm": "확인을 위해 {{text}}를 입력하세요.", + "popup-block-message": "로그인 팝업이 브라우저에 의해 차단되었습니다. <0>활성화한 후 다시 시도하세요.", + "process-pii-sensitive-column-message": "열 이름을 확인하여 PII 민감/비민감 열에 자동으로 태그를 지정하세요.", + "process-pii-sensitive-column-message-profiler": "활성화되면 샘플 데이터를 분석하여 각 열에 적절한 PII 태그를 결정합니다.", + "profile-sample-percentage-message": "프로파일러 값을 백분율로 설정하세요.", + "profile-sample-row-count-message": "프로파일러 값을 행 수로 설정하세요.", + "profiler-ingestion-description": "메타데이터 수집 설정 후 프로파일러 워크플로우를 구성 및 배포할 수 있습니다. 동일한 데이터베이스 서비스에 대해 여러 프로파일러 파이프라인을 설정할 수 있으며, 이 파이프라인은 테이블 엔터티의 프로파일러 탭에 데이터를 공급하고, 해당 엔터티에 구성된 테스트를 실행합니다. 시작하려면 이름, FQN, 그리고 필터 패턴을 정의하세요.", + "profiler-timeout-seconds-message": "프로파일러의 타임아웃 시간을 초 단위로 입력하세요 (선택 사항). 프로파일러는 대기 중인 쿼리가 실행되도록 기다리거나 타임아웃에 도달하면 해당 쿼리를 종료합니다.", + "queries-result-test": "1개 이상의 행을 반환하는 쿼리는 테스트 실패로 처리됩니다.", + "query-log-duration-message": "쿼리 로그에서 사용 데이터를 처리하기 위해 조회할 기간을 조정하는 구성입니다.", + "query-used-by-other-tables": "다른 테이블에서 사용된 쿼리", + "reacted-with-emoji": "{{type}} 이모지로 반응했습니다.", + "redirect-message": "리다이렉트 중입니다. 잠시만 기다리세요.", + "redirecting-to-home-page": "홈페이지로 리다이렉트 중입니다.", + "refer-to-our-doc": "더 도움이 필요하신가요? 자세한 내용은 <0>{{doc}}를 참조하세요.", + "remove-edge-between-source-and-target": "정말로 \"{{sourceDisplayName}}와 {{targetDisplayName}}\" 사이의 연결을 제거하시겠습니까?", + "remove-lineage-edge": "계보 연결 제거", + "rename-entity": "{{entity}}의 이름과 표시 이름을 변경하세요.", + "request-approval-message": "승인 요청:", + "request-description": "요청 설명", + "request-description-message": "요청 설명:", + "request-tags-message": "요청 태그:", + "request-test-case-failure-resolution-message": "테스트 케이스 실패 해결 요청:", + "request-update-description": "업데이트 요청 설명", + "reset-layout-confirmation": "정말로 \"기본 레이아웃\"을 적용하시겠습니까?", + "reset-link-has-been-sent": "비밀번호 재설정 링크가 귀하의 이메일로 전송되었습니다.", + "restore-action-description": "이 {{entityType}}을(를) 복구하면 OpenMetadata에서 해당 메타데이터가 복원됩니다.", + "restore-deleted-team": "팀을 복구하면 모든 메타데이터가 OpenMetadata에 다시 추가됩니다.", + "restore-entities-error": "{{entity}} 복구 중 오류가 발생했습니다.", + "restore-entities-success": "{{entity}}이(가) 성공적으로 복구되었습니다.", + "result-limit-message": "쿼리 로그의 제한을 설정하는 구성입니다.", + "retention-period-description": "보존 기간은 데이터가 삭제 또는 보관 대상으로 간주되기 전까지 유지되는 기간을 의미합니다. 예: 30일, 6개월, 1년 또는 UTC 기준의 ISO 8601 형식(P23DT23H) 등이 유효합니다.", + "run-sample-data-to-ingest-sample-data": "샘플 데이터를 실행하여 OpenMetadata에 샘플 데이터 자산을 수집하세요.", + "run-status-at-timestamp": "{{timestamp}}에 {{status}} 상태로 실행됨", + "schedule-description": "Schedule the ingestion to run at a specific time and frequency.", + "schedule-for-ingestion-description": "스케줄은 시간별, 일별 또는 주별로 설정할 수 있습니다. 타임존은 UTC입니다.", + "scheduled-run-every": "매번 실행되도록 예약됨", + "scopes-comma-separated": "쉼표로 구분된 스코프 값을 추가하세요.", + "search-entity-count": "{{count}} assets have been found with this filter.", + "search-for-edge": "파이프라인, 저장 프로시저 검색", + "search-for-entity-types": "테이블, 토픽, 대시보드, 파이프라인, ML 모델, 용어집, 태그 등을 검색하세요.", + "search-for-ingestion": "수집 검색", + "select-alert-type": "경고 유형을 선택하세요.", + "select-column-name": "열 이름을 선택하세요.", + "select-gcs-config-type": "GCS 구성 유형을 선택하세요.", + "select-interval-type": "간격 유형을 선택하세요.", + "select-interval-unit": "간격 단위를 선택하세요.", + "select-team": "팀 유형을 선택하세요.", + "select-test-case": "예약 간격 실행을 위한 테스트 케이스를 선택하세요. 선택하지 않으면 모든 테스트 케이스가 기본으로 실행됩니다.", + "select-token-expiration": "토큰 만료 기간을 선택하세요.", + "service-created-entity-description": "이(가) 성공적으로 생성되었습니다. 새로 생성된 서비스를 방문하여 세부 정보를 확인하세요. {{entity}}", + "service-description": "다양한 소스로부터 메타데이터를 수집하기 위해 커넥터를 설정하세요.", + "service-name-length": "서비스 이름은 1자 이상 128자 이하여야 합니다.", + "service-requirements-description": "각 서비스에는 연결을 위한 기본 요구 사항이 있으며, 아래에 필요한 기본 사항이 표시됩니다.", + "service-with-delimiters-not-allowed": "구분 기호가 있는 서비스 이름은 허용되지 않습니다.", + "service-with-space-not-allowed": "공백이 포함된 서비스 이름은 허용되지 않습니다.", + "session-expired": "세션이 만료되었습니다! OpenMetadata에 접근하려면 다시 로그인하세요.", + "setup-custom-property": "OpenMetadata는 테이블 엔터티에 맞춤 속성을 지원합니다. 고유한 속성 이름을 추가하여 맞춤 속성을 생성하세요. 이름은 카멜케이스 형식에 따라 소문자로 시작해야 하며, 대문자와 숫자는 포함될 수 있으나 공백, 밑줄, 점은 허용되지 않습니다. 제공된 옵션 중에서 원하는 속성 유형을 선택하고, 팀에 추가 정보를 제공할 수 있도록 설명을 작성하세요.", + "setup-data-insights": "데이터 인사이트 설정 방법", + "size-evolution-description": "조직 내 자산의 크기 변화를 보여줍니다.", + "soft-delete-message-for-entity": "소프트 삭제 시 {{entity}}이(가) 비활성화되며, 해당 {{entity}}에 대한 검색, 읽기 또는 쓰기 작업이 중지됩니다.", + "something-went-wrong": "문제가 발생했습니다.", + "source-aligned-domain-type-description": "다양한 도메인의 데이터를 결합하여 사용자에게 제공하는, 데이터 기반 의사결정을 지원하는 사용자 친화적 도메인입니다.", + "special-character-not-allowed": "특수 문자는 허용되지 않습니다.", + "sql-query-tooltip": "1개 이상의 행을 반환하는 쿼리는 테스트 실패로 처리됩니다.", + "sso-provider-not-supported": "SSO 공급자 {{provider}}은(는) 지원되지 않습니다.", + "stage-file-location-message": "쿼리 로그를 처리하기 전에 저장할 임시 파일 이름입니다. 절대 경로를 입력하세요.", + "star-on-github-description": "개발자 여러분, OpenMetadata의 오픈 소스 활동을 강화해 봅시다! 🚀 여러분의 별 표시는 OpenMetadata를 최고의 메타데이터 플랫폼으로 도약시키는 데 큰 도움이 됩니다. 소문을 퍼뜨리고, 함께해 주세요! 🌟", + "still-running-into-issue": "문제가 계속된다면 슬랙으로 문의해 주세요.", + "success-status-for-entity-deploy": "<0>{{entity}}이(가) {{entityStatus}}되었으며 성공적으로 배포되었습니다.", + "successfully-completed-the-tour": "투어를 성공적으로 완료했습니다.", + "synonym-placeholder": "동의어를 추가하려면 입력 후 Enter 키를 누르세요.", + "system-alert-edit-message": "Editing a system generated alert is not allowed.", + "system-tag-delete-disable-message": "시스템에서 생성된 태그는 삭제할 수 없습니다. 대신 태그를 비활성화해 보세요.", + "tag-update-confirmation": "태그 업데이트를 진행하시겠습니까?", + "take-quick-product-tour": "시작하려면 제품 투어를 진행해 보세요!", + "team-distinct-user-description": "The total number of distinct users belongs to this team.", + "team-moved-success": "팀이 성공적으로 이동되었습니다!", + "team-no-asset": "귀하의 팀에는 자산이 없습니다.", + "test-case-schedule-description": "데이터 품질 테스트는 원하는 빈도로 예약 실행할 수 있습니다. 타임존은 UTC입니다.", + "test-connection-cannot-be-triggered": "연결 테스트를 실행할 수 없습니다.", + "test-connection-taking-too-long": "연결 테스트에 너무 오랜 시간이 소요되고 있습니다. 다시 시도하세요.", + "test-your-connection-before-creating-service": "서비스 생성 전에 연결 테스트를 진행하세요.", + "testing-your-connection-may-take-two-minutes": "연결 테스트에는 최대 2분이 소요될 수 있습니다.", + "this-action-is-not-allowed-for-deleted-entities": "삭제된 엔터티에 대해서는 이 작업을 수행할 수 없습니다.", + "this-will-pick-in-next-run": "다음 실행 시 반영됩니다.", + "thread-count-message": "메트릭 계산 시 사용할 스레드 수를 설정하세요. 입력하지 않으면 기본값 5가 사용됩니다.", + "to-add-new-line": "새 줄을 추가하려면", + "token-has-no-expiry": "이 토큰은 만료 날짜가 없습니다.", + "token-security-description": "JWT 토큰을 가진 사람은 OpenMetadata 서버에 REST API 요청을 보낼 수 있습니다. JWT 토큰을 애플리케이션 코드에 노출하거나 GitHub 등 공개 장소에 공유하지 마세요.", + "total-entity-insight": "유형별 최신 데이터 자산 수를 표시합니다.", + "tour-follow-step": "데이터 자산을 팔로우하여 해당 자산의 변경 사항을 실시간으로 확인하세요. 활동 피드에 팔로우 중인 자산의 모든 변경 사항이 표시되며, 데이터 품질 문제 발생 시 알림도 받게 됩니다.", + "tour-high-level-assets-information-step": "데이터 자산 상세 페이지에서 자산의 제목, 설명, 소유자, 티어, 사용량, 위치 등 모든 맥락을 360도 뷰로 확인하여 최적의 활용 방안을 모색하세요.", + "tour-owner-step": "여기서 데이터 자산의 소유자를 팀 또는 개인으로 설정할 수 있습니다. 데이터 소유자와 협업하며, 데이터를 이해하고 누락된 설명을 요청하거나 변경을 제안할 수 있습니다.", + "tour-step-activity-feed": "<0>{{text}}를 통해 조직 내 데이터 변화 현황을 파악하세요.", + "tour-step-click-on-entity-tab": "<0>\"{{text}}\" 탭을 클릭하세요.", + "tour-step-click-on-link-to-view-more": "자세한 정보를 보려면 <0>자산의 제목을 클릭하세요.", + "tour-step-discover-all-assets-at-one-place": "<0>{{text}}와 같은 중앙 집중식 메타데이터 저장소를 통해 모든 데이터 자산을 한 곳에서 확인하고, 팀과 협업하여 조직의 전체 데이터를 파악하세요.", + "tour-step-discover-data-assets-with-data-profile": "<0>{{text}}를 통해 자산을 확인하고, 테이블 사용 통계, 널 값, 중복, 열 데이터 분포 등을 파악하세요.", + "tour-step-explore-summary-asset": "<0>\"{{text}}\" 페이지에서 각 자산의 제목, 설명, 소유자, 티어(중요도), 사용량, 위치 등의 요약 정보를 확인하세요.", + "tour-step-get-to-know-table-schema": "테이블 <0>스키마를 확인하여 열 이름, 데이터 유형, 열 설명 및 태그를 파악하고, 구조체와 같은 복잡한 유형의 메타데이터도 확인하세요.", + "tour-step-look-at-sample-data": "<0>{{text}}를 살펴보며 테이블에 포함된 데이터를 확인하고 활용 방안을 모색하세요.", + "tour-step-search-for-matching-dataset": "검색 상자에 '이름', '설명', '열 이름' 등을 입력하여 일치하는 데이터 자산을 찾으세요: <0>{{text}}", + "tour-step-trace-path-across-tables": "<0>{{text}}를 사용하여 테이블, 파이프라인, 대시보드 간의 데이터 경로를 추적하세요.", + "tour-step-type-search-term": "검색 상자에 <0>\"{{text}}\"를 입력하고 <0>{{enterText}}를 누르세요.", + "try-adjusting-filter": "원하는 항목을 찾기 위해 검색어나 필터를 조정해 보세요.", + "try-different-time-period-filtering": "결과가 없습니다. 다른 기간으로 필터링해 보세요.", + "try-extending-time-frame": "결과를 확인하려면 시간 범위를 확장해 보세요.", + "type-delete-to-confirm": "확인을 위해 <0>DELETE를 입력하세요.", + "unable-to-connect-to-your-dbt-cloud-instance": "dbt 클라우드 인스턴스에 연결할 URL입니다. 예: \n https://cloud.getdbt.com 또는 https://emea.dbt.com/", + "unable-to-error-elasticsearch": "엔터티 인덱스를 위해 Elasticsearch에서 {{error}}할 수 없습니다.", + "uninstall-app": "이 {{app}} 애플리케이션을 제거하면 OpenMetadata에서 삭제됩니다.", + "unix-epoch-time-in-ms": "{{prefix}} 밀리초 단위의 Unix 에포크 시간", + "update-description-message": "설명을 업데이트하라는 요청:", + "update-displayName-entity": "{{entity}}의 표시 이름을 업데이트하세요.", + "update-profiler-settings": "프로파일러 설정을 업데이트하세요.", + "update-tag-message": "태그 업데이트 요청:", + "updating-existing-not-possible-can-add-new-values": "기존 값을 업데이트하는 것은 불가능하며, 새로운 값 추가만 허용됩니다.", + "upload-file": "파일 업로드", + "upstream-depth-message": "업스트림 깊이에 대한 값을 선택하세요.", + "upstream-depth-tooltip": "출처(상위 레벨)를 식별하기 위해 최대 3개의 업스트림 노드를 표시합니다.", + "usage-ingestion-description": "메타데이터 수집 설정 후 사용량 수집을 구성 및 배포할 수 있습니다. 사용량 수집 워크플로우는 기본 데이터베이스에서 쿼리 로그와 테이블 생성 세부 정보를 가져와 OpenMetadata로 전송합니다. 한 데이터베이스 서비스에 대해 메타데이터 및 사용량 수집 파이프라인은 하나만 설정할 수 있습니다. 시작하려면 쿼리 로그 기간(일), 스테이지 파일 위치, 결과 제한을 정의하세요.", + "use-fqn-for-filtering-message": "정규식은 원시 이름(예: table_name) 대신 전체 이름(예: service_name.db_name.schema_name.table_name)에 적용됩니다.", + "user-assign-new-task": "{{user}}이(가) 귀하에게 새 작업을 할당했습니다.", + "user-mentioned-in-comment": "{{user}}이(가) 댓글에서 귀하를 언급했습니다.", + "user-verified-successfully": "사용자 인증이 성공적으로 완료되었습니다.", + "valid-url-endpoint": "엔드포인트는 유효한 URL이어야 합니다.", + "validation-error-assets": "추가 중인 모든 자산을 확인하세요.", + "value-should-equal-to-value": "값은 {{value}}와 같아야 합니다.", + "value-should-not-equal-to-value": "값은 {{value}}와 같으면 안 됩니다.", + "version-released-try-now": "{{version}} 버전이 출시되었습니다. <0>새 소식을 확인하세요!", + "view-deleted-entity": "이 {{parent}}에 속한 모든 삭제된 {{entity}}을(를) 확인하세요.", + "view-sample-data-entity": "샘플 데이터를 보려면 {{entity}}을(를) 실행하세요. 스케줄 설정 방법은 <0>{{entity}} 문서를 참조하세요.", + "view-test-suite": "테스트 스위트 보기", + "viewing-older-version": "이전 버전을 보고 있습니다.\n최신 버전으로 돌아가 세부 정보를 업데이트하세요.", + "webhook-listing-message": "웹훅을 사용하면 API를 통해 조직 내 메타데이터 변경 이벤트에 대해 외부 서비스에 알림을 보낼 수 있습니다. 웹훅 통합으로 콜백 URL을 등록하여 메타데이터 이벤트 알림을 받으세요. 웹훅을 추가, 목록 확인, 업데이트 및 삭제할 수 있습니다.", + "webhook-type-listing-message": "{{webhookType}} 알림을 통해 메타데이터 생산자와 소비자에게 시기적절한 업데이트를 제공하세요. {{webhookType}} 웹훅을 사용하여 조직 내 메타데이터 변경 이벤트에 대해 알림을 전송할 수 있습니다. 이러한 웹훅을 추가, 목록 확인, 업데이트 및 삭제할 수 있습니다.", + "welcome-screen-message": "모든 데이터를 한 곳에서 확인하고, 신뢰할 수 있는 데이터를 바탕으로 팀과 원활하게 협업하세요.", + "welcome-to-om": "OpenMetadata에 오신 것을 환영합니다!", + "welcome-to-open-metadata": "OpenMetadata에 오신 것을 환영합니다!", + "would-like-to-start-adding-some": "데이터를 추가해 보시겠습니까?", + "write-your-announcement-lowercase": "공지사항을 작성하세요", + "write-your-description": "설명을 작성하세요", + "write-your-text": "{{text}}을(를) 작성하세요", + "you-can-also-set-up-the-metadata-ingestion": "메타데이터 수집도 설정할 수 있습니다." + }, + "server": { + "account-verify-success": "이메일 인증이 성공적으로 완료되었습니다", + "add-entity-error": "{{entity}} 추가 중 오류 발생!", + "auth-provider-not-supported-renewing": "토큰 갱신을 위한 인증 공급자 {{provider}}은(는) 지원되지 않습니다.", + "can-not-renew-token-authentication-not-present": "id 토큰을 갱신할 수 없습니다. 인증 공급자가 없습니다.", + "column-fetch-error": "열 테스트 케이스를 가져오는 중 오류 발생!", + "connection-tested-successfully": "연결 테스트가 성공적으로 완료되었습니다", + "create-entity-error": "{{entity}} 생성 중 오류 발생!", + "create-entity-success": "{{entity}}이(가) 성공적으로 생성되었습니다.", + "delete-entity-error": "\"{{entity}}\" 삭제 중 오류 발생.", + "deploy-entity-error": "{{entity}} 배포 중 오류 발생!", + "email-already-exist": "\"{{name}}\" 이름의 {{entity}}이(가) 이미 존재합니다. 다른 이메일을 선택하세요.", + "email-confirmation": "이메일을 확인해 주세요. 확인 메일이 전송되었습니다.", + "email-found": "해당 이메일 주소를 가진 사용자가 이미 존재합니다!", + "email-not-found": "해당 이메일 주소를 가진 사용자가 존재하지 않습니다!", + "email-verification-error": "이메일 인증 메일 전송 중 문제가 발생했습니다. 관리자에게 문의하세요.", + "entity-already-exist": "\"{{name}}\" 이름의 {{entity}}이(가) 이미 존재합니다. 중복 {{entityPlural}}은 허용되지 않습니다.", + "entity-already-exist-message-without-name": "입력한 정보로 {{entity}}이(가) 이미 존재합니다. 중복 {{entityPlural}}은 허용되지 않습니다.", + "entity-creation-error": "{{entity}} 생성 중 오류 발생", + "entity-deleted-successfully": "\"{{entity}}\"이(가) 성공적으로 삭제되었습니다!", + "entity-details-fetch-error": "{{entityType}} {{entityName}} 상세 정보 가져오는 중 오류 발생", + "entity-feed-fetch-error": "엔터티 피드 수를 가져오는 중 오류 발생!", + "entity-fetch-error": "{{entity}} 가져오는 중 오류 발생", + "entity-fetch-version-error": "{{version}} 버전의 {{entity}} 가져오는 중 오류 발생", + "entity-follow-error": "{{entity}} 팔로우 중 오류 발생", + "entity-limit-reached": "{{entity}} 제한에 도달했습니다", + "entity-removing-error": "{{entity}} 제거 중 오류 발생", + "entity-unfollow-error": "{{entity}} 언팔로우 중 오류 발생", + "entity-updating-error": "{{entity}} 업데이트 중 오류 발생", + "error-selected-node-name-details": "{{selectedNodeName}} 상세 정보 가져오는 중 오류 발생", + "error-while-renewing-id-token-with-message": "Auth0 SSO에서 id 토큰 갱신 중 오류 발생: {{message}}", + "feed-post-error": "메시지 게시 중 오류 발생!", + "fetch-entity-permissions-error": "{{entity}}에 대한 권한을 가져올 수 없습니다.", + "fetch-re-index-data-error": "재인덱스 데이터를 가져오는 중 오류 발생!", + "fetch-table-profiler-config-error": "테이블 프로파일러 구성을 가져오는 중 오류 발생!", + "fetch-updated-conversation-error": "업데이트된 대화 가져오는 중 오류 발생!", + "forgot-password-email-error": "이메일 전송 중 문제가 발생했습니다. 관리자에게 문의하세요.", + "indexing-error": "인덱싱 중 오류 발생", + "ingestion-workflow-operation-error": "{{displayName}} 수집 워크플로우 {{operation}} 중 오류 발생", + "invalid-username-or-password": "잘못된 사용자명 또는 비밀번호입니다.", + "join-team-error": "팀 가입 중 오류 발생!", + "join-team-success": "팀에 성공적으로 가입했습니다!", + "leave-team-error": "팀 탈퇴 중 오류 발생!", + "leave-team-success": "팀에서 성공적으로 탈퇴했습니다!", + "no-application-schema-found": "No application schema found for {{appName}}", + "no-owned-entities": "아직 소유한 항목이 없습니다.", + "no-query-available": "사용 가능한 쿼리가 없습니다.", + "no-task-available": "작업 데이터가 없습니다.", + "no-task-creation-without-assignee": "담당자 없이 작업을 생성할 수 없습니다", + "page-layout-operation-error": "페이지 레이아웃 {{operation}} 중 오류 발생.", + "page-layout-operation-success": "페이지 레이아웃이 {{operation}}되었습니다.", + "please-add-description": "빈 설명은 허용되지 않습니다. 설명을 추가하세요.", + "please-add-tags": "빈 태그 목록은 허용되지 않습니다. 태그를 추가하세요.", + "re-indexing-error": "재인덱싱 중 오류 발생!", + "re-indexing-started": "재인덱싱 시작됨", + "re-indexing-stopped": "재인덱싱 중지됨", + "reset-password-success": "비밀번호가 성공적으로 재설정되었습니다!", + "stop-re-indexing-error": "재인덱싱 중지 시 오류가 발생했습니다!", + "task-closed-successfully": "작업이 성공적으로 닫혔습니다.", + "task-closed-without-comment": "댓글 없이 작업을 닫을 수 없습니다.", + "task-resolved-successfully": "작업이 성공적으로 해결되었습니다", + "team-moved-error": "팀 이동 중 오류 발생", + "test-connection-error": "연결 테스트 중 오류 발생!", + "unauthorized-user": "권한이 없는 사용자입니다! 이메일 또는 비밀번호를 확인하세요.", + "unexpected-error": "예기치 않은 오류가 발생했습니다.", + "unexpected-response": "서버로부터 예기치 않은 응답이 있었습니다.", + "update-entity-success": "{{entity}}이(가) 성공적으로 업데이트되었습니다.", + "you-have-not-action-anything-yet": "아직 아무 것도 {{action}}하지 않았습니다." } - \ No newline at end of file +} From 6676e2443c22b63af17302c07664c907c174744a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:14:50 +0530 Subject: [PATCH 58/63] update function signature --- .../components/DataAssets/CommonWidgets/CommonWidgets.tsx | 5 ++++- .../ui/src/utils/APIEndpoints/APIEndpointClassBase.ts | 3 ++- .../ui/src/utils/CommonWidget/CommonWidgetClassBase.ts | 6 ++++-- .../resources/ui/src/utils/ContainerDetailsClassBase.ts | 3 ++- .../main/resources/ui/src/utils/DashboardDataModelBase.ts | 3 ++- .../resources/ui/src/utils/DashboardDetailsClassBase.ts | 3 ++- .../resources/ui/src/utils/Database/DatabaseClassBase.ts | 3 ++- .../main/resources/ui/src/utils/DatabaseSchemaClassBase.ts | 3 ++- .../main/resources/ui/src/utils/Domain/DomainClassBase.ts | 3 ++- .../src/utils/MetricEntityUtils/MetricDetailsClassBase.ts | 3 ++- .../main/resources/ui/src/utils/MlModel/MlModelClassBase.ts | 3 ++- .../src/main/resources/ui/src/utils/PipelineClassBase.ts | 3 ++- .../resources/ui/src/utils/SearchIndexDetailsClassBase.ts | 3 ++- .../src/main/resources/ui/src/utils/StoredProcedureBase.ts | 3 ++- .../src/main/resources/ui/src/utils/TableClassBase.ts | 3 ++- .../src/main/resources/ui/src/utils/TopicClassBase.ts | 3 ++- 16 files changed, 36 insertions(+), 17 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index bce7aa0d8c2a..5b0e2aa2f8fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -234,7 +234,10 @@ export const CommonWidgets = ({ return />; } - return commonWidgetClassBase.getCommonWidgetsFromConfig(widgetConfig); + const Widget = + commonWidgetClassBase.getCommonWidgetsFromConfig(widgetConfig); + + return Widget ? : null; }, [widgetConfig, descriptionWidget, glossaryWidget, tagsWidget]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts index 8b865ed3230c..d678bbf92a29 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -82,7 +83,7 @@ class APIEndpointClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.SCHEMA) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonWidget/CommonWidgetClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CommonWidget/CommonWidgetClassBase.ts index 67a2d4146ddb..70a5c981eec6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonWidget/CommonWidgetClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonWidget/CommonWidgetClassBase.ts @@ -13,8 +13,10 @@ import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; export class CommonWidgetClassBase { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public getCommonWidgetsFromConfig(_widgetConfig: WidgetConfig) { + public getCommonWidgetsFromConfig( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _widgetConfig: WidgetConfig + ): null | React.FC { // this will be overridden to show additional widgets return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index 36dcab59e606..e1d9ab5ceaf3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -75,7 +76,7 @@ class ContainerDetailsClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && ![EntityTabs.CHILDREN, EntityTabs.SCHEMA].includes(tab)) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts index 77b718a9f289..f0c251157855 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -70,7 +71,7 @@ class DashboardDataModelBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.MODEL) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index 2d0150f7ce5a..c72a7a47b2a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -71,7 +72,7 @@ class DashboardDetailsClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.DETAILS) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index c88b768c5b31..d445c9e6d8b0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -72,7 +73,7 @@ class DatabaseClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.SCHEMA) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index c98c5635713b..88addfa3a545 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -74,7 +75,7 @@ class DatabaseSchemaClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.TABLE) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 36ee44dc800d..46fc1401e875 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { DataProductsTabRef } from '../../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.interface'; import { EntityDetailsObjectInterface } from '../../components/Explore/ExplorePage.interface'; @@ -82,7 +83,7 @@ class DomainClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.DOCUMENTATION) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts index c0c981972525..a5965618ca16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -83,7 +84,7 @@ class MetricDetailsClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.OVERVIEW) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts index 3894aa702f41..d83e561abecc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -82,7 +83,7 @@ class MlModelDetailsClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.OVERVIEW) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index e239821e1a71..ef1198a631e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -80,7 +81,7 @@ class PipelineClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.TASKS) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts index e7a2c6908e62..641dc5e6edf4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -82,7 +83,7 @@ class SearchIndexClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.FIELDS) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts index 40772b5bbe40..d03a6aae8550 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -82,7 +83,7 @@ class StoredProcedureClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.CODE) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index fad87897a051..9179a689e95b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -90,7 +91,7 @@ class TableClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.SCHEMA) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts index 2d7d4e3d757b..8e09b5e2411e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -76,7 +77,7 @@ class TopicClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.SCHEMA) { return []; } From 0084844b70078dd1d9af79fe5e9721cf5e10b69b Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:46:06 +0530 Subject: [PATCH 59/63] update file names and fix imports --- .../components/Customization/GenericWidget/GenericWidget.tsx | 2 +- .../ui/src/utils/APICollection/APICollectionClassBase.ts | 3 ++- .../ui/src/utils/CustomizePage/CustomizePageUtils.ts | 4 ++-- ...shboardDataModelBase.ts => DashboardDataModelClassBase.ts} | 0 .../main/resources/ui/src/utils/DashboardDataModelUtils.tsx | 2 +- .../{StoredProcedureBase.ts => StoredProcedureClassBase.ts} | 0 .../src/main/resources/ui/src/utils/StoredProceduresUtils.tsx | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) rename openmetadata-ui/src/main/resources/ui/src/utils/{DashboardDataModelBase.ts => DashboardDataModelClassBase.ts} (100%) rename openmetadata-ui/src/main/resources/ui/src/utils/{StoredProcedureBase.ts => StoredProcedureClassBase.ts} (100%) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx index 821762599430..1861e32ee5fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Customization/GenericWidget/GenericWidget.tsx @@ -36,7 +36,7 @@ import { FrequentlyJoinedTables } from '../../../pages/TableDetailsPageV1/Freque import TableConstraints from '../../../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import containerDetailsClassBase from '../../../utils/ContainerDetailsClassBase'; import customizeGlossaryTermPageClassBase from '../../../utils/CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; -import dashboardDataModelClassBase from '../../../utils/DashboardDataModelBase'; +import dashboardDataModelClassBase from '../../../utils/DashboardDataModelClassBase'; import domainClassBase from '../../../utils/Domain/DomainClassBase'; import { renderReferenceElement } from '../../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts index 0e4db3dd5156..f9b5ee79aadd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { CUSTOM_PROPERTIES_WIDGET, @@ -78,7 +79,7 @@ class APICollectionClassBase { })); } - public getDefaultLayout(tab?: EntityTabs) { + public getDefaultLayout(tab?: EntityTabs): Layout[] { if (tab && tab !== EntityTabs.API_ENDPOINT) { return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts index 97f4b02cf03a..dc48a9abac51 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizePage/CustomizePageUtils.ts @@ -21,7 +21,7 @@ import apiEndpointClassBase from '../APIEndpoints/APIEndpointClassBase'; import containerDetailsClassBase from '../ContainerDetailsClassBase'; import customizeGlossaryPageClassBase from '../CustomizeGlossaryPage/CustomizeGlossaryPage'; import customizeGlossaryTermPageClassBase from '../CustomizeGlossaryTerm/CustomizeGlossaryTermBaseClass'; -import dashboardDataModelClassBase from '../DashboardDataModelBase'; +import dashboardDataModelClassBase from '../DashboardDataModelClassBase'; import dashboardDetailsClassBase from '../DashboardDetailsClassBase'; import databaseClassBase from '../Database/DatabaseClassBase'; import databaseSchemaClassBase from '../DatabaseSchemaClassBase'; @@ -32,7 +32,7 @@ import metricDetailsClassBase from '../MetricEntityUtils/MetricDetailsClassBase' import mlModelClassBase from '../MlModel/MlModelClassBase'; import pipelineClassBase from '../PipelineClassBase'; import searchIndexClassBase from '../SearchIndexDetailsClassBase'; -import storedProcedureClassBase from '../StoredProcedureBase'; +import storedProcedureClassBase from '../StoredProcedureClassBase'; import tableClassBase from '../TableClassBase'; import topicClassBase from '../TopicClassBase'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts similarity index 100% rename from openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelBase.ts rename to openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx index 63e51faae84d..df4aaaa2745a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelUtils.tsx @@ -29,7 +29,7 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum'; import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; -import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelBase'; +import { DashboardDataModelDetailPageTabProps } from './DashboardDataModelClassBase'; import i18n from './i18next/LocalUtil'; export const getDashboardDataModelDetailPageTabs = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts similarity index 100% rename from openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureBase.ts rename to openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 6a8cb538527b..d22d72ea1c8f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -25,7 +25,7 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { PageType } from '../generated/system/ui/page'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; -import { StoredProcedureDetailPageTabProps } from './StoredProcedureBase'; +import { StoredProcedureDetailPageTabProps } from './StoredProcedureClassBase'; // eslint-disable-next-line max-len export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS}, ${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS}, ${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; From 14674ca4cd9ad76dd186e2eb157980e7a3e5d1f6 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:54:12 +0530 Subject: [PATCH 60/63] address comments --- .../APIEndpointDetails/APIEndpointDetails.tsx | 27 +- .../GenericWidget/GenericWidget.tsx | 7 +- .../DashboardDetails.component.tsx | 26 +- .../DataModels/DataModelDetails.component.tsx | 29 +- .../DomainDetailsPage.component.tsx | 26 +- .../GlossaryDetails.component.tsx | 25 +- .../GlossaryTermsV1.component.tsx | 27 +- .../Metric/MetricDetails/MetricDetails.tsx | 27 +- .../MlModelDetail/MlModelDetail.component.tsx | 27 +- .../PipelineDetails.component.tsx | 26 +- .../TopicDetails/TopicDetails.component.tsx | 27 +- .../src/constants/APICollection.constnats.ts | 217 +++++++++++++ .../ui/src/constants/Contianer.constants.ts | 70 ++++ .../ui/src/constants/Dashboard.constnats.ts | 178 ++++++++++ .../ui/src/constants/Database.constants.ts | 148 +++++++++ .../ui/src/constants/Domain.constants.ts | 45 +++ .../ui/src/constants/Metric.constnsts.ts | 48 +++ .../ui/src/constants/MlModel.constants.ts | 162 ++++++++++ .../ui/src/constants/SearchIndex.constants.ts | 150 +++++++++ .../ui/src/constants/Table.constants.ts | 305 ++++++++++++++++++ .../ui/src/constants/Topic.constant.ts | 150 +++++++++ .../ui/src/constants/pipeline.constants.ts | 93 ++++++ .../ui/src/enums/CustomizeDetailPage.enum.ts | 1 + .../resources/ui/src/hooks/useCustomPages.ts | 47 +++ .../APICollectionPage/APICollectionPage.tsx | 29 +- .../src/pages/ContainerPage/ContainerPage.tsx | 29 +- .../DatabaseDetailsPage.tsx | 26 +- .../DatabaseSchemaPage.component.tsx | 29 +- .../SearchIndexDetailsPage.tsx | 27 +- .../ServiceMainTabContent.tsx | 33 +- .../StoredProcedure/StoredProcedurePage.tsx | 29 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 26 +- .../APICollection/APICollectionClassBase.ts | 37 +-- .../APIEndpoints/APIEndpointClassBase.ts | 182 +---------- .../ui/src/utils/ContainerDetailsClassBase.ts | 66 +--- .../src/utils/DashboardDataModelClassBase.ts | 96 +----- .../ui/src/utils/DashboardDetailsClassBase.ts | 77 +---- .../src/utils/Database/DatabaseClassBase.ts | 112 +------ .../ui/src/utils/DatabaseSchemaClassBase.ts | 131 +------- .../ui/src/utils/Domain/DomainClassBase.ts | 48 +-- .../MetricDetailsClassBase.ts | 39 +-- .../ui/src/utils/MlModel/MlModelClassBase.ts | 180 +---------- .../ui/src/utils/MlModelDetailsUtils.tsx | 2 +- .../ui/src/utils/PipelineClassBase.ts | 143 +------- .../src/utils/SearchIndexDetailsClassBase.ts | 192 +---------- .../ui/src/utils/StoredProcedureClassBase.ts | 122 +------ .../resources/ui/src/utils/TableClassBase.ts | 239 +------------- .../resources/ui/src/utils/TopicClassBase.ts | 150 +-------- 48 files changed, 1746 insertions(+), 2186 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/APICollection.constnats.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Contianer.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Dashboard.constnats.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Database.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Metric.constnsts.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/MlModel.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/SearchIndex.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Topic.constant.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx index ca2eaad83d6a..87103cbc315f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/APIEndpoint/APIEndpointDetails/APIEndpointDetails.tsx @@ -16,19 +16,18 @@ import { AxiosError } from 'axios'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { APIEndpoint } from '../../../generated/entity/data/apiEndpoint'; -import { Page, PageType } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/page'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { restoreApiEndPoint } from '../../../rest/apiEndpointsAPI'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import apiEndpointClassBase from '../../../utils/APIEndpoints/APIEndpointClassBase'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { @@ -59,7 +58,7 @@ const APIEndpointDetails: React.FC = ({ onUpdateVote, }: APIEndpointDetailsProps) => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedApiEndpointFqn } = useFqn(); @@ -67,7 +66,7 @@ const APIEndpointDetails: React.FC = ({ const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.APIEndpoint); const { owners, @@ -229,24 +228,6 @@ const APIEndpointDetails: React.FC = ({ editLineagePermission, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.APIEndpoint) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( { data-testid="drag-widget-button" size={14} /> - } + size="small" onClick={handleRemoveClick} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx index 9db5fba7b96e..0c98eb8b2234 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardDetails/DashboardDetails.component.tsx @@ -16,7 +16,6 @@ import { AxiosError } from 'axios'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; @@ -24,14 +23,13 @@ import { ResourceEntity } from '../../../context/PermissionProvider/PermissionPr import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Dashboard } from '../../../generated/entity/data/dashboard'; -import { Page } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/uiCustomization'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { restoreDashboard } from '../../../rest/dashboardAPI'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { getDetailsTabWithNewLabel, @@ -60,11 +58,11 @@ const DashboardDetails = ({ handleToggleDelete, }: DashboardDetailsProps) => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const history = useHistory(); const { tab: activeTab = EntityTabs.DETAILS } = useParams<{ tab: EntityTabs }>(); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Dashboard); const { fqn: decodedDashboardFQN } = useFqn(); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -262,24 +260,6 @@ const DashboardDetails = ({ onExtensionUpdate, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Dashboard) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( (); const { fqn: decodedDataModelFQN } = useFqn(); - const [customizedPage, setCustomizedPage] = useState(null); - const { selectedPersona } = useApplicationStore(); + const { customizedPage } = useCustomPages(PageType.DashboardDataModel); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -191,26 +188,6 @@ const DataModelDetails = ({ editLineagePermission, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find( - (p: Page) => p.pageType === PageType.DashboardDataModel - ) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( (null); + const { customizedPage } = useCustomPages(PageType.Domain); const isSubDomain = useMemo(() => !isEmpty(domain.parent), [domain]); @@ -563,24 +561,6 @@ const DomainDetailsPage = ({ fetchDataProducts(); }, [domain.fullyQualifiedName]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Domain) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - useEffect(() => { fetchSubDomains(); }, [domainFqn]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx index 5b3a416daa6d..5c1cf89bdc0a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.component.tsx @@ -18,7 +18,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import RGL, { WidthProvider } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getGlossaryTermDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { EntityField } from '../../../constants/Feeds.constants'; @@ -29,12 +28,11 @@ import { ChangeDescription } from '../../../generated/entity/type'; import { Page, PageType, Tab } from '../../../generated/system/ui/page'; import { TagLabel } from '../../../generated/tests/testCase'; import { TagSource } from '../../../generated/type/tagLabel'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection'; import { FeedCounts } from '../../../interface/feed.interface'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import { useCustomizeStore } from '../../../pages/CustomizablePage/CustomizeStore'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import customizeGlossaryPageClassBase from '../../../utils/CustomizeGlossaryPage/CustomizeGlossaryPage'; import { @@ -81,12 +79,11 @@ const GlossaryDetails = ({ const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const { selectedPersona } = useApplicationStore(); const { currentPersonaDocStore } = useCustomizeStore(); // Since we are rendering this component for all customized tabs we need tab ID to get layout form store const { tab: activeTab = EntityTabs.TERMS } = useParams<{ tab: EntityTabs }>(); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Glossary); useGridLayoutDirection(); @@ -342,24 +339,6 @@ const GlossaryDetails = ({ getEntityFeedCount(); }, [glossary.fullyQualifiedName]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Glossary) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( data={updatedGlossary} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index f074d676d4e1..b31b96752860 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -22,7 +22,6 @@ import React, { useState, } from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getGlossaryTermDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { EntityField } from '../../../constants/Feeds.constants'; @@ -36,12 +35,11 @@ import { GlossaryTerm, Status, } from '../../../generated/entity/data/glossaryTerm'; -import { Page, PageType } from '../../../generated/system/ui/page'; -import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { PageType } from '../../../generated/system/ui/page'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; import { MOCK_GLOSSARY_NO_PERMISSIONS } from '../../../mocks/Glossary.mock'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { searchData } from '../../../rest/miscAPI'; import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils'; import { @@ -91,11 +89,10 @@ const GlossaryTermsV1 = ({ const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const { selectedPersona } = useApplicationStore(); const [assetCount, setAssetCount] = useState(0); const { glossaryChildTerms } = useGlossaryStore(); const childGlossaryTerms = glossaryChildTerms ?? []; - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.GlossaryTerm); const assetPermissions = useMemo(() => { const glossaryTermStatus = glossaryTerm.status ?? Status.Approved; @@ -330,24 +327,6 @@ const GlossaryTermsV1 = ({ getEntityFeedCount(); }, [glossaryFqn]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.GlossaryTerm) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - const updatedGlossaryTerm = useMemo(() => { const name = isVersionView ? getEntityVersionByField( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx index f8933c672f7d..f85c1f0dbd1e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Metric/MetricDetails/MetricDetails.tsx @@ -16,18 +16,17 @@ import { AxiosError } from 'axios'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath, ROUTES } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Metric } from '../../../generated/entity/data/metric'; -import { Page, PageType } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/page'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restoreMetric } from '../../../rest/metricsAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { @@ -57,7 +56,7 @@ const MetricDetails: React.FC = ({ onUpdateVote, }: MetricDetailsProps) => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const { tab: activeTab = EntityTabs.OVERVIEW } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedMetricFqn } = useFqn(); @@ -71,7 +70,7 @@ const MetricDetails: React.FC = ({ deleted, followers = [], } = useMemo(() => metricDetails, [metricDetails]); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Metric); const { isFollowing } = useMemo( () => ({ @@ -214,24 +213,6 @@ const MetricDetails: React.FC = ({ viewAllPermission, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Metric) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( = ({ onMlModelUpdate, }) => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const history = useHistory(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.MlModel); const { fqn: decodedMlModelFqn } = useFqn(); @@ -361,24 +360,6 @@ const MlModelDetail: FC = ({ customizedPage?.tabs, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.MlModel) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - return ( (); const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const userID = currentUser?.id ?? ''; const { deleted, owners, description, entityName, tier, followers } = useMemo(() => { @@ -87,7 +85,7 @@ const PipelineDetails = ({ }, [pipelineDetails]); // local state variables - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Pipeline); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -303,24 +301,6 @@ const PipelineDetails = ({ viewAllPermission, ]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Pipeline) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - useEffect(() => { getEntityFeedCount(); }, [pipelineFQN]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx index 9f5f273e7e83..e782260a09d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Topic/TopicDetails/TopicDetails.component.tsx @@ -17,7 +17,6 @@ import { EntityTags } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { getEntityDetailsPath } from '../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants'; import LineageProvider from '../../../context/LineageProvider/LineageProvider'; @@ -26,13 +25,12 @@ import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { Tag } from '../../../generated/entity/classification/tag'; import { Topic } from '../../../generated/entity/data/topic'; import { DataProduct } from '../../../generated/entity/domains/dataProduct'; -import { Page, PageType } from '../../../generated/system/ui/page'; +import { PageType } from '../../../generated/system/ui/page'; import { TagLabel } from '../../../generated/type/schema'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { FeedCounts } from '../../../interface/feed.interface'; -import { getDocumentByFQN } from '../../../rest/DocStoreAPI'; import { restoreTopic } from '../../../rest/topicsAPI'; import { getFeedCounts } from '../../../utils/CommonUtils'; import { @@ -56,6 +54,7 @@ import { DataAssetsHeader } from '../../DataAssets/DataAssetsHeader/DataAssetsHe import SampleDataWithMessages from '../../Database/SampleDataWithMessages/SampleDataWithMessages'; import Lineage from '../../Lineage/Lineage.component'; +import { useCustomPages } from '../../../hooks/useCustomPages'; import QueryViewer from '../../common/QueryViewer/QueryViewer.component'; import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; @@ -74,12 +73,12 @@ const TopicDetails: React.FC = ({ onUpdateVote, }: TopicDetailsProps) => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedTopicFQN } = useFqn(); const history = useHistory(); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Topic); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA @@ -274,24 +273,6 @@ const TopicDetails: React.FC = ({ [topicPermissions, deleted] ); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Topic) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - useEffect(() => { getEntityFeedCount(); }, [topicPermissions, decodedTopicFQN]); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/APICollection.constnats.ts b/openmetadata-ui/src/main/resources/ui/src/constants/APICollection.constnats.ts new file mode 100644 index 000000000000..4de25ba80d03 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/APICollection.constnats.ts @@ -0,0 +1,217 @@ +/* + * Copyright 2025 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 { APIServiceType } from '../generated/entity/data/apiCollection'; + +import { APICollection } from '../generated/entity/data/apiCollection'; +import { + APIEndpoint, + APIRequestMethod, + DataTypeTopic, + SchemaType, +} from '../generated/entity/data/apiEndpoint'; + +export const API_COLLECTION_DUMMY_DATA: APICollection = { + id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', + name: 'pet', + fullyQualifiedName: 'sample_api_service.pet', + version: 0.1, + updatedAt: 1722588116104, + updatedBy: 'ingestion-bot', + endpointURL: 'https://petstore3.swagger.io/#/pet', + owners: [], + tags: [], + service: { + id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', + type: 'apiService', + name: 'sample_api_service', + fullyQualifiedName: 'sample_api_service', + displayName: 'sample_api_service', + deleted: false, + }, + serviceType: APIServiceType.REST, + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; + +export const API_ENDPOINT_DUMMY_DATA: APIEndpoint = { + id: 'b41d3506-09ac-4e02-ae40-8d6933f6a77f', + name: 'addPet', + displayName: 'Add Pet', + fullyQualifiedName: 'sample_api_service.pet.addPet', + description: 'add a new pet', + version: 0.5, + updatedAt: 1723268606694, + updatedBy: 'sachin', + endpointURL: 'https://petstore3.swagger.io/#/pet/addPet', + requestMethod: APIRequestMethod.Post, + requestSchema: { + schemaType: SchemaType.JSON, + schemaFields: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of pet that needs to be updated', + fullyQualifiedName: 'sample_api_service.pet.addPet.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.name', + tags: [], + }, + { + name: 'category', + dataType: DataTypeTopic.Record, + description: 'Category of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.category', + tags: [], + children: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.name', + tags: [], + }, + ], + }, + { + name: 'photoUrls', + dataType: DataTypeTopic.Array, + description: "URLs of pet's photos", + fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', + tags: [], + }, + { + name: 'tags', + dataType: DataTypeTopic.Array, + description: 'Tags of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.tags', + tags: [], + }, + { + name: 'status', + dataType: DataTypeTopic.String, + description: 'Status of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.status', + tags: [], + }, + ], + }, + responseSchema: { + schemaType: SchemaType.JSON, + schemaFields: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of pet that needs to be updated', + fullyQualifiedName: 'sample_api_service.pet.addPet.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.name', + tags: [], + }, + { + name: 'category', + dataType: DataTypeTopic.Record, + description: 'Category of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.category', + tags: [], + children: [ + { + name: 'id', + dataType: DataTypeTopic.Int, + description: 'ID of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', + tags: [], + }, + { + name: 'name', + dataType: DataTypeTopic.String, + description: 'Name of category', + fullyQualifiedName: 'sample_api_service.pet.addPet.category.name', + tags: [], + }, + ], + }, + { + name: 'photoUrls', + dataType: DataTypeTopic.Array, + description: "URLs of pet's photos", + fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', + tags: [], + }, + { + name: 'tags', + dataType: DataTypeTopic.Array, + description: 'Tags of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.tags', + tags: [], + }, + { + name: 'status', + dataType: DataTypeTopic.String, + description: 'Status of pet', + fullyQualifiedName: 'sample_api_service.pet.addPet.status', + tags: [], + }, + ], + }, + apiCollection: { + id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', + type: 'apiCollection', + name: 'pet', + fullyQualifiedName: 'sample_api_service.pet', + displayName: 'pet', + deleted: false, + }, + owners: [], + followers: [], + tags: [], + service: { + id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', + type: 'apiService', + name: 'sample_api_service', + fullyQualifiedName: 'sample_api_service', + displayName: 'sample_api_service', + deleted: false, + }, + serviceType: APIServiceType.REST, + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Contianer.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Contianer.constants.ts new file mode 100644 index 000000000000..b9c9f1d98dbe --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Contianer.constants.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2025 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 { + FileFormat, + StorageServiceType, +} from '../generated/entity/data/container'; + +export const CONTAINER_DUMMY_DATA = { + id: '4e90debf-d063-49fd-9a5d-71ee43e6840a', + name: 'departments', + fullyQualifiedName: 's3_storage_sample.departments', + displayName: 'Company departments', + description: 'Bucket containing company department information. asd', + version: 0.3, + updatedAt: 1722838506844, + updatedBy: 'sachin', + service: { + id: '5354aaf3-063e-47aa-9f1d-bae19755e905', + type: 'storageService', + name: 's3_storage_sample', + fullyQualifiedName: 's3_storage_sample', + displayName: 's3_storage_sample', + deleted: false, + }, + children: [ + { + id: '11e8f1c5-77c8-4a27-a546-c6561baeba18', + type: 'container', + name: 'engineering', + fullyQualifiedName: 's3_storage_sample.departments.engineering', + description: 'Bucket containing engineering department information', + displayName: 'Engineering department', + deleted: false, + }, + { + id: 'c704e3d2-33ec-4cf0-a3fc-5e8d181c2723', + type: 'container', + name: 'finance', + fullyQualifiedName: 's3_storage_sample.departments.finance', + description: 'Bucket containing finance department information', + displayName: 'Finance department', + deleted: false, + }, + { + id: 'ffe5b6be-57cd-4cdc-9e0a-09677658160c', + type: 'container', + name: 'media', + fullyQualifiedName: 's3_storage_sample.departments.media', + description: 'Bucket containing media department information', + displayName: 'Media department', + deleted: false, + }, + ], + prefix: '/departments/', + numberOfObjects: 2, + size: 2048, + fileFormats: [FileFormat.CSV], + serviceType: StorageServiceType.S3, + deleted: false, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Dashboard.constnats.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Dashboard.constnats.ts new file mode 100644 index 000000000000..95d8b0ddab28 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Dashboard.constnats.ts @@ -0,0 +1,178 @@ +/* + * Copyright 2025 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 { + Dashboard, + DashboardServiceType, +} from '../generated/entity/data/dashboard'; +import { + DashboardDataModel, + DataModelType, +} from '../generated/entity/data/dashboardDataModel'; +import { DataType } from '../generated/entity/data/table'; + +export const DASHBOARD_DATA_MODEL_DUMMY_DATA: DashboardDataModel = { + id: '3519340b-7a36-45d1-abdf-348a5f1c582c', + name: 'orders', + displayName: 'Orders', + fullyQualifiedName: 'sample_looker.model.orders', + description: 'Orders explore from Sample Data', + version: 0.1, + updatedAt: 1697265260863, + updatedBy: 'ingestion-bot', + owners: [], + dataProducts: [], + tags: [], + deleted: false, + followers: [], + service: { + id: '2d102aaa-e683-425c-a8bf-e4afa43dde99', + type: 'dashboardService', + name: 'sample_looker', + fullyQualifiedName: 'sample_looker', + displayName: 'sample_looker', + deleted: false, + }, + serviceType: DashboardServiceType.Looker, + dataModelType: DataModelType.LookMlExplore, + // eslint-disable-next-line max-len + sql: "SELECT CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END AS clinical_stage,\n COUNT(*) AS count\nFROM covid_vaccines\nGROUP BY CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END\nORDER BY count DESC\nLIMIT 10000\nOFFSET 0;\n", + columns: [ + { + name: '0. Pre-clinical', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Pre-clinical'", + fullyQualifiedName: 'sample_looker.model.orders."0. Pre-clinical"', + tags: [], + ordinalPosition: 1, + }, + { + name: '2. Phase II or Combined I/II', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Phase II or Combined I/II'", + fullyQualifiedName: + 'sample_looker.model.orders."2. Phase II or Combined I/II"', + tags: [], + ordinalPosition: 2, + }, + { + name: '1. Phase I', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Phase I'", + fullyQualifiedName: 'sample_looker.model.orders."1. Phase I"', + tags: [], + ordinalPosition: 3, + }, + { + name: '3. Phase III', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Phase III'", + fullyQualifiedName: 'sample_looker.model.orders."3. Phase III"', + tags: [], + ordinalPosition: 4, + }, + { + name: '4. Authorized', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: "Vaccine Candidates in phase: 'Authorize'", + fullyQualifiedName: 'sample_looker.model.orders."4. Authorized"', + tags: [], + ordinalPosition: 5, + }, + ], + domain: { + id: '52fc9c67-78b7-42bf-8147-69278853c230', + type: 'domain', + name: 'Design', + fullyQualifiedName: 'Design', + description: "

Here' the description for Product Design

", + displayName: 'Product Design ', + inherited: true, + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; + +export const DASHBOARD_DUMMY_DATA: Dashboard = { + id: '574c383c-735f-44c8-abbb-355f87c8b19f', + name: 'customers', + displayName: 'Customers dashboard', + fullyQualifiedName: 'SampleLookerService.customers', + description: 'This is a sample Dashboard for Looker', + version: 0.1, + updatedAt: 1736493713236, + updatedBy: 'admin', + charts: [ + { + id: '81cdc1f3-66ae-462f-bf3e-b5fbbfe7792f', + type: 'chart', + name: 'chart_1', + fullyQualifiedName: 'SampleLookerService.chart_1', + description: 'This is a sample Chart for Looker', + displayName: 'Chart 1', + deleted: false, + }, + { + id: '6f5057aa-8d7c-41a7-ab93-76bf8ed2bc27', + type: 'chart', + name: 'chart_2', + fullyQualifiedName: 'SampleLookerService.chart_2', + description: 'This is a sample Chart for Looker', + displayName: 'Chart 2', + deleted: false, + }, + ], + owners: [], + followers: [], + tags: [], + service: { + id: 'fb4df3ed-75b9-45d3-a2df-da07785893d7', + type: 'dashboardService', + name: 'SampleLookerService', + fullyQualifiedName: 'SampleLookerService', + displayName: 'SampleLookerService', + deleted: false, + }, + serviceType: DashboardServiceType.Looker, + usageSummary: { + dailyStats: { + count: 0, + percentileRank: 0, + }, + weeklyStats: { + count: 0, + percentileRank: 0, + }, + monthlyStats: { + count: 0, + percentileRank: 0, + }, + date: new Date('2025-02-03'), + }, + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Database.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Database.constants.ts new file mode 100644 index 000000000000..a0cf44dcbf51 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Database.constants.ts @@ -0,0 +1,148 @@ +/* + * Copyright 2025 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 { + Database, + DatabaseServiceType, +} from '../generated/entity/data/database'; +import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; + +export const DATABASE_DUMMY_DATA: Database = { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + displayName: '', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + dataProducts: [], + tags: [], + version: 1.2, + updatedAt: 1736405710107, + updatedBy: 'prajwal.p', + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + }, + ], + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + }, + serviceType: DatabaseServiceType.BigQuery, + default: false, + deleted: false, + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; + +export const DATABASE_SCHEMA_DUMMY_DATA: DatabaseSchema = { + id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + dataProducts: [], + version: 1.1, + updatedAt: 1736405774154, + updatedBy: 'prajwal.p', + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + }, + ], + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + }, + serviceType: DatabaseServiceType.BigQuery, + database: { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + displayName: 'ecommerce_db', + deleted: false, + }, + usageSummary: { + dailyStats: { + count: 21, + percentileRank: 0, + }, + weeklyStats: { + count: 21, + percentileRank: 0, + }, + monthlyStats: { + count: 21, + percentileRank: 0, + }, + date: new Date('2023-11-10'), + }, + tags: [], + deleted: false, + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + }, + votes: { + upVotes: 0, + downVotes: 1, + upVoters: [], + downVoters: [ + { + id: 'f14f17bf-0923-4234-8e73-2dcc051f2adc', + type: 'user', + name: 'admin', + fullyQualifiedName: 'admin', + displayName: 'admin', + deleted: false, + }, + ], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Domain.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Domain.constants.ts index ec2fb7d017ba..c2c10c91fd81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Domain.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Domain.constants.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { Domain, DomainType } from '../generated/entity/domains/domain'; import i18n from '../utils/i18next/LocalUtil'; export const DOMAIN_TYPE_DATA = [ @@ -27,3 +28,47 @@ export const DOMAIN_TYPE_DATA = [ description: i18n.t('message.source-aligned-domain-type-description'), }, ]; + +export const DOMAIN_DUMMY_DATA: Domain = { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + domainType: DomainType.ConsumerAligned, + name: 'Engineering', + fullyQualifiedName: 'Engineering', + displayName: 'Engineering', + description: 'Domain related engineering development.', + style: {}, + version: 0.8, + updatedAt: 1698061758989, + updatedBy: 'rupesh', + children: [], + owners: [ + { + id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', + type: 'team', + name: 'Data', + fullyQualifiedName: 'Data', + description: '', + displayName: 'Data', + deleted: false, + }, + ], + experts: [ + { + id: '34ee72dc-7dad-4710-9f1d-e934ad0554a9', + type: 'user', + name: 'brian_smith7', + fullyQualifiedName: 'brian_smith7', + displayName: 'Brian Smith', + deleted: false, + }, + { + id: '9a6687fa-8bd5-446c-aa8f-81416c88fe67', + type: 'user', + name: 'brittney_thomas3', + fullyQualifiedName: 'brittney_thomas3', + displayName: 'Brittney Thomas', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/users/9a6687fa-8bd5-446c-aa8f-81416c88fe67', + }, + ], +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Metric.constnsts.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Metric.constnsts.ts new file mode 100644 index 000000000000..5311634bcecb --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Metric.constnsts.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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 { + Language, + Metric, + MetricGranularity, + MetricType, + UnitOfMeasurement, +} from '../generated/entity/data/metric'; + +export const METRIC_DUMMY_DATA: Metric = { + id: '2be869f2-a8c6-4781-89fa-df45d671e449', + name: 'LTV', + fullyQualifiedName: 'LTV', + displayName: 'Lifetime Value', + metricExpression: { + language: Language.Python, + code: 'def ltv(customer: Customer, orders: List[Order]):\n\tlifetime = customer.lifetime\n ltv: float = sum([o.amount for o in orders if o.customer_id == customer.id])\n \n return ltv', + }, + metricType: MetricType.Other, + unitOfMeasurement: UnitOfMeasurement.Dollars, + granularity: MetricGranularity.Quarter, + relatedMetrics: [], + version: 0.1, + updatedAt: 1727366423816, + updatedBy: 'teddy', + owners: [], + followers: [], + tags: [], + deleted: false, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/MlModel.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/MlModel.constants.ts new file mode 100644 index 000000000000..83fb5fcf6ac0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/MlModel.constants.ts @@ -0,0 +1,162 @@ +/* + * Copyright 2025 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 { + FeatureSourceDataType, + FeatureType, + Mlmodel, + MlModelServiceType, +} from '../generated/entity/data/mlmodel'; + +export const ML_MODEL_DUMMY_DATA: Mlmodel = { + id: '6ef964b1-edb7-4d7c-85f1-51845197206c', + name: 'eta_predictions', + fullyQualifiedName: 'mlflow_svc.eta_predictions', + displayName: 'ETA Predictions', + description: 'ETA Predictions Model', + algorithm: 'Neural Network', + mlFeatures: [ + { + name: 'sales', + dataType: FeatureType.Numerical, + description: 'Sales amount', + fullyQualifiedName: 'mlflow_svc.eta_predictions.sales', + featureSources: [ + { + name: 'gross_sales', + dataType: FeatureSourceDataType.Integer, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.fact_sale.gross_sales', + dataSource: { + id: '8cb6dbd3-1c4b-48c3-ab53-1e964f355d03', + type: 'table', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_sale', + description: + // eslint-disable-next-line max-len + 'The fact table captures the value of products sold or returned, as well as the values of other charges such as taxes and shipping costs. The sales table contains one row per order line item, one row per returned line item, and one row per shipping charge. Use this table when you need financial metrics.', + }, + tags: [], + }, + ], + }, + { + name: 'persona', + dataType: FeatureType.Categorical, + description: 'type of buyer', + fullyQualifiedName: 'mlflow_svc.eta_predictions.persona', + featureSources: [ + { + name: 'membership', + dataType: FeatureSourceDataType.String, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer.membership', + dataSource: { + id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', + type: 'table', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_customer', + description: + // eslint-disable-next-line max-len + 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', + }, + tags: [], + }, + { + name: 'platform', + dataType: FeatureSourceDataType.String, + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.raw_customer.platform', + dataSource: { + id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', + type: 'table', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_customer', + description: + // eslint-disable-next-line max-len + 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', + }, + tags: [], + }, + ], + featureAlgorithm: 'PCA', + }, + ], + mlHyperParameters: [ + { + name: 'regularisation', + value: '0.5', + }, + { + name: 'random', + value: 'hello', + }, + ], + target: 'ETA_time', + dashboard: { + id: '1faa74ba-9952-4591-aef2-9acfaf9c14d1', + type: 'dashboard', + name: 'eta_predictions_performance', + fullyQualifiedName: 'sample_superset.eta_predictions_performance', + description: '', + displayName: 'ETA Predictions Performance', + deleted: false, + }, + mlStore: { + storage: 's3://path-to-pickle', + imageRepository: 'https://docker.hub.com/image', + }, + server: 'http://my-server.ai/', + owners: [], + followers: [], + tags: [], + usageSummary: { + dailyStats: { + count: 0, + percentileRank: 0, + }, + weeklyStats: { + count: 0, + percentileRank: 0, + }, + monthlyStats: { + count: 0, + percentileRank: 0, + }, + date: new Date('2025-02-22'), + }, + version: 1.2, + updatedAt: 1722587630921, + updatedBy: 'harsh.s', + service: { + id: 'b8cbebe5-58f0-493e-81f9-ad76e7695164', + type: 'mlmodelService', + name: 'mlflow_svc', + fullyQualifiedName: 'mlflow_svc', + displayName: 'mlflow_svc', + deleted: false, + }, + serviceType: MlModelServiceType.Mlflow, + deleted: false, + domain: { + id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', + type: 'domain', + name: 'domain.with.dot', + fullyQualifiedName: '"domain.with.dot"', + description: 'domain.with.dot', + displayName: 'domain.with.dot', + }, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/SearchIndex.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/SearchIndex.constants.ts new file mode 100644 index 000000000000..89b8626dd294 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/SearchIndex.constants.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2025 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 { + DataType, + IndexType, + SearchIndex, + SearchServiceType, +} from '../generated/entity/data/searchIndex'; + +export const SEARCH_INDEX_DUMMY_DATA: SearchIndex = { + id: '2de6c0f1-c85a-4c90-b74b-b40869cc446c', + name: 'table_search_index', + fullyQualifiedName: 'elasticsearch_sample.table_search_index', + displayName: 'TableSearchIndex', + description: 'Table Search Index', + version: 0.2, + updatedAt: 1726204919320, + updatedBy: 'ashish', + service: { + id: 'dde0ee41-f45f-4cd4-8826-07cd298905f2', + type: 'searchService', + name: 'elasticsearch_sample', + fullyQualifiedName: 'elasticsearch_sample', + displayName: 'elasticsearch_sample', + deleted: false, + }, + serviceType: SearchServiceType.ElasticSearch, + fields: [ + { + name: 'name', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity Name.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.name', + tags: [], + }, + { + name: 'displayName', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity DisplayName.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.displayName', + tags: [], + }, + { + name: 'description', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Table Entity Description.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.description', + tags: [], + }, + { + name: 'columns', + dataType: DataType.Nested, + dataTypeDisplay: 'nested', + description: 'Table Columns.', + fullyQualifiedName: 'elasticsearch_sample.table_search_index.columns', + tags: [], + children: [ + { + name: 'name', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column Name.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.name', + tags: [], + }, + { + name: 'displayName', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column DisplayName.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.displayName', + tags: [], + }, + { + name: 'description', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Column Description.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.columns.description', + tags: [], + }, + ], + }, + { + name: 'databaseSchema', + dataType: DataType.Text, + dataTypeDisplay: 'text', + description: 'Database Schema that this table belongs to.', + fullyQualifiedName: + 'elasticsearch_sample.table_search_index.databaseSchema', + tags: [], + }, + ], + indexType: IndexType.Index, + owners: [], + followers: [ + { + id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + type: 'user', + name: 'ashish', + fullyQualifiedName: 'ashish', + description: + '

data driven car

Hybrid Modal

', + displayName: 'Ashish', + deleted: false, + }, + ], + tags: [], + deleted: false, + domain: { + id: 'a440b3a9-fbbf-464d-91d4-c9cfeeccc8e4', + type: 'domain', + name: 'Domain 1.6.0', + fullyQualifiedName: '"Domain 1.6.0"', + description: 'csacasc', + displayName: 'Domain 1.6.0', + }, + dataProducts: [ + { + id: '8c046cc5-ab23-40c0-a0bf-b58d206290f7', + type: 'dataProduct', + name: 'TestDataProduct', + fullyQualifiedName: 'TestDataProduct', + description: 'asd', + displayName: 'TestDataProduct', + }, + ], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Table.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Table.constants.ts index 4708e7d94d8a..b4c8bb0f8e16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Table.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Table.constants.ts @@ -11,10 +11,15 @@ * limitations under the License. */ +import { StoredProcedure } from '../generated/entity/data/storedProcedure'; import { Constraint, ConstraintType, + DatabaseServiceType, + DataType, RelationshipType, + Table, + TableType, } from '../generated/entity/data/table'; import i18n from '../utils/i18next/LocalUtil'; @@ -88,3 +93,303 @@ export const RELATIONSHIP_TYPE_OPTION = [ value: RelationshipType.ManyToMany, }, ]; + +export const TABLE_DUMMY_DATA: Table = { + id: 'ab4f893b-c303-43d9-9375-3e620a670b02', + name: 'raw_product_catalog', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_product_catalog', + description: + 'This is a raw product catalog table contains the product listing, price, seller etc.. represented in our online DB. ', + version: 0.2, + updatedAt: 1688442727895, + updatedBy: 'admin', + tableType: TableType.Regular, + dataProducts: [ + { + id: 'c9b891b1-5d60-4171-9af0-7fd6d74f8f2b', + type: 'dataProduct', + name: 'Design Data product ', + fullyQualifiedName: 'Design Data product ', + description: "Here's the description for the Design Data product Name.", + displayName: 'Design Data product Name', + href: '#', + }, + ], + joins: { + startDate: new Date(), + dayCount: 30, + columnJoins: [ + { + columnName: 'address_id', + joinedWith: [ + { + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', + joinCount: 0, + }, + { + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address.address_id', + joinCount: 0, + }, + ], + }, + ], + directTableJoins: [ + { + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_address', + joinCount: 0, + }, + ], + }, + columns: [ + { + name: 'address_id', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: 'Unique identifier for the address.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', + tags: [], + ordinalPosition: 1, + }, + { + name: 'shop_id', + dataType: DataType.Numeric, + dataTypeDisplay: 'numeric', + description: + 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim_shop table.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.shop_id', + tags: [], + ordinalPosition: 2, + }, + { + name: 'first_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'First name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.first_name', + tags: [], + ordinalPosition: 3, + }, + { + name: 'last_name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Last name of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.last_name', + tags: [], + ordinalPosition: 4, + }, + { + name: 'address', + dataType: DataType.Varchar, + dataLength: 500, + dataTypeDisplay: 'varchar', + description: 'Clean address test', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.address', + tags: [], + ordinalPosition: 5, + }, + { + name: 'company', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: "The name of the customer's business, if one exists.", + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.company', + tags: [], + ordinalPosition: 7, + }, + { + name: 'city', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'The name of the city. For example, Palo Alto.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.city', + tags: [], + ordinalPosition: 8, + }, + { + name: 'region', + dataType: DataType.Varchar, + dataLength: 512, + dataTypeDisplay: 'varchar', + description: + // eslint-disable-next-line max-len + 'The name of the region, such as a province or state, where the customer is located. For example, Ontario or New York. This column is the same as CustomerAddress.province in the Admin API.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.region', + tags: [], + ordinalPosition: 9, + }, + { + name: 'zip', + dataType: DataType.Varchar, + dataLength: 10, + dataTypeDisplay: 'varchar', + description: 'The ZIP or postal code. For example, 90210.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.zip', + tags: [], + ordinalPosition: 10, + }, + { + name: 'country', + dataType: DataType.Varchar, + dataLength: 50, + dataTypeDisplay: 'varchar', + description: 'The full name of the country. For example, Canada.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.country', + tags: [], + ordinalPosition: 11, + }, + { + name: 'phone', + dataType: DataType.Varchar, + dataLength: 15, + dataTypeDisplay: 'varchar', + description: 'The phone number of the customer.', + fullyQualifiedName: + 'sample_data.ecommerce_db.shopify.dim_address_clean.phone', + tags: [], + ordinalPosition: 12, + }, + ], + owners: [ + { + id: '38be030f-f817-4712-bc3b-ff7b9b9b805e', + type: 'user', + name: 'aaron_johnson0', + fullyQualifiedName: 'aaron_johnson0', + displayName: 'Aaron Johnson', + deleted: false, + }, + ], + databaseSchema: { + id: '3f0d9c39-0926-4028-8070-65b0c03556cb', + type: 'databaseSchema', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + deleted: false, + }, + database: { + id: 'f085e133-e184-47c8-ada5-d7e005d3153b', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + deleted: false, + }, + service: { + id: 'e61069a9-29e3-49fa-a7f4-f5227ae50b72', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + deleted: false, + }, + tableConstraints: [ + { + constraintType: ConstraintType.ForeignKey, + columns: ['post_id'], + referredColumns: ['mysql_sample.default.posts_db.Posts.post_id'], + relationshipType: RelationshipType.ManyToOne, + }, + { + constraintType: ConstraintType.ForeignKey, + columns: ['user_id'], + referredColumns: ['mysql_sample.default.posts_db.Users.user_id'], + relationshipType: RelationshipType.ManyToOne, + }, + ], + serviceType: DatabaseServiceType.BigQuery, + tags: [], + followers: [], + deleted: false, +}; + +export const STORED_PROCEDURE_DUMMY_DATA: StoredProcedure = { + id: '5f3509ca-c8e5-449a-a7e8-b1c3b96e39e8', + name: 'calculate_average', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.calculate_average', + description: 'Procedure to calculate average', + storedProcedureCode: { + // eslint-disable-next-line max-len + code: 'CREATE OR REPLACE PROCEDURE calculate_average(numbers INT ARRAY) RETURNS FLOAT NOT NULL LANGUAGE SQL AS $$DECLARE sum_val INT = 0;count_val INT = 0;average_val FLOAT;BEGIN\n FOR num IN ARRAY numbers DO sum_val := sum_val + num;\n count_val := count_val + 1;\nEND FOR;\nIF count_val = 0 THEN\n average_val := 0.0;\nELSE\n average_val := sum_val / count_val;\nEND IF;\nRETURN average_val;\nEND;$$;', + }, + version: 0.5, + dataProducts: [], + updatedAt: 1709544812674, + updatedBy: 'admin', + databaseSchema: { + id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', + type: 'databaseSchema', + name: 'shopify', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify', + description: + 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', + displayName: 'shopify', + deleted: false, + }, + database: { + id: '77147d45-888b-42dd-a369-8b7ba882dffb', + type: 'database', + name: 'ecommerce_db', + fullyQualifiedName: 'sample_data.ecommerce_db', + description: + 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', + displayName: 'ecommerce_db', + deleted: false, + }, + service: { + id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', + type: 'databaseService', + name: 'sample_data', + fullyQualifiedName: 'sample_data', + displayName: 'sample_data', + deleted: false, + }, + serviceType: DatabaseServiceType.BigQuery, + deleted: false, + owners: [ + { + id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', + type: 'user', + name: 'aaron.singh2', + fullyQualifiedName: '"aaron.singh2"', + displayName: 'Aaron Singh', + deleted: false, + inherited: true, + }, + ], + followers: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, + tags: [], + domain: { + id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', + type: 'domain', + name: 'Engineering', + fullyQualifiedName: 'Engineering', + description: 'Domain related engineering development.', + displayName: 'Engineering', + inherited: true, + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Topic.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Topic.constant.ts new file mode 100644 index 000000000000..3e752e582a58 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Topic.constant.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2025 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 { + CleanupPolicy, + DataTypeTopic, + MessagingServiceType, + SchemaType, + Topic, +} from '../generated/entity/data/topic'; + +export const TOPIC_DUMMY_DATA: Topic = { + id: 'd68d2e86-41cd-4c6c-bd78-41db489d46d1', + name: 'address_book', + fullyQualifiedName: 'sample_kafka.address_book', + description: 'All Protobuf record related events gets captured in this topic', + version: 0.7, + updatedAt: 1701432879105, + updatedBy: 'aniket', + service: { + id: '46f09e52-34de-4580-8133-a7e54e23c22b', + type: 'messagingService', + name: 'sample_kafka', + fullyQualifiedName: 'sample_kafka', + displayName: 'sample_kafka', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/services/messagingServices/46f09e52-34de-4580-8133-a7e54e23c22b', + }, + serviceType: MessagingServiceType.Kafka, + messageSchema: { + schemaText: + // eslint-disable-next-line max-len + 'syntax = "proto2";\n\npackage tutorial;\n\nmessage Person {\n optional string name = 1;\n optional int32 id = 2;\n optional string email = 3;\n\n enum PhoneType {\n MOBILE = 0;\n HOME = 1;\n WORK = 2;\n }\n\n message PhoneNumber {\n optional string number = 1;\n optional PhoneType type = 2 [default = HOME];\n }\n\n repeated PhoneNumber phones = 4;\n}\n\nmessage AddressBook {\n repeated Person people = 1;\n}', + schemaType: SchemaType.Protobuf, + schemaFields: [ + { + name: 'AddressBook', + dataType: DataTypeTopic.Record, + fullyQualifiedName: 'sample_kafka.address_book.AddressBook', + tags: [], + children: [ + { + name: 'people', + dataType: DataTypeTopic.Record, + fullyQualifiedName: 'sample_kafka.address_book.AddressBook.people', + tags: [], + children: [ + { + name: 'name', + dataType: DataTypeTopic.String, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.name', + tags: [], + }, + { + name: 'id', + dataType: DataTypeTopic.Int, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.id', + tags: [], + }, + { + name: 'email', + dataType: DataTypeTopic.String, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.email', + tags: [], + }, + { + name: 'phones', + dataType: DataTypeTopic.Record, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones', + tags: [], + children: [ + { + name: 'number', + dataType: DataTypeTopic.String, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones.number', + tags: [], + }, + { + name: 'type', + dataType: DataTypeTopic.Enum, + fullyQualifiedName: + 'sample_kafka.address_book.AddressBook.people.phones.type', + tags: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + partitions: 128, + cleanupPolicies: [CleanupPolicy.Compact, CleanupPolicy.Delete], + replicationFactor: 4, + maximumMessageSize: 249, + retentionSize: 1931232624, + owners: [ + { + id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', + type: 'team', + name: 'Data', + fullyQualifiedName: 'Data', + description: '', + displayName: 'Data', + deleted: false, + }, + ], + followers: [ + { + id: '96546482-1b99-4293-9e0f-7194fe25bcbf', + type: 'user', + name: 'sonal.w', + fullyQualifiedName: '"sonal.w"', + displayName: 'admin', + deleted: false, + }, + ], + tags: [], + deleted: false, + domain: { + id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', + type: 'domain', + name: 'domain.with.dot', + fullyQualifiedName: '"domain.with.dot"', + description: 'domain.with.dot', + displayName: 'domain.with.dot', + }, + dataProducts: [], + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/pipeline.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/pipeline.constants.ts index 61eb64a94d1a..12b93654d460 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/pipeline.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/pipeline.constants.ts @@ -11,6 +11,12 @@ * limitations under the License. */ +import { + Pipeline, + PipelineServiceType, + StatusType, +} from '../generated/entity/data/pipeline'; + export enum PIPELINE_TASK_TABS { LIST_VIEW = 'List', DAG_VIEW = 'Dag', @@ -29,3 +35,90 @@ export const PIPELINE_INGESTION_RUN_STATUS = { partialSuccess: '#439897', paused: '#FFBE0E', }; + +export const PIPELINE_DUMMY_DATA: Pipeline = { + id: '60670fe2-eb0a-41a2-a683-d32964e7e61b', + name: 'dim_address_etl', + displayName: 'dim_address etl', + fullyQualifiedName: 'sample_airflow.dim_address_etl', + description: 'dim_address ETL pipeline', + dataProducts: [], + version: 0.2, + updatedAt: 1726204840178, + updatedBy: 'ashish', + tasks: [ + { + name: 'dim_address_task', + displayName: 'dim_address Task', + fullyQualifiedName: 'sample_airflow.dim_address_etl.dim_address_task', + description: + 'Airflow operator to perform ETL and generate dim_address table', + downstreamTasks: ['assert_table_exists'], + taskType: 'PrestoOperator', + tags: [], + }, + { + name: 'assert_table_exists', + displayName: 'Assert Table Exists', + fullyQualifiedName: 'sample_airflow.dim_address_etl.assert_table_exists', + description: 'Assert if a table exists', + downstreamTasks: [], + taskType: 'HiveOperator', + tags: [], + }, + ], + pipelineStatus: { + timestamp: 1723014798482, + executionStatus: StatusType.Pending, + taskStatus: [ + { + name: 'dim_address_task', + executionStatus: StatusType.Pending, + }, + { + name: 'assert_table_exists', + executionStatus: StatusType.Pending, + }, + ], + }, + followers: [ + { + id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', + type: 'user', + name: 'ashish', + fullyQualifiedName: 'ashish', + description: + '

data driven car

Hybrid Modal

', + displayName: 'Ashish', + deleted: false, + }, + ], + tags: [], + owners: [], + service: { + id: 'f83c2b6e-0d09-4839-98df-5571cc17c829', + type: 'pipelineService', + name: 'sample_airflow', + fullyQualifiedName: 'sample_airflow', + displayName: 'sample_airflow', + deleted: false, + }, + serviceType: PipelineServiceType.Airflow, + deleted: false, + scheduleInterval: '5 * * * *', + domain: { + id: '6b440596-144b-417b-b7ee-95cf0b0d7de4', + type: 'domain', + name: 'Version -1.6.2 Domain', + fullyQualifiedName: '"Version -1.6.2 Domain"', + description: '

Version -1.6.2 Domain

', + displayName: 'Version -1.6.2 Domain', + inherited: true, + }, + votes: { + upVotes: 0, + downVotes: 0, + upVoters: [], + downVoters: [], + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts index ec2c0432ee90..ded49699bf19 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/CustomizeDetailPage.enum.ts @@ -46,6 +46,7 @@ export enum DetailPageWidgetKeys { API_ENDPOINTS = 'KnowledgePanel.APIEndpoints', API_SCHEMA = 'KnowledgePanel.APISchema', RELATED_METRICS = 'KnowledgePanel.RelatedMetrics', + ML_MODEL_FEATURES = 'KnowledgePanel.MlModelFeatures', } export enum GlossaryTermDetailPageWidgetKeys { diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts new file mode 100644 index 000000000000..6cc30d06a081 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2025 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 { useEffect } from 'react'; + +import { useCallback, useState } from 'react'; +import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; +import { EntityType } from '../enums/entity.enum'; +import { Page, PageType } from '../generated/system/ui/page'; +import { getDocumentByFQN } from '../rest/DocStoreAPI'; +import { useApplicationStore } from './useApplicationStore'; + +export const useCustomPages = (pageType: PageType) => { + const { selectedPersona } = useApplicationStore(); + const [customizedPage, setCustomizedPage] = useState(null); + + const fetchDocument = useCallback(async () => { + const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; + try { + const doc = await getDocumentByFQN(pageFQN); + setCustomizedPage( + doc.data?.pages?.find((p: Page) => p.pageType === pageType) + ); + } catch (error) { + // fail silent + } + }, [selectedPersona.fullyQualifiedName, pageType]); + + useEffect(() => { + if (selectedPersona?.fullyQualifiedName) { + fetchDocument(); + } + }, [selectedPersona, pageType]); + + return { + customizedPage, + }; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx index 20d249b76fcd..331ada6b77f4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/APICollectionPage/APICollectionPage.tsx @@ -32,7 +32,6 @@ import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/D import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -53,9 +52,9 @@ import { } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { APICollection } from '../../generated/entity/data/apiCollection'; -import { Page, PageType } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { useTableFilters } from '../../hooks/useTableFilters'; import { FeedCounts } from '../../interface/feed.interface'; @@ -66,7 +65,6 @@ import { updateApiCollectionVote, } from '../../rest/apiCollectionsAPI'; import { getApiEndPoints } from '../../rest/apiEndpointsAPI'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import apiCollectionClassBase from '../../utils/APICollection/APICollectionClassBase'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; import { @@ -82,8 +80,7 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const APICollectionPage: FunctionComponent = () => { const { t } = useTranslation(); const { getEntityPermissionByFqn } = usePermissionProvider(); - const [customizedPage, setCustomizedPage] = useState(null); - const { selectedPersona } = useApplicationStore(); + const { customizedPage } = useCustomPages(PageType.APICollection); const { tab: activeTab = EntityTabs.API_ENDPOINT } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedAPICollectionFQN } = useFqn(); @@ -436,26 +433,6 @@ const APICollectionPage: FunctionComponent = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find( - (p: Page) => p.pageType === PageType.APICollection - ) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - if (isPermissionsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 684664d1a3e9..f661e6b9b215 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -25,7 +25,6 @@ import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/D import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -46,14 +45,13 @@ import { } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { Container } from '../../generated/entity/data/container'; -import { Page } from '../../generated/system/ui/page'; -import { PageType } from '../../generated/system/ui/uiCustomization'; +import { PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { addContainerFollower, getContainerByName, @@ -80,16 +78,15 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const ContainerPage = () => { const history = useHistory(); const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const { getEntityPermissionByFqn } = usePermissionProvider(); const { tab } = useParams<{ tab: EntityTabs }>(); - + const { customizedPage } = useCustomPages(PageType.Container); const { fqn: decodedContainerName } = useFqn(); // Local states const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); - const [customizedPage, setCustomizedPage] = useState(null); const [containerData, setContainerData] = useState(); const [containerPermissions, setContainerPermissions] = useState(DEFAULT_ENTITY_PERMISSION); @@ -490,24 +487,6 @@ const ContainerPage = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Container) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - // Effects useEffect(() => { fetchResourcePermission(decodedContainerName); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx index 41743f24e0ad..be86b0cfe536 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseDetailsPage/DatabaseDetailsPage.tsx @@ -34,7 +34,6 @@ import ProfilerSettings from '../../components/Database/Profiler/ProfilerSetting import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getExplorePath, @@ -56,11 +55,10 @@ import { } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { Database } from '../../generated/entity/data/database'; -import { Page } from '../../generated/system/ui/page'; import { PageType } from '../../generated/system/ui/uiCustomization'; import { Include } from '../../generated/type/include'; import { useLocationSearch } from '../../hooks/LocationSearch/useLocationSearch'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; import { @@ -70,7 +68,6 @@ import { restoreDatabase, updateDatabaseVotes, } from '../../rest/databaseAPI'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; import { getDetailsTabWithNewLabel, @@ -91,13 +88,12 @@ const DatabaseDetails: FunctionComponent = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const { withinPageSearch } = useLocationSearch<{ withinPageSearch: string }>(); - const { selectedPersona } = useApplicationStore(); const { tab: activeTab = EntityTabs.SCHEMA } = useParams<{ tab: EntityTabs }>(); const { fqn: decodedDatabaseFQN } = useFqn(); const [isLoading, setIsLoading] = useState(true); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Database); const [database, setDatabase] = useState({} as Database); const [serviceType, setServiceType] = useState(); @@ -438,24 +434,6 @@ const DatabaseDetails: FunctionComponent = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Database) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - if (isLoading || isDatabaseDetailsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index c1def2b913c1..9ce637419693 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -33,7 +33,6 @@ import ProfilerSettings from '../../components/Database/Profiler/ProfilerSetting import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -56,9 +55,9 @@ import { } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { DatabaseSchema } from '../../generated/entity/data/databaseSchema'; -import { Page, PageType } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { useTableFilters } from '../../hooks/useTableFilters'; import { FeedCounts } from '../../interface/feed.interface'; @@ -68,7 +67,6 @@ import { restoreDatabaseSchema, updateDatabaseSchemaVotes, } from '../../rest/databaseAPI'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getStoredProceduresList } from '../../rest/storedProceduresAPI'; import { getTableList } from '../../rest/tableAPI'; import { getEntityMissingError, getFeedCounts } from '../../utils/CommonUtils'; @@ -102,8 +100,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [customizedPage, setCustomizedPage] = useState(null); - const { selectedPersona } = useApplicationStore(); + const { customizedPage } = useCustomPages(PageType.DatabaseSchema); const [databaseSchemaPermission, setDatabaseSchemaPermission] = useState(DEFAULT_ENTITY_PERMISSION); const [storedProcedureCount, setStoredProcedureCount] = useState(0); @@ -488,26 +485,6 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find( - (p: Page) => p.pageType === PageType.DatabaseSchema - ) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - if (isPermissionsLoading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index b4c0bcdceb2a..800ce884e52a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -26,7 +26,6 @@ import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/D import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -41,12 +40,12 @@ import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; import { Tag } from '../../generated/entity/classification/tag'; import { SearchIndex, TagLabel } from '../../generated/entity/data/searchIndex'; -import { Page, PageType } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/page'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { addFollower, getSearchIndexDetailsByFQN, @@ -75,14 +74,14 @@ function SearchIndexDetailsPage() { const { fqn: decodedSearchIndexFQN } = useFqn(); const { t } = useTranslation(); const history = useHistory(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const USERId = currentUser?.id ?? ''; const [loading, setLoading] = useState(true); const [searchIndexDetails, setSearchIndexDetails] = useState(); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.SearchIndex); const [searchIndexPermissions, setSearchIndexPermissions] = useState(DEFAULT_ENTITY_PERMISSION); @@ -503,24 +502,6 @@ function SearchIndexDetailsPage() { } }, [decodedSearchIndexFQN]); - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.SearchIndex) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - useEffect(() => { if (viewPermission) { fetchSearchIndexDetails(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx index e5575de2391c..4d4816802c06 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceDetailsPage/ServiceMainTabContent.tsx @@ -26,6 +26,7 @@ import Loader from '../../components/common/Loader/Loader'; import NextPrevious from '../../components/common/NextPrevious/NextPrevious'; import { NextPreviousProps } from '../../components/common/NextPrevious/NextPrevious.interface'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; +import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import EntityRightPanel from '../../components/Entity/EntityRightPanel/EntityRightPanel'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import { COMMON_RESIZABLE_PANEL_CONFIG } from '../../constants/ResizablePanel.constants'; @@ -292,19 +293,25 @@ function ServiceMainTabContent({ }} secondPanel={{ children: ( -
- -
+ +
+ +
+
), ...COMMON_RESIZABLE_PANEL_CONFIG.RIGHT_PANEL, className: diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index 88934ace17bd..c5601b67ef30 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -24,7 +24,6 @@ import { DataAssetsHeader } from '../../components/DataAssets/DataAssetsHeader/D import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface'; import { EntityName } from '../../components/Modals/EntityNameModal/EntityNameModal.interface'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { getEntityDetailsPath, getVersionPath, @@ -44,13 +43,13 @@ import { StoredProcedure, StoredProcedureCodeObject, } from '../../generated/entity/data/storedProcedure'; -import { Page, PageType } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/page'; import { Include } from '../../generated/type/include'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { FeedCounts } from '../../interface/feed.interface'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { addStoredProceduresFollower, getStoredProceduresByFqn, @@ -76,7 +75,7 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const StoredProcedurePage = () => { const { t } = useTranslation(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const USER_ID = currentUser?.id ?? ''; const history = useHistory(); const { tab: activeTab = EntityTabs.CODE } = useParams<{ tab: string }>(); @@ -87,7 +86,7 @@ const StoredProcedurePage = () => { const [storedProcedure, setStoredProcedure] = useState(); const [storedProcedurePermissions, setStoredProcedurePermissions] = useState(DEFAULT_ENTITY_PERMISSION); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.StoredProcedure); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); @@ -502,26 +501,6 @@ const StoredProcedurePage = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find( - (p: Page) => p.pageType === PageType.StoredProcedure - ) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - useEffect(() => { if (decodedStoredProcedureFQN) { fetchResourcePermission(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 3e98d54e3f85..1691a62b28e4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -55,15 +55,15 @@ import { import { Tag } from '../../generated/entity/classification/tag'; import { Table, TableType } from '../../generated/entity/data/table'; import { Suggestion } from '../../generated/entity/feed/suggestion'; -import { Page, PageType } from '../../generated/system/ui/page'; +import { PageType } from '../../generated/system/ui/page'; import { TestSummary } from '../../generated/tests/testCase'; import { TagLabel } from '../../generated/type/tagLabel'; import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; import { useSub } from '../../hooks/usePubSub'; import { FeedCounts } from '../../interface/feed.interface'; -import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getDataQualityLineage } from '../../rest/lineageAPI'; import { getQueriesList } from '../../rest/queryAPI'; import { @@ -103,7 +103,7 @@ import './table-details-page-v1.less'; const TableDetailsPageV1: React.FC = () => { const { isTourOpen, activeTabForTourDatasetPage, isTourPage } = useTourProvider(); - const { currentUser, selectedPersona } = useApplicationStore(); + const { currentUser } = useApplicationStore(); const { setDqLineageData } = useTestCaseStore(); const [tableDetails, setTableDetails] = useState
(); const { tab: activeTab } = useParams<{ tab: EntityTabs }>(); @@ -123,7 +123,7 @@ const TableDetailsPageV1: React.FC = () => { ); const [testCaseSummary, setTestCaseSummary] = useState(); const [dqFailureCount, setDqFailureCount] = useState(0); - const [customizedPage, setCustomizedPage] = useState(null); + const { customizedPage } = useCustomPages(PageType.Table); const tableFqn = useMemo( () => @@ -747,24 +747,6 @@ const TableDetailsPageV1: React.FC = () => { } }; - const fetchDocument = useCallback(async () => { - const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona.fullyQualifiedName}`; - try { - const doc = await getDocumentByFQN(pageFQN); - setCustomizedPage( - doc.data?.pages?.find((p: Page) => p.pageType === PageType.Table) - ); - } catch (error) { - // fail silent - } - }, [selectedPersona.fullyQualifiedName]); - - useEffect(() => { - if (selectedPersona?.fullyQualifiedName) { - fetchDocument(); - } - }, [selectedPersona]); - if (loading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts index f9b5ee79aadd..b14f9bff1e5f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APICollection/APICollectionClassBase.ts @@ -13,6 +13,7 @@ import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { API_COLLECTION_DUMMY_DATA } from '../../constants/APICollection.constnats'; import { CUSTOM_PROPERTIES_WIDGET, DATA_PRODUCTS_WIDGET, @@ -23,10 +24,7 @@ import { } from '../../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; -import { - APICollection, - APIServiceType, -} from '../../generated/entity/data/apiCollection'; +import { APICollection } from '../../generated/entity/data/apiCollection'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { FeedCounts } from '../../interface/feed.interface'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; @@ -136,36 +134,7 @@ class APICollectionClassBase { ]; } public getDummyData(): APICollection { - return { - id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', - name: 'pet', - fullyQualifiedName: 'sample_api_service.pet', - version: 0.1, - updatedAt: 1722588116104, - updatedBy: 'ingestion-bot', - endpointURL: 'https://petstore3.swagger.io/#/pet', - href: 'http://sandbox-beta.open-metadata.org/api/v1/apiCollections/db03ef8f-82f9-4a23-a940-3ba5af5bba29', - owners: [], - tags: [], - service: { - id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', - type: 'apiService', - name: 'sample_api_service', - fullyQualifiedName: 'sample_api_service', - displayName: 'sample_api_service', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/apiServices/449b7937-c4ca-4dce-866c-5f6d0acc45c1', - }, - serviceType: APIServiceType.REST, - deleted: false, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return API_COLLECTION_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts index d678bbf92a29..96302d924d1e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIEndpoints/APIEndpointClassBase.ts @@ -13,6 +13,7 @@ import { Layout } from 'react-grid-layout'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import { API_ENDPOINT_DUMMY_DATA } from '../../constants/APICollection.constnats'; import { CUSTOM_PROPERTIES_WIDGET, DATA_PRODUCTS_WIDGET, @@ -23,13 +24,7 @@ import { } from '../../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; -import { - APIEndpoint, - APIRequestMethod, - APIServiceType, - DataTypeTopic, - SchemaType, -} from '../../generated/entity/data/apiEndpoint'; +import { APIEndpoint } from '../../generated/entity/data/apiEndpoint'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { FeedCounts } from '../../interface/feed.interface'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; @@ -140,178 +135,7 @@ class APIEndpointClassBase { ]; } public getDummyData(): APIEndpoint { - return { - id: 'b41d3506-09ac-4e02-ae40-8d6933f6a77f', - name: 'addPet', - displayName: 'Add Pet', - fullyQualifiedName: 'sample_api_service.pet.addPet', - description: 'add a new pet', - version: 0.5, - updatedAt: 1723268606694, - updatedBy: 'sachin', - endpointURL: 'https://petstore3.swagger.io/#/pet/addPet', - requestMethod: APIRequestMethod.Post, - requestSchema: { - schemaType: SchemaType.JSON, - schemaFields: [ - { - name: 'id', - dataType: DataTypeTopic.Int, - description: 'ID of pet that needs to be updated', - fullyQualifiedName: 'sample_api_service.pet.addPet.id', - tags: [], - }, - { - name: 'name', - dataType: DataTypeTopic.String, - description: 'Name of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.name', - tags: [], - }, - { - name: 'category', - dataType: DataTypeTopic.Record, - description: 'Category of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.category', - tags: [], - children: [ - { - name: 'id', - dataType: DataTypeTopic.Int, - description: 'ID of category', - fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', - tags: [], - }, - { - name: 'name', - dataType: DataTypeTopic.String, - description: 'Name of category', - fullyQualifiedName: - 'sample_api_service.pet.addPet.category.name', - tags: [], - }, - ], - }, - { - name: 'photoUrls', - dataType: DataTypeTopic.Array, - description: "URLs of pet's photos", - fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', - tags: [], - }, - { - name: 'tags', - dataType: DataTypeTopic.Array, - description: 'Tags of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.tags', - tags: [], - }, - { - name: 'status', - dataType: DataTypeTopic.String, - description: 'Status of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.status', - tags: [], - }, - ], - }, - responseSchema: { - schemaType: SchemaType.JSON, - schemaFields: [ - { - name: 'id', - dataType: DataTypeTopic.Int, - description: 'ID of pet that needs to be updated', - fullyQualifiedName: 'sample_api_service.pet.addPet.id', - tags: [], - }, - { - name: 'name', - dataType: DataTypeTopic.String, - description: 'Name of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.name', - tags: [], - }, - { - name: 'category', - dataType: DataTypeTopic.Record, - description: 'Category of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.category', - tags: [], - children: [ - { - name: 'id', - dataType: DataTypeTopic.Int, - description: 'ID of category', - fullyQualifiedName: 'sample_api_service.pet.addPet.category.id', - tags: [], - }, - { - name: 'name', - dataType: DataTypeTopic.String, - description: 'Name of category', - fullyQualifiedName: - 'sample_api_service.pet.addPet.category.name', - tags: [], - }, - ], - }, - { - name: 'photoUrls', - dataType: DataTypeTopic.Array, - description: "URLs of pet's photos", - fullyQualifiedName: 'sample_api_service.pet.addPet.photoUrls', - tags: [], - }, - { - name: 'tags', - dataType: DataTypeTopic.Array, - description: 'Tags of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.tags', - tags: [], - }, - { - name: 'status', - dataType: DataTypeTopic.String, - description: 'Status of pet', - fullyQualifiedName: 'sample_api_service.pet.addPet.status', - tags: [], - }, - ], - }, - apiCollection: { - id: 'db03ef8f-82f9-4a23-a940-3ba5af5bba29', - type: 'apiCollection', - name: 'pet', - fullyQualifiedName: 'sample_api_service.pet', - displayName: 'pet', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/apiCollections/db03ef8f-82f9-4a23-a940-3ba5af5bba29', - }, - href: 'http://sandbox-beta.open-metadata.org/api/v1/apiEndpoints/b41d3506-09ac-4e02-ae40-8d6933f6a77f', - owners: [], - followers: [], - tags: [], - service: { - id: '449b7937-c4ca-4dce-866c-5f6d0acc45c1', - type: 'apiService', - name: 'sample_api_service', - fullyQualifiedName: 'sample_api_service', - displayName: 'sample_api_service', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/apiServices/449b7937-c4ca-4dce-866c-5f6d0acc45c1', - }, - serviceType: APIServiceType.REST, - - deleted: false, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return API_ENDPOINT_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts index e1d9ab5ceaf3..4cd5b0d6d611 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContainerDetailsClassBase.ts @@ -13,6 +13,7 @@ import { Layout } from 'react-grid-layout'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; +import { CONTAINER_DUMMY_DATA } from '../constants/Contianer.constants'; import { CUSTOM_PROPERTIES_WIDGET, DATA_PRODUCTS_WIDGET, @@ -23,11 +24,7 @@ import { } from '../constants/CustomizeWidgets.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - Container, - FileFormat, - StorageServiceType, -} from '../generated/entity/data/container'; +import { Container } from '../generated/entity/data/container'; import { Tab } from '../generated/system/ui/uiCustomization'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; @@ -134,64 +131,7 @@ class ContainerDetailsClassBase { } public getDummyData(): Container { - return { - id: '4e90debf-d063-49fd-9a5d-71ee43e6840a', - name: 'departments', - fullyQualifiedName: 's3_storage_sample.departments', - displayName: 'Company departments', - description: 'Bucket containing company department information. asd', - version: 0.3, - updatedAt: 1722838506844, - updatedBy: 'sachin', - href: 'http://test-argo.getcollate.io/api/v1/containers/4e90debf-d063-49fd-9a5d-71ee43e6840a', - service: { - id: '5354aaf3-063e-47aa-9f1d-bae19755e905', - type: 'storageService', - name: 's3_storage_sample', - fullyQualifiedName: 's3_storage_sample', - displayName: 's3_storage_sample', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/services/storageServices/5354aaf3-063e-47aa-9f1d-bae19755e905', - }, - children: [ - { - id: '11e8f1c5-77c8-4a27-a546-c6561baeba18', - type: 'container', - name: 'engineering', - fullyQualifiedName: 's3_storage_sample.departments.engineering', - description: 'Bucket containing engineering department information', - displayName: 'Engineering department', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/containers/11e8f1c5-77c8-4a27-a546-c6561baeba18', - }, - { - id: 'c704e3d2-33ec-4cf0-a3fc-5e8d181c2723', - type: 'container', - name: 'finance', - fullyQualifiedName: 's3_storage_sample.departments.finance', - description: 'Bucket containing finance department information', - displayName: 'Finance department', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/containers/c704e3d2-33ec-4cf0-a3fc-5e8d181c2723', - }, - { - id: 'ffe5b6be-57cd-4cdc-9e0a-09677658160c', - type: 'container', - name: 'media', - fullyQualifiedName: 's3_storage_sample.departments.media', - description: 'Bucket containing media department information', - displayName: 'Media department', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/containers/ffe5b6be-57cd-4cdc-9e0a-09677658160c', - }, - ], - prefix: '/departments/', - numberOfObjects: 2, - size: 2048, - fileFormats: [FileFormat.CSV], - serviceType: StorageServiceType.S3, - deleted: false, - }; + return CONTAINER_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts index f0c251157855..428c3291680a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDataModelClassBase.ts @@ -20,6 +20,7 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { DASHBOARD_DATA_MODEL_DUMMY_DATA } from '../constants/Dashboard.constnats'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; @@ -129,100 +130,7 @@ class DashboardDataModelBase { } public getDummyData(): DashboardDataModel { - return { - id: '3519340b-7a36-45d1-abdf-348a5f1c582c', - name: 'orders', - displayName: 'Orders', - fullyQualifiedName: 'sample_looker.model.orders', - description: 'Orders explore from Sample Data', - version: 0.1, - updatedAt: 1697265260863, - updatedBy: 'ingestion-bot', - href: 'http://sandbox-beta.open-metadata.org/api/v1/dashboard/datamodels/3519340b-7a36-45d1-abdf-348a5f1c582c', - owners: [], - dataProducts: [], - tags: [], - deleted: false, - followers: [], - service: { - id: '2d102aaa-e683-425c-a8bf-e4afa43dde99', - type: 'dashboardService', - name: 'sample_looker', - fullyQualifiedName: 'sample_looker', - displayName: 'sample_looker', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/dashboardServices/2d102aaa-e683-425c-a8bf-e4afa43dde99', - }, - serviceType: 'Looker', - dataModelType: 'LookMlExplore', - // eslint-disable-next-line max-len - sql: "SELECT CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END AS clinical_stage,\n COUNT(*) AS count\nFROM covid_vaccines\nGROUP BY CASE\n WHEN stage_of_development = 'Pre-clinical' THEN '0. Pre-clinical'\n WHEN stage_of_development = 'Phase I' THEN '1. Phase I'\n WHEN stage_of_development = 'Phase I/II'\n or stage_of_development = 'Phase II' THEN '2. Phase II or Combined I/II'\n WHEN stage_of_development = 'Phase III' THEN '3. Phase III'\n WHEN stage_of_development = 'Authorized' THEN '4. Authorized'\n END\nORDER BY count DESC\nLIMIT 10000\nOFFSET 0;\n", - columns: [ - { - name: '0. Pre-clinical', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: "Vaccine Candidates in phase: 'Pre-clinical'", - fullyQualifiedName: 'sample_looker.model.orders."0. Pre-clinical"', - tags: [], - ordinalPosition: 1, - }, - { - name: '2. Phase II or Combined I/II', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: - "Vaccine Candidates in phase: 'Phase II or Combined I/II'", - fullyQualifiedName: - 'sample_looker.model.orders."2. Phase II or Combined I/II"', - tags: [], - ordinalPosition: 2, - }, - { - name: '1. Phase I', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: "Vaccine Candidates in phase: 'Phase I'", - fullyQualifiedName: 'sample_looker.model.orders."1. Phase I"', - tags: [], - ordinalPosition: 3, - }, - { - name: '3. Phase III', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: "Vaccine Candidates in phase: 'Phase III'", - fullyQualifiedName: 'sample_looker.model.orders."3. Phase III"', - tags: [], - ordinalPosition: 4, - }, - { - name: '4. Authorized', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: "Vaccine Candidates in phase: 'Authorize'", - fullyQualifiedName: 'sample_looker.model.orders."4. Authorized"', - tags: [], - ordinalPosition: 5, - }, - ], - domain: { - id: '52fc9c67-78b7-42bf-8147-69278853c230', - type: 'domain', - name: 'Design', - fullyQualifiedName: 'Design', - description: "

Here' the description for Product Design

", - displayName: 'Product Design ', - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/52fc9c67-78b7-42bf-8147-69278853c230', - }, - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - } as DashboardDataModel; + return DASHBOARD_DATA_MODEL_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts index c72a7a47b2a4..74f5704bc2b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsClassBase.ts @@ -20,12 +20,10 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { DASHBOARD_DUMMY_DATA } from '../constants/Dashboard.constnats'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - Dashboard, - DashboardServiceType, -} from '../generated/entity/data/dashboard'; +import { Dashboard } from '../generated/entity/data/dashboard'; import { Tab } from '../generated/system/ui/page'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; @@ -130,76 +128,7 @@ class DashboardDetailsClassBase { } public getDummyData(): Dashboard { - return { - id: '574c383c-735f-44c8-abbb-355f87c8b19f', - name: 'customers', - displayName: 'Customers dashboard', - fullyQualifiedName: 'SampleLookerService.customers', - description: 'This is a sample Dashboard for Looker', - version: 0.1, - updatedAt: 1736493713236, - updatedBy: 'admin', - sourceUrl: 'http://localhost:808/looker/dashboard/1/', - charts: [ - { - id: '81cdc1f3-66ae-462f-bf3e-b5fbbfe7792f', - type: 'chart', - name: 'chart_1', - fullyQualifiedName: 'SampleLookerService.chart_1', - description: 'This is a sample Chart for Looker', - displayName: 'Chart 1', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/charts/81cdc1f3-66ae-462f-bf3e-b5fbbfe7792f', - }, - { - id: '6f5057aa-8d7c-41a7-ab93-76bf8ed2bc27', - type: 'chart', - name: 'chart_2', - fullyQualifiedName: 'SampleLookerService.chart_2', - description: 'This is a sample Chart for Looker', - displayName: 'Chart 2', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/charts/6f5057aa-8d7c-41a7-ab93-76bf8ed2bc27', - }, - ], - href: 'http://test-argo.getcollate.io/api/v1/dashboards/574c383c-735f-44c8-abbb-355f87c8b19f', - owners: [], - followers: [], - tags: [], - service: { - id: 'fb4df3ed-75b9-45d3-a2df-da07785893d7', - type: 'dashboardService', - name: 'SampleLookerService', - fullyQualifiedName: 'SampleLookerService', - displayName: 'SampleLookerService', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/services/dashboardServices/fb4df3ed-75b9-45d3-a2df-da07785893d7', - }, - serviceType: DashboardServiceType.Looker, - usageSummary: { - dailyStats: { - count: 0, - percentileRank: 0, - }, - weeklyStats: { - count: 0, - percentileRank: 0, - }, - monthlyStats: { - count: 0, - percentileRank: 0, - }, - date: new Date('2025-02-03'), - }, - deleted: false, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return DASHBOARD_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts index d445c9e6d8b0..b3bde4cc7dfc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Database/DatabaseClassBase.ts @@ -20,15 +20,11 @@ import { GridSizes, TAGS_WIDGET, } from '../../constants/CustomizeWidgets.constants'; +import { DATABASE_DUMMY_DATA } from '../../constants/Database.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; -import { - Database, - DatabaseServiceType, - State, -} from '../../generated/entity/data/database'; +import { Database } from '../../generated/entity/data/database'; import { Tab } from '../../generated/system/ui/uiCustomization'; -import { LabelType, TagSource } from '../../generated/type/tagLabel'; import { FeedCounts } from '../../interface/feed.interface'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; @@ -131,109 +127,7 @@ class DatabaseClassBase { } public getDummyData(): Database { - return { - id: '77147d45-888b-42dd-a369-8b7ba882dffb', - name: 'ecommerce_db', - fullyQualifiedName: 'sample_data.ecommerce_db', - displayName: '', - description: - 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', - dataProducts: [], - tags: [ - { - tagFQN: 'KnowledgeCenter.QuickLink', - name: 'QuickLink', - description: 'Knowledge Quick Link.', - style: { - color: '#c415d1', - }, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ], - version: 1.2, - updatedAt: 1736405710107, - updatedBy: 'prajwal.p', - href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', - owners: [ - { - id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', - type: 'user', - name: 'aaron.singh2', - fullyQualifiedName: '"aaron.singh2"', - displayName: 'Aaron Singh', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', - }, - { - id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - type: 'user', - name: 'ayush', - fullyQualifiedName: 'ayush', - displayName: 'Ayush Shah', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - }, - { - id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', - type: 'user', - name: 'ayush02shah12', - fullyQualifiedName: 'ayush02shah12', - displayName: 'Ayush Shah', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', - }, - { - id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', - type: 'user', - name: 'prajwal161998', - fullyQualifiedName: 'prajwal161998', - displayName: 'prajwal161998', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', - }, - ], - service: { - id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', - type: 'databaseService', - name: 'sample_data', - fullyQualifiedName: 'sample_data', - displayName: 'sample_data', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', - }, - serviceType: DatabaseServiceType.BigQuery, - changeDescription: { - fieldsAdded: [ - { - name: 'owners', - newValue: - '[{"id":"f7971c49-bca7-48fb-bb1a-821a1e2c5802","type":"user","name":"prajwal161998","fullyQualifiedName":"prajwal161998","displayName":"prajwal161998","deleted":false}]', - }, - ], - fieldsUpdated: [], - fieldsDeleted: [], - previousVersion: 1.1, - }, - default: false, - deleted: false, - domain: { - id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - type: 'domain', - name: 'Engineering', - fullyQualifiedName: 'Engineering', - description: 'Domain related engineering development.', - displayName: 'Engineering', - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - }, - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return DATABASE_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts index 88addfa3a545..1358d5048f35 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseSchemaClassBase.ts @@ -20,16 +20,12 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { DATABASE_SCHEMA_DUMMY_DATA } from '../constants/Database.constants'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - DatabaseSchema, - DatabaseServiceType, - State, -} from '../generated/entity/data/databaseSchema'; +import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { LabelType, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; @@ -150,128 +146,7 @@ class DatabaseSchemaClassBase { } public getDummyData(): DatabaseSchema { - return { - id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', - name: 'shopify', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify', - description: - 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', - dataProducts: [], - version: 1.1, - updatedAt: 1736405774154, - updatedBy: 'prajwal.p', - href: 'http://sandbox-beta.open-metadata.org/api/v1/databaseSchemas/9f127bdc-d060-4fac-ae7b-c635933fc2e0', - owners: [ - { - id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', - type: 'user', - name: 'aaron.singh2', - fullyQualifiedName: '"aaron.singh2"', - displayName: 'Aaron Singh', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', - }, - { - id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - type: 'user', - name: 'ayush', - fullyQualifiedName: 'ayush', - displayName: 'Ayush Shah', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - }, - { - id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', - type: 'user', - name: 'ayush02shah12', - fullyQualifiedName: 'ayush02shah12', - displayName: 'Ayush Shah', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', - }, - { - id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', - type: 'user', - name: 'prajwal161998', - fullyQualifiedName: 'prajwal161998', - displayName: 'prajwal161998', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', - }, - ], - service: { - id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', - type: 'databaseService', - name: 'sample_data', - fullyQualifiedName: 'sample_data', - displayName: 'sample_data', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', - }, - serviceType: DatabaseServiceType.BigQuery, - database: { - id: '77147d45-888b-42dd-a369-8b7ba882dffb', - type: 'database', - name: 'ecommerce_db', - fullyQualifiedName: 'sample_data.ecommerce_db', - description: - 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', - displayName: 'ecommerce_db', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', - }, - usageSummary: { - dailyStats: { - count: 21, - percentileRank: 0, - }, - weeklyStats: { - count: 21, - percentileRank: 0, - }, - monthlyStats: { - count: 21, - percentileRank: 0, - }, - date: new Date('2023-11-10'), - }, - tags: [ - { - tagFQN: 'PersonalData.Personal', - name: 'Personal', - description: - 'Data that can be used to directly or indirectly identify a person.', - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ], - deleted: false, - domain: { - id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - type: 'domain', - name: 'Engineering', - fullyQualifiedName: 'Engineering', - description: 'Domain related engineering development.', - displayName: 'Engineering', - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - }, - votes: { - upVotes: 0, - downVotes: 1, - upVoters: [], - downVoters: [ - { - id: 'f14f17bf-0923-4234-8e73-2dcc051f2adc', - type: 'user', - name: 'admin', - fullyQualifiedName: 'admin', - displayName: 'admin', - deleted: false, - }, - ], - }, - }; + return DATABASE_SCHEMA_DUMMY_DATA; } public getWidgetsFromKey(widgetConfig: WidgetConfig) { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 46fc1401e875..2fbb720197d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -20,6 +20,7 @@ import { DESCRIPTION_WIDGET, GridSizes, } from '../../constants/CustomizeWidgets.constants'; +import { DOMAIN_DUMMY_DATA } from '../../constants/Domain.constants'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; @@ -125,52 +126,7 @@ class DomainClassBase { } public getDummyData(): Domain { - return { - id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - domainType: 'Consumer-aligned', - name: 'Engineering', - fullyQualifiedName: 'Engineering', - displayName: 'Engineering', - description: 'Domain related engineering development.', - style: {}, - version: 0.8, - updatedAt: 1698061758989, - updatedBy: 'rupesh', - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - children: [], - owners: [ - { - id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', - type: 'team', - name: 'Data', - fullyQualifiedName: 'Data', - description: '', - displayName: 'Data', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/ebac156e-6779-499c-8bbf-ab98a6562bc5', - }, - ], - experts: [ - { - id: '34ee72dc-7dad-4710-9f1d-e934ad0554a9', - type: 'user', - name: 'brian_smith7', - fullyQualifiedName: 'brian_smith7', - displayName: 'Brian Smith', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/34ee72dc-7dad-4710-9f1d-e934ad0554a9', - }, - { - id: '9a6687fa-8bd5-446c-aa8f-81416c88fe67', - type: 'user', - name: 'brittney_thomas3', - fullyQualifiedName: 'brittney_thomas3', - displayName: 'Brittney Thomas', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/9a6687fa-8bd5-446c-aa8f-81416c88fe67', - }, - ], - } as Domain; + return DOMAIN_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts index a5965618ca16..6f149f60f5ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetricEntityUtils/MetricDetailsClassBase.ts @@ -21,15 +21,10 @@ import { GridSizes, TAGS_WIDGET, } from '../../constants/CustomizeWidgets.constants'; +import { METRIC_DUMMY_DATA } from '../../constants/Metric.constnsts'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; -import { - Language, - Metric, - MetricGranularity, - MetricType, - UnitOfMeasurement, -} from '../../generated/entity/data/metric'; +import { Metric } from '../../generated/entity/data/metric'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { FeedCounts } from '../../interface/feed.interface'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; @@ -141,35 +136,7 @@ class MetricDetailsClassBase { ]; } public getDummyData(): Metric { - return { - id: '2be869f2-a8c6-4781-89fa-df45d671e449', - name: 'LTV', - fullyQualifiedName: 'LTV', - displayName: 'Lifetime Value', - metricExpression: { - language: Language.Python, - code: 'def ltv(customer: Customer, orders: List[Order]):\n\tlifetime = customer.lifetime\n ltv: float = sum([o.amount for o in orders if o.customer_id == customer.id])\n \n return ltv', - }, - metricType: MetricType.Other, - unitOfMeasurement: UnitOfMeasurement.Dollars, - granularity: MetricGranularity.Quarter, - relatedMetrics: [], - version: 0.1, - updatedAt: 1727366423816, - updatedBy: 'teddy', - href: 'http://sandbox-beta.open-metadata.org/api/v1/metrics/2be869f2-a8c6-4781-89fa-df45d671e449', - owners: [], - followers: [], - tags: [], - deleted: false, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return METRIC_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts index d83e561abecc..57efd9f375bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModel/MlModelClassBase.ts @@ -21,14 +21,10 @@ import { GridSizes, TAGS_WIDGET, } from '../../constants/CustomizeWidgets.constants'; +import { ML_MODEL_DUMMY_DATA } from '../../constants/MlModel.constants'; import { DetailPageWidgetKeys } from '../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../enums/entity.enum'; -import { - FeatureSourceDataType, - FeatureType, - Mlmodel, - MlModelServiceType, -} from '../../generated/entity/data/mlmodel'; +import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { FeedCounts } from '../../interface/feed.interface'; import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.interface'; @@ -90,13 +86,21 @@ class MlModelDetailsClassBase { return [ { - h: 6, + h: 1, i: DetailPageWidgetKeys.DESCRIPTION, w: 6, x: 0, y: 0, static: false, }, + { + h: 8, + i: DetailPageWidgetKeys.ML_MODEL_FEATURES, + w: 6, + x: 0, + y: 1, + static: false, + }, { h: 1, i: DetailPageWidgetKeys.DATA_PRODUCTS, @@ -121,14 +125,6 @@ class MlModelDetailsClassBase { y: 3, static: false, }, - { - h: 2, - i: DetailPageWidgetKeys.RELATED_METRICS, - w: 2, - x: 6, - y: 4, - static: false, - }, { h: 4, i: DetailPageWidgetKeys.CUSTOM_PROPERTIES, @@ -140,159 +136,7 @@ class MlModelDetailsClassBase { ]; } public getDummyData(): Mlmodel { - return { - id: '6ef964b1-edb7-4d7c-85f1-51845197206c', - name: 'eta_predictions', - fullyQualifiedName: 'mlflow_svc.eta_predictions', - displayName: 'ETA Predictions', - description: 'ETA Predictions Model', - algorithm: 'Neural Network', - mlFeatures: [ - { - name: 'sales', - dataType: FeatureType.Numerical, - description: 'Sales amount', - fullyQualifiedName: 'mlflow_svc.eta_predictions.sales', - featureSources: [ - { - name: 'gross_sales', - dataType: FeatureSourceDataType.Integer, - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.fact_sale.gross_sales', - dataSource: { - id: '8cb6dbd3-1c4b-48c3-ab53-1e964f355d03', - type: 'table', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.fact_sale', - description: - // eslint-disable-next-line max-len - 'The fact table captures the value of products sold or returned, as well as the values of other charges such as taxes and shipping costs. The sales table contains one row per order line item, one row per returned line item, and one row per shipping charge. Use this table when you need financial metrics.', - href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/8cb6dbd3-1c4b-48c3-ab53-1e964f355d03', - }, - tags: [], - }, - ], - }, - { - name: 'persona', - dataType: FeatureType.Categorical, - description: 'type of buyer', - fullyQualifiedName: 'mlflow_svc.eta_predictions.persona', - featureSources: [ - { - name: 'membership', - dataType: FeatureSourceDataType.String, - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.raw_customer.membership', - dataSource: { - id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', - type: 'table', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.raw_customer', - description: - // eslint-disable-next-line max-len - 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', - href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/7de1572e-e66a-47b4-aec0-67366f1c56fb', - }, - tags: [], - }, - { - name: 'platform', - dataType: FeatureSourceDataType.String, - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.raw_customer.platform', - dataSource: { - id: '7de1572e-e66a-47b4-aec0-67366f1c56fb', - type: 'table', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.raw_customer', - description: - // eslint-disable-next-line max-len - 'This is a raw customers table as represented in our online DB. This contains personal, shipping and billing addresses and details of the customer store and customer profile. This table is used to build our dimensional and fact tables', - href: 'http://sandbox-beta.open-metadata.org/api/v1/tables/7de1572e-e66a-47b4-aec0-67366f1c56fb', - }, - tags: [], - }, - ], - featureAlgorithm: 'PCA', - }, - ], - mlHyperParameters: [ - { - name: 'regularisation', - value: '0.5', - }, - { - name: 'random', - value: 'hello', - }, - ], - target: 'ETA_time', - dashboard: { - id: '1faa74ba-9952-4591-aef2-9acfaf9c14d1', - type: 'dashboard', - name: 'eta_predictions_performance', - fullyQualifiedName: 'sample_superset.eta_predictions_performance', - description: '', - displayName: 'ETA Predictions Performance', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/dashboards/1faa74ba-9952-4591-aef2-9acfaf9c14d1', - }, - mlStore: { - storage: 's3://path-to-pickle', - imageRepository: 'https://docker.hub.com/image', - }, - server: 'http://my-server.ai/', - href: 'http://sandbox-beta.open-metadata.org/api/v1/mlmodels/6ef964b1-edb7-4d7c-85f1-51845197206c', - owners: [], - followers: [], - tags: [], - usageSummary: { - dailyStats: { - count: 0, - percentileRank: 0, - }, - weeklyStats: { - count: 0, - percentileRank: 0, - }, - monthlyStats: { - count: 0, - percentileRank: 0, - }, - date: new Date('2025-02-22'), - }, - version: 1.2, - updatedAt: 1722587630921, - updatedBy: 'harsh.s', - service: { - id: 'b8cbebe5-58f0-493e-81f9-ad76e7695164', - type: 'mlmodelService', - name: 'mlflow_svc', - fullyQualifiedName: 'mlflow_svc', - displayName: 'mlflow_svc', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/mlmodelServices/b8cbebe5-58f0-493e-81f9-ad76e7695164', - }, - serviceType: MlModelServiceType.Mlflow, - deleted: false, - domain: { - id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', - type: 'domain', - name: 'domain.with.dot', - fullyQualifiedName: '"domain.with.dot"', - description: 'domain.with.dot', - displayName: 'domain.with.dot', - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/761f0a12-7b08-4889-acc3-b8d4d11a7865', - }, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return ML_MODEL_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx index bd1edd011903..96c08a1152e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx @@ -142,7 +142,7 @@ export const getMlModelDetailsPageTabs = ({ }; export const getMlModelWidgetsFromKey = (widgetConfig: WidgetConfig) => { - if (widgetConfig.i.startsWith(DetailPageWidgetKeys.RELATED_METRICS)) { + if (widgetConfig.i.startsWith(DetailPageWidgetKeys.ML_MODEL_FEATURES)) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts index ef1198a631e6..1dde4e4eb4c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineClassBase.ts @@ -21,16 +21,11 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { PIPELINE_DUMMY_DATA } from '../constants/pipeline.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - Pipeline, - PipelineServiceType, - State, - StatusType, -} from '../generated/entity/data/pipeline'; +import { Pipeline } from '../generated/entity/data/pipeline'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { LabelType, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; @@ -139,139 +134,7 @@ class PipelineClassBase { } public getDummyData(): Pipeline { - return { - id: '60670fe2-eb0a-41a2-a683-d32964e7e61b', - name: 'dim_address_etl', - displayName: 'dim_address etl', - fullyQualifiedName: 'sample_airflow.dim_address_etl', - description: 'dim_address ETL pipeline', - dataProducts: [], - version: 0.2, - updatedAt: 1726204840178, - updatedBy: 'ashish', - sourceUrl: 'http://localhost:8080/tree?dag_id=dim_address_etl', - tasks: [ - { - name: 'dim_address_task', - displayName: 'dim_address Task', - fullyQualifiedName: 'sample_airflow.dim_address_etl.dim_address_task', - description: - 'Airflow operator to perform ETL and generate dim_address table', - sourceUrl: - 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=dim_address_task', - downstreamTasks: ['assert_table_exists'], - taskType: 'PrestoOperator', - tags: [ - { - tagFQN: '"collection.new"."I.have.dots"', - name: 'I.have.dots', - displayName: '', - description: 'asd', - source: TagSource.Glossary, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - { - tagFQN: 'test Classification.test tag 1', - name: 'test tag 1', - displayName: '', - description: 'test tag 1', - style: { - color: '#da1010', - iconURL: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJUAAACUCAMAAACtIJvYAAAAA1BMVEX/AAAZ4gk3AAAALElEQVR4nO3BMQEAAADCoPVP7WkJoAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAGVrgAAQdwvSEAAAAASUVORK5CYII=', - }, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ], - }, - { - name: 'assert_table_exists', - displayName: 'Assert Table Exists', - fullyQualifiedName: - 'sample_airflow.dim_address_etl.assert_table_exists', - description: 'Assert if a table exists', - sourceUrl: - 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists', - downstreamTasks: [], - taskType: 'HiveOperator', - tags: [], - }, - ], - pipelineStatus: { - timestamp: 1723014798482, - executionStatus: StatusType.Pending, - taskStatus: [ - { - name: 'dim_address_task', - executionStatus: StatusType.Pending, - }, - { - name: 'assert_table_exists', - executionStatus: StatusType.Pending, - }, - ], - }, - followers: [ - { - id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', - type: 'user', - name: 'ashish', - fullyQualifiedName: 'ashish', - description: - '

data driven car

Hybrid Modal

', - displayName: 'Ashish', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/users/e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', - }, - ], - tags: [ - { - tagFQN: 'Tier.Tier1', - name: 'Tier1', - displayName: 'Priority 1', - description: - // eslint-disable-next-line max-len - '**Critical Source of Truth business data assets of an organization**\n\n- Used in critical metrics and dashboards to drive business and product decisions\n\n- Used in critical compliance reporting to regulators, govt entities, and third party\n\n- Used in brand or revenue impacting online user-facing experiences (search results, advertisement, promotions, and experimentation)\n\n- Other high impact use, such as ML models and fraud detection\n\n- Source used to derive other critical Tier-1 datasets', - style: {}, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ], - href: 'http://test-argo.getcollate.io/api/v1/pipelines/60670fe2-eb0a-41a2-a683-d32964e7e61b', - owners: [], - service: { - id: 'f83c2b6e-0d09-4839-98df-5571cc17c829', - type: 'pipelineService', - name: 'sample_airflow', - fullyQualifiedName: 'sample_airflow', - displayName: 'sample_airflow', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/services/pipelineServices/f83c2b6e-0d09-4839-98df-5571cc17c829', - }, - serviceType: PipelineServiceType.Airflow, - deleted: false, - scheduleInterval: '5 * * * *', - domain: { - id: '6b440596-144b-417b-b7ee-95cf0b0d7de4', - type: 'domain', - name: 'Version -1.6.2 Domain', - fullyQualifiedName: '"Version -1.6.2 Domain"', - description: '

Version -1.6.2 Domain

', - displayName: 'Version -1.6.2 Domain', - inherited: true, - href: 'http://test-argo.getcollate.io/api/v1/domains/6b440596-144b-417b-b7ee-95cf0b0d7de4', - }, - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return PIPELINE_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts index 641dc5e6edf4..578b41552af4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexDetailsClassBase.ts @@ -21,17 +21,11 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { SEARCH_INDEX_DUMMY_DATA } from '../constants/SearchIndex.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - DataType, - IndexType, - SearchIndex, - SearchServiceType, - State, -} from '../generated/entity/data/searchIndex'; +import { SearchIndex } from '../generated/entity/data/searchIndex'; import { Tab } from '../generated/system/ui/uiCustomization'; -import { LabelType, TagSource } from '../generated/type/tagLabel'; import { FeedCounts } from '../interface/feed.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils'; @@ -141,187 +135,7 @@ class SearchIndexClassBase { } public getDummyData(): SearchIndex { - return { - id: '2de6c0f1-c85a-4c90-b74b-b40869cc446c', - name: 'table_search_index', - fullyQualifiedName: 'elasticsearch_sample.table_search_index', - displayName: 'TableSearchIndex', - description: 'Table Search Index', - version: 0.2, - updatedAt: 1726204919320, - updatedBy: 'ashish', - service: { - id: 'dde0ee41-f45f-4cd4-8826-07cd298905f2', - type: 'searchService', - name: 'elasticsearch_sample', - fullyQualifiedName: 'elasticsearch_sample', - displayName: 'elasticsearch_sample', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/services/storageServices/dde0ee41-f45f-4cd4-8826-07cd298905f2', - }, - serviceType: SearchServiceType.ElasticSearch, - fields: [ - { - name: 'name', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Table Entity Name.', - fullyQualifiedName: 'elasticsearch_sample.table_search_index.name', - tags: [], - }, - { - name: 'displayName', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Table Entity DisplayName.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.displayName', - tags: [], - }, - { - name: 'description', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Table Entity Description.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.description', - tags: [], - }, - { - name: 'columns', - dataType: DataType.Nested, - dataTypeDisplay: 'nested', - description: 'Table Columns.', - fullyQualifiedName: 'elasticsearch_sample.table_search_index.columns', - tags: [], - children: [ - { - name: 'name', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Column Name.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.columns.name', - tags: [], - }, - { - name: 'displayName', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Column DisplayName.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.columns.displayName', - tags: [], - }, - { - name: 'description', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Column Description.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.columns.description', - tags: [], - }, - ], - }, - { - name: 'databaseSchema', - dataType: DataType.Text, - dataTypeDisplay: 'text', - description: 'Database Schema that this table belongs to.', - fullyQualifiedName: - 'elasticsearch_sample.table_search_index.databaseSchema', - tags: [], - }, - ], - indexType: IndexType.Index, - owners: [], - followers: [ - { - id: 'e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', - type: 'user', - name: 'ashish', - fullyQualifiedName: 'ashish', - description: - '

data driven car

Hybrid Modal

', - displayName: 'Ashish', - deleted: false, - href: 'http://test-argo.getcollate.io/api/v1/users/e596a9cd-9ce1-4e12-aee1-b6f31926b0e8', - }, - ], - tags: [ - { - tagFQN: '"Testing%Version@1.56"."156-Term.3"', - name: '156-Term.3', - displayName: '156-Term.3', - description: '156-Term.3', - source: TagSource.Glossary, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - { - tagFQN: '"classify-1.6.2".tagtest-1', - name: 'tagtest-1', - displayName: 'tagtest-1', - description: '

tagtest-1 for.1.6.2

', - style: {}, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - { - tagFQN: 'Data Center.center-1', - name: 'center-1', - displayName: '', - description: 'this is center-1', - style: {}, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - { - tagFQN: 'Tier.Tier5', - name: 'Tier5', - displayName: 'Priority 5', - description: - // eslint-disable-next-line max-len - '**Private/Unused data assets - No impact beyond individual users**\n\n- Data assets without any ownership with no usage in the last 60 days\n\n- Data assets owned by individuals without team ownership\n\n', - style: {}, - source: TagSource.Classification, - labelType: LabelType.Manual, - state: State.Confirmed, - }, - ], - href: 'http://test-argo.getcollate.io/api/v1/searchIndexes/2de6c0f1-c85a-4c90-b74b-b40869cc446c', - - deleted: false, - domain: { - id: 'a440b3a9-fbbf-464d-91d4-c9cfeeccc8e4', - type: 'domain', - name: 'Domain 1.6.0', - fullyQualifiedName: '"Domain 1.6.0"', - description: 'csacasc', - displayName: 'Domain 1.6.0', - href: 'http://test-argo.getcollate.io/api/v1/domains/a440b3a9-fbbf-464d-91d4-c9cfeeccc8e4', - }, - dataProducts: [ - { - id: '8c046cc5-ab23-40c0-a0bf-b58d206290f7', - type: 'dataProduct', - name: 'TestDataProduct', - fullyQualifiedName: 'TestDataProduct', - description: 'asd', - displayName: 'TestDataProduct', - href: 'http://test-argo.getcollate.io/api/v1/dataProducts/8c046cc5-ab23-40c0-a0bf-b58d206290f7', - }, - ], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - }; + return SEARCH_INDEX_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts index d03a6aae8550..176edb565b40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProcedureClassBase.ts @@ -20,6 +20,7 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { STORED_PROCEDURE_DUMMY_DATA } from '../constants/Table.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { StoredProcedure } from '../generated/entity/data/storedProcedure'; @@ -145,126 +146,7 @@ class StoredProcedureClassBase { } public getDummyData(): StoredProcedure { - return { - id: '5f3509ca-c8e5-449a-a7e8-b1c3b96e39e8', - name: 'calculate_average', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify.calculate_average', - description: 'Procedure to calculate average', - storedProcedureCode: { - // eslint-disable-next-line max-len - code: 'CREATE OR REPLACE PROCEDURE calculate_average(numbers INT ARRAY) RETURNS FLOAT NOT NULL LANGUAGE SQL AS $$DECLARE sum_val INT = 0;count_val INT = 0;average_val FLOAT;BEGIN\n FOR num IN ARRAY numbers DO sum_val := sum_val + num;\n count_val := count_val + 1;\nEND FOR;\nIF count_val = 0 THEN\n average_val := 0.0;\nELSE\n average_val := sum_val / count_val;\nEND IF;\nRETURN average_val;\nEND;$$;', - }, - version: 0.5, - dataProducts: [], - updatedAt: 1709544812674, - updatedBy: 'admin', - href: 'http://sandbox-beta.open-metadata.org/api/v1/storedProcedures/5f3509ca-c8e5-449a-a7e8-b1c3b96e39e8', - changeDescription: { - fieldsAdded: [], - fieldsUpdated: [ - { - name: 'deleted', - oldValue: true, - newValue: false, - }, - ], - fieldsDeleted: [], - previousVersion: 0.4, - }, - databaseSchema: { - id: '9f127bdc-d060-4fac-ae7b-c635933fc2e0', - type: 'databaseSchema', - name: 'shopify', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify', - description: - 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', - displayName: 'shopify', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/databaseSchemas/9f127bdc-d060-4fac-ae7b-c635933fc2e0', - }, - database: { - id: '77147d45-888b-42dd-a369-8b7ba882dffb', - type: 'database', - name: 'ecommerce_db', - fullyQualifiedName: 'sample_data.ecommerce_db', - description: - 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', - displayName: 'ecommerce_db', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/databases/77147d45-888b-42dd-a369-8b7ba882dffb', - }, - service: { - id: '75199480-3d06-4b6f-89d2-e8805ebe8d01', - type: 'databaseService', - name: 'sample_data', - fullyQualifiedName: 'sample_data', - displayName: 'sample_data', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/75199480-3d06-4b6f-89d2-e8805ebe8d01', - }, - serviceType: 'BigQuery', - deleted: false, - owners: [ - { - id: '50bb97a5-cf0c-4273-930e-b3e802b52ee1', - type: 'user', - name: 'aaron.singh2', - fullyQualifiedName: '"aaron.singh2"', - displayName: 'Aaron Singh', - deleted: false, - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/50bb97a5-cf0c-4273-930e-b3e802b52ee1', - }, - { - id: '1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - type: 'user', - name: 'ayush', - fullyQualifiedName: 'ayush', - displayName: 'Ayush Shah', - deleted: false, - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/1eb7eb26-21da-42d7-b0ed-8812f04f4ca4', - }, - { - id: '32e07f38-faff-45b1-9b51-4e42caa69e3c', - type: 'user', - name: 'ayush02shah12', - fullyQualifiedName: 'ayush02shah12', - displayName: 'Ayush Shah', - deleted: false, - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/32e07f38-faff-45b1-9b51-4e42caa69e3c', - }, - { - id: 'f7971c49-bca7-48fb-bb1a-821a1e2c5802', - type: 'user', - name: 'prajwal161998', - fullyQualifiedName: 'prajwal161998', - displayName: 'prajwal161998', - deleted: false, - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/f7971c49-bca7-48fb-bb1a-821a1e2c5802', - }, - ], - followers: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - tags: [], - domain: { - id: '31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - type: 'domain', - name: 'Engineering', - fullyQualifiedName: 'Engineering', - description: 'Domain related engineering development.', - displayName: 'Engineering', - inherited: true, - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/31c2b84e-b87a-4e47-934f-9c5309fbb7c3', - }, - } as StoredProcedure; + return STORED_PROCEDURE_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts index 9179a689e95b..942d11020d88 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableClassBase.ts @@ -20,17 +20,11 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { TABLE_DUMMY_DATA } from '../constants/Table.constants'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; -import { - ConstraintType, - DatabaseServiceType, - DataType, - RelationshipType, - Table, - TableType, -} from '../generated/entity/data/table'; +import { Table } from '../generated/entity/data/table'; import { Tab } from '../generated/system/ui/uiCustomization'; import { TestSummary } from '../generated/tests/testCase'; import { FeedCounts } from '../interface/feed.interface'; @@ -169,234 +163,7 @@ class TableClassBase { } public getDummyData(): Table { - return { - id: 'ab4f893b-c303-43d9-9375-3e620a670b02', - name: 'raw_product_catalog', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.raw_product_catalog', - description: - 'This is a raw product catalog table contains the product listing, price, seller etc.. represented in our online DB. ', - version: 0.2, - updatedAt: 1688442727895, - updatedBy: 'admin', - tableType: TableType.Regular, - dataProducts: [ - { - id: 'c9b891b1-5d60-4171-9af0-7fd6d74f8f2b', - type: 'dataProduct', - name: 'Design Data product ', - fullyQualifiedName: 'Design Data product ', - description: - "Here's the description for the Design Data product Name.", - displayName: 'Design Data product Name', - href: '#', - }, - ], - joins: { - startDate: new Date(), - dayCount: 30, - columnJoins: [ - { - columnName: 'address_id', - joinedWith: [ - { - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', - joinCount: 0, - }, - { - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address.address_id', - joinCount: 0, - }, - ], - }, - ], - directTableJoins: [ - { - fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_address', - joinCount: 0, - }, - ], - }, - columns: [ - { - name: 'address_id', - dataType: DataType.Numeric, - dataTypeDisplay: 'numeric', - description: 'Unique identifier for the address.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.address_id', - tags: [], - ordinalPosition: 1, - }, - { - name: 'shop_id', - dataType: DataType.Numeric, - dataTypeDisplay: 'numeric', - description: - 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim_shop table.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.shop_id', - tags: [], - ordinalPosition: 2, - }, - { - name: 'first_name', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'First name of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.first_name', - tags: [], - ordinalPosition: 3, - }, - { - name: 'last_name', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'Last name of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.last_name', - tags: [], - ordinalPosition: 4, - }, - { - name: 'address', - dataType: DataType.Varchar, - dataLength: 500, - dataTypeDisplay: 'varchar', - description: 'Clean address test', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.address', - tags: [], - ordinalPosition: 5, - }, - { - name: 'company', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: "The name of the customer's business, if one exists.", - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.company', - tags: [], - ordinalPosition: 7, - }, - { - name: 'city', - dataType: DataType.Varchar, - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'The name of the city. For example, Palo Alto.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.city', - tags: [], - ordinalPosition: 8, - }, - { - name: 'region', - dataType: DataType.Varchar, - dataLength: 512, - dataTypeDisplay: 'varchar', - description: - // eslint-disable-next-line max-len - 'The name of the region, such as a province or state, where the customer is located. For example, Ontario or New York. This column is the same as CustomerAddress.province in the Admin API.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.region', - tags: [], - ordinalPosition: 9, - }, - { - name: 'zip', - dataType: DataType.Varchar, - dataLength: 10, - dataTypeDisplay: 'varchar', - description: 'The ZIP or postal code. For example, 90210.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.zip', - tags: [], - ordinalPosition: 10, - }, - { - name: 'country', - dataType: DataType.Varchar, - dataLength: 50, - dataTypeDisplay: 'varchar', - description: 'The full name of the country. For example, Canada.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.country', - tags: [], - ordinalPosition: 11, - }, - { - name: 'phone', - dataType: DataType.Varchar, - dataLength: 15, - dataTypeDisplay: 'varchar', - description: 'The phone number of the customer.', - fullyQualifiedName: - 'sample_data.ecommerce_db.shopify.dim_address_clean.phone', - tags: [], - ordinalPosition: 12, - }, - ], - owners: [ - { - id: '38be030f-f817-4712-bc3b-ff7b9b9b805e', - type: 'user', - name: 'aaron_johnson0', - fullyQualifiedName: 'aaron_johnson0', - displayName: 'Aaron Johnson', - deleted: false, - }, - ], - databaseSchema: { - id: '3f0d9c39-0926-4028-8070-65b0c03556cb', - type: 'databaseSchema', - name: 'shopify', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify', - description: - 'This **mock** database contains schema related to shopify sales and orders with related dimension tables.', - deleted: false, - }, - database: { - id: 'f085e133-e184-47c8-ada5-d7e005d3153b', - type: 'database', - name: 'ecommerce_db', - fullyQualifiedName: 'sample_data.ecommerce_db', - description: - 'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.', - deleted: false, - }, - service: { - id: 'e61069a9-29e3-49fa-a7f4-f5227ae50b72', - type: 'databaseService', - name: 'sample_data', - fullyQualifiedName: 'sample_data', - deleted: false, - }, - tableConstraints: [ - { - constraintType: ConstraintType.ForeignKey, - columns: ['post_id'], - referredColumns: ['mysql_sample.default.posts_db.Posts.post_id'], - relationshipType: RelationshipType.ManyToOne, - }, - { - constraintType: ConstraintType.ForeignKey, - columns: ['user_id'], - referredColumns: ['mysql_sample.default.posts_db.Users.user_id'], - relationshipType: RelationshipType.ManyToOne, - }, - ], - serviceType: DatabaseServiceType.BigQuery, - tags: [], - followers: [], - deleted: false, - }; + return TABLE_DUMMY_DATA; } public getCommonWidgetList() { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts index 8e09b5e2411e..1380817af8a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicClassBase.ts @@ -20,6 +20,7 @@ import { GridSizes, TAGS_WIDGET, } from '../constants/CustomizeWidgets.constants'; +import { TOPIC_DUMMY_DATA } from '../constants/Topic.constant'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../enums/entity.enum'; import { Topic } from '../generated/entity/data/topic'; @@ -139,154 +140,7 @@ class TopicClassBase { } public getDummyData(): Topic { - return { - id: 'd68d2e86-41cd-4c6c-bd78-41db489d46d1', - name: 'address_book', - fullyQualifiedName: 'sample_kafka.address_book', - description: - 'All Protobuf record related events gets captured in this topic', - version: 0.7, - updatedAt: 1701432879105, - updatedBy: 'aniket', - service: { - id: '46f09e52-34de-4580-8133-a7e54e23c22b', - type: 'messagingService', - name: 'sample_kafka', - fullyQualifiedName: 'sample_kafka', - displayName: 'sample_kafka', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/services/messagingServices/46f09e52-34de-4580-8133-a7e54e23c22b', - }, - serviceType: 'Kafka', - messageSchema: { - schemaText: - // eslint-disable-next-line max-len - 'syntax = "proto2";\n\npackage tutorial;\n\nmessage Person {\n optional string name = 1;\n optional int32 id = 2;\n optional string email = 3;\n\n enum PhoneType {\n MOBILE = 0;\n HOME = 1;\n WORK = 2;\n }\n\n message PhoneNumber {\n optional string number = 1;\n optional PhoneType type = 2 [default = HOME];\n }\n\n repeated PhoneNumber phones = 4;\n}\n\nmessage AddressBook {\n repeated Person people = 1;\n}', - schemaType: 'Protobuf', - schemaFields: [ - { - name: 'AddressBook', - dataType: 'RECORD', - fullyQualifiedName: 'sample_kafka.address_book.AddressBook', - tags: [], - children: [ - { - name: 'people', - dataType: 'RECORD', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people', - tags: [], - children: [ - { - name: 'name', - dataType: 'STRING', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.name', - tags: [], - }, - { - name: 'id', - dataType: 'INT', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.id', - tags: [], - }, - { - name: 'email', - dataType: 'STRING', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.email', - tags: [], - }, - { - name: 'phones', - dataType: 'RECORD', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.phones', - tags: [], - children: [ - { - name: 'number', - dataType: 'STRING', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.phones.number', - tags: [], - }, - { - name: 'type', - dataType: 'ENUM', - fullyQualifiedName: - 'sample_kafka.address_book.AddressBook.people.phones.type', - tags: [], - }, - ], - }, - ], - }, - ], - }, - ], - }, - partitions: 128, - cleanupPolicies: ['compact', 'delete'], - replicationFactor: 4, - maximumMessageSize: 249, - retentionSize: 1931232624, - owners: [ - { - id: 'ebac156e-6779-499c-8bbf-ab98a6562bc5', - type: 'team', - name: 'Data', - fullyQualifiedName: 'Data', - description: '', - displayName: 'Data', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/teams/ebac156e-6779-499c-8bbf-ab98a6562bc5', - }, - ], - followers: [ - { - id: '96546482-1b99-4293-9e0f-7194fe25bcbf', - type: 'user', - name: 'sonal.w', - fullyQualifiedName: '"sonal.w"', - displayName: 'admin', - deleted: false, - href: 'http://sandbox-beta.open-metadata.org/api/v1/users/96546482-1b99-4293-9e0f-7194fe25bcbf', - }, - ], - tags: [], - href: 'http://sandbox-beta.open-metadata.org/api/v1/topics/d68d2e86-41cd-4c6c-bd78-41db489d46d1', - changeDescription: { - fieldsAdded: [], - fieldsUpdated: [ - { - name: 'deleted', - oldValue: true, - newValue: false, - }, - ], - fieldsDeleted: [], - previousVersion: 0.6, - }, - deleted: false, - domain: { - id: '761f0a12-7b08-4889-acc3-b8d4d11a7865', - type: 'domain', - name: 'domain.with.dot', - fullyQualifiedName: '"domain.with.dot"', - description: 'domain.with.dot', - displayName: 'domain.with.dot', - href: 'http://sandbox-beta.open-metadata.org/api/v1/domains/761f0a12-7b08-4889-acc3-b8d4d11a7865', - }, - dataProducts: [], - votes: { - upVotes: 0, - downVotes: 0, - upVoters: [], - downVoters: [], - }, - } as Topic; + return TOPIC_DUMMY_DATA; } public getCommonWidgetList() { From 96d7fd959dfe18f468183a50e22dc988fced3c4a Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:15:10 +0530 Subject: [PATCH 61/63] support tags and glossary to domain --- .../ui/src/utils/Domain/DomainClassBase.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index 2fbb720197d1..7fc205ae3567 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -103,12 +103,20 @@ class DomainClassBase { i: DetailPageWidgetKeys.OWNERS, w: 2, x: 6, + y: 0, + static: false, + }, + { + h: 2, + i: DetailPageWidgetKeys.TAGS, + w: 2, + x: 6, y: 1, static: false, }, { h: 2, - i: DetailPageWidgetKeys.EXPERTS, + i: DetailPageWidgetKeys.GLOSSARY_TERMS, w: 2, x: 6, y: 2, @@ -116,12 +124,20 @@ class DomainClassBase { }, { h: 2, - i: DetailPageWidgetKeys.DOMAIN_TYPE, + i: DetailPageWidgetKeys.EXPERTS, w: 2, x: 6, y: 3, static: false, }, + { + h: 2, + i: DetailPageWidgetKeys.DOMAIN_TYPE, + w: 2, + x: 6, + y: 4, + static: false, + }, ]; } From 9680a4f000b801a3dfe8267fcc00dcb3b662711c Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:09:04 +0530 Subject: [PATCH 62/63] fix failing tests --- .../ui/playwright/support/entity/ingestion/ServiceBaseClass.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index 2b88a4cf8609..0526a455e550 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -524,9 +524,6 @@ class ServiceBaseClass { page.getByTestId('KnowledgePanel.GlossaryTerms') ).toBeVisible(); await expect(page.getByTestId('KnowledgePanel.DataProducts')).toBeVisible(); - await expect( - page.getByTestId('KnowledgePanel.CustomProperties') - ).toBeVisible(); } async runAdditionalTests( From d0e6f3e66bd5584fbe694efdcfbd49612bfd9bf8 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:13:28 +0530 Subject: [PATCH 63/63] option to show hide task handlers --- .../DataAssets/CommonWidgets/CommonWidgets.tsx | 16 ++++++++++++---- .../main/resources/ui/src/utils/DomainUtils.tsx | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx index 5b0e2aa2f8fe..b5e0a52d16a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/CommonWidgets/CommonWidgets.tsx @@ -53,11 +53,13 @@ interface GenericEntity interface CommonWidgetsProps { widgetConfig: WidgetConfig; entityType: EntityType; + showTaskHandler?: boolean; } export const CommonWidgets = ({ widgetConfig, entityType, + showTaskHandler = true, }: CommonWidgetsProps) => { const { data, type, onUpdate, permissions } = useGenericContext(); @@ -140,32 +142,38 @@ export const CommonWidgets = ({ const tagsWidget = useMemo(() => { return ( ); - }, [editTagsPermission, tags, type, fullyQualifiedName]); + }, [editTagsPermission, tags, type, fullyQualifiedName, showTaskHandler]); const glossaryWidget = useMemo(() => { return ( ); - }, [editGlossaryTermsPermission, tags, type, fullyQualifiedName]); + }, [ + editGlossaryTermsPermission, + tags, + type, + fullyQualifiedName, + showTaskHandler, + ]); const descriptionWidget = useMemo(() => { return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index 8d7c41f07fc1..31ff8d75a2e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -458,6 +458,10 @@ export const getDomainWidgetsFromKey = (widgetConfig: WidgetConfig) => { } return ( - + ); };