diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-stored-procedure.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-stored-procedure.svg new file mode 100644 index 000000000000..d78213314b89 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-stored-procedure.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 27ede19f766b..7b90285c7482 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 @@ -21,6 +21,7 @@ import { Database } from 'generated/entity/data/database'; import { DatabaseSchema } from 'generated/entity/data/databaseSchema'; import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Pipeline } from 'generated/entity/data/pipeline'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { Table } from 'generated/entity/data/table'; import { Topic } from 'generated/entity/data/topic'; import { DashboardService } from 'generated/entity/services/dashboardService'; @@ -43,6 +44,7 @@ export type DataAssetsType = | Container | Database | DashboardDataModel + | StoredProcedure | DatabaseSchema | DatabaseService | MessagingService @@ -90,6 +92,7 @@ export type DataAssetsHeaderProps = { | DataAssetMlmodel | DataAssetContainer | DataAssetDashboardDataModel + | DataAssetStoredProcedure | DataAssetDatabase | DataAssetDatabaseSchema | DataAssetDatabaseService @@ -135,6 +138,11 @@ export interface DataAssetDashboardDataModel { entityType: EntityType.DASHBOARD_DATA_MODEL; } +export interface DataAssetStoredProcedure { + dataAsset: StoredProcedure; + entityType: EntityType.STORED_PROCEDURE; +} + export interface DataAssetDatabase { dataAsset: Database; entityType: EntityType.DATABASE; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx index 8eea039a20bf..f92240358c1a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityInfoDrawer/EntityInfoDrawer.component.tsx @@ -19,12 +19,14 @@ import DashboardSummary from 'components/Explore/EntitySummaryPanel/DashboardSum import DataModelSummary from 'components/Explore/EntitySummaryPanel/DataModelSummary/DataModelSummary.component'; import MlModelSummary from 'components/Explore/EntitySummaryPanel/MlModelSummary/MlModelSummary.component'; import PipelineSummary from 'components/Explore/EntitySummaryPanel/PipelineSummary/PipelineSummary.component'; +import StoredProcedureSummary from 'components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.component'; import TableSummary from 'components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component'; import TopicSummary from 'components/Explore/EntitySummaryPanel/TopicSummary/TopicSummary.component'; import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; import { Container } from 'generated/entity/data/container'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { Mlmodel } from 'generated/entity/data/mlmodel'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { EntityDetailUnion } from 'Models'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,6 +35,7 @@ import { getDataModelsByName } from 'rest/dataModelsAPI'; import { getMlModelByFQN } from 'rest/mlModelAPI'; import { getPipelineByFqn } from 'rest/pipelineAPI'; import { getContainerByName } from 'rest/storageAPI'; +import { getStoredProceduresByName } from 'rest/storedProceduresAPI'; import { getTableDetailsByFQN } from 'rest/tableAPI'; import { getTopicByFqn } from 'rest/topicsAPI'; import { EntityType } from '../../../enums/entity.enum'; @@ -130,6 +133,12 @@ const EntityInfoDrawer = ({ break; } + + case EntityType.STORED_PROCEDURE: { + response = await getStoredProceduresByName(encodedFqn, 'owner,tags'); + + break; + } default: break; } @@ -221,6 +230,16 @@ const EntityInfoDrawer = ({ /> ); + case EntityType.STORED_PROCEDURE: + return ( + + ); + default: return null; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.component.tsx new file mode 100644 index 000000000000..c24fe1456422 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.component.tsx @@ -0,0 +1,132 @@ +/* + * 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, Divider, Row, Typography } from 'antd'; +import classNames from 'classnames'; +import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component'; +import SchemaEditor from 'components/schema-editor/SchemaEditor'; +import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component'; +import { CSMode } from 'enums/codemirror.enum'; +import { ExplorePageTabs } from 'enums/Explore.enum'; +import { StoredProcedureCodeObject } from 'generated/entity/data/storedProcedure'; +import { isObject } from 'lodash'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { + DRAWER_NAVIGATION_OPTIONS, + getEntityOverview, +} from 'utils/EntityUtils'; +import { StoredProcedureSummaryProps } from './StoredProcedureSummary.interface'; + +const StoredProcedureSummary = ({ + entityDetails, + componentType = DRAWER_NAVIGATION_OPTIONS.explore, + tags, + isLoading, +}: StoredProcedureSummaryProps) => { + const { t } = useTranslation(); + + const entityInfo = useMemo( + () => getEntityOverview(ExplorePageTabs.STORED_PROCEDURE, entityDetails), + [entityDetails] + ); + + return ( + + <> + + + + {entityInfo.map((info) => { + const isOwner = info.name === t('label.owner'); + + return info.visible?.includes(componentType) ? ( + + + {!isOwner ? ( + + + {info.name} + + + ) : null} + + {info.isLink ? ( + + {info.value} + + ) : ( + + {info.value} + + )} + + + + ) : null; + })} + + + + + + + + + + {isObject(entityDetails.storedProcedureCode) && ( + + + + {t('label.code')} + + + + + + + )} + + + ); +}; + +export default StoredProcedureSummary; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.interface.ts new file mode 100644 index 000000000000..f5ae6e8905a2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/StoredProcedureSummary/StoredProcedureSummary.interface.ts @@ -0,0 +1,22 @@ +/* + * 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 { StoredProcedure } from 'generated/entity/data/storedProcedure'; +import { TagLabel } from 'generated/type/tagLabel'; + +export interface StoredProcedureSummaryProps { + entityDetails: StoredProcedure; + componentType?: string; + tags?: TagLabel[]; + isLoading: boolean; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/explore.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/explore.interface.ts index b20b317ff008..82d3b9a8d6ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/explore.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/explore.interface.ts @@ -20,6 +20,7 @@ import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { Database } from 'generated/entity/data/database'; import { DatabaseSchema } from 'generated/entity/data/databaseSchema'; import { Glossary } from 'generated/entity/data/glossary'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { QueryFilterInterface } from 'pages/explore/ExplorePage.interface'; import { SearchIndex } from '../../enums/search.enum'; import { Dashboard } from '../../generated/entity/data/dashboard'; @@ -120,7 +121,8 @@ export type EntityUnion = | Database | Glossary | Tag - | DashboardDataModel; + | DashboardDataModel + | StoredProcedure; export type EntityWithServices = | Topic diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PermissionProvider/PermissionProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/PermissionProvider/PermissionProvider.interface.ts index 618140e8cdb2..fcca04e47411 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PermissionProvider/PermissionProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/PermissionProvider/PermissionProvider.interface.ts @@ -72,6 +72,7 @@ export enum ResourceEntity { QUERY = 'query', DASHBOARD_DATA_MODEL = 'dashboardDataModel', EVENT_SUBSCRIPTION = 'eventsubscription', + STORED_PROCEDURE = 'storedProcedure', } export interface PermissionContextType { 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 f4ce98ecc94a..70e357806921 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 @@ -12,6 +12,7 @@ */ import { Container } from 'generated/entity/data/container'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { EntityType } from '../../../enums/entity.enum'; import { Dashboard } from '../../../generated/entity/data/dashboard'; import { Mlmodel } from '../../../generated/entity/data/mlmodel'; @@ -24,7 +25,8 @@ export type EntityDetails = Table & Dashboard & Pipeline & Mlmodel & - Container; + Container & + StoredProcedure; export interface CustomPropertyProps { isVersionView?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx index 35ba8cb33e81..8dc4b4bb3b9b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx @@ -132,6 +132,10 @@ const DataModelDetailsPage = withSuspenseFallback( React.lazy(() => import('pages/DataModelPage/DataModelPage.component')) ); +const StoredProcedureDetailsPage = withSuspenseFallback( + React.lazy(() => import('pages/StoredProcedure/StoredProcedurePage')) +); + const TableDetailsPageV1 = withSuspenseFallback( React.lazy(() => import('pages/TableDetailsPageV1/TableDetailsPageV1')) ); @@ -461,6 +465,23 @@ const AuthenticatedAppRouter: FunctionComponent = () => { component={DataModelDetailsPage} path={ROUTES.DATA_MODEL_DETAILS_WITH_SUB_TAB} /> + + + + + { return `${path}${columnName ? `.${columnName}` : ''}`; }; +export const getStoredProcedureDetailsPath = ( + storedProcedureFQN: string, + columnName?: string +) => { + let path = ROUTES.STORED_PROCEDURE_DETAILS; + path = path.replace( + PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN, + getEncodedFqn(storedProcedureFQN) + ); + + return `${path}${columnName ? `.${columnName}` : ''}`; +}; + export const getTagsDetailsPath = (entityFQN: string, columnName?: string) => { let path = ROUTES.TAG_DETAILS; const classification = getPartialNameFromFQN(entityFQN, ['service']); @@ -660,6 +678,32 @@ export const getContainerDetailPath = ( return path; }; +export const getStoredProcedureDetailPath = ( + storedProcedureFQN: string, + tab?: string, + subTab = 'all' +) => { + let path = tab + ? ROUTES.STORED_PROCEDURE_DETAILS_WITH_TAB + : ROUTES.STORED_PROCEDURE_DETAILS; + + if (tab === EntityTabs.ACTIVITY_FEED) { + path = ROUTES.STORED_PROCEDURE_DETAILS_WITH_SUB_TAB; + path = path.replace(PLACEHOLDER_ROUTE_SUB_TAB, subTab); + } + + if (tab) { + path = path.replace(PLACEHOLDER_ROUTE_TAB, tab); + } + + path = path.replace( + PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN, + getEncodedFqn(storedProcedureFQN) + ); + + return path; +}; + export const getGlossaryTermDetailsPath = ( glossaryFQN: string, tab?: string @@ -784,6 +828,7 @@ export const ENTITY_PATH: Record = { containers: 'container', tags: 'tag', glossaries: 'glossary', + storedprocedure: 'storedProcedure', }; export const VALIDATION_MESSAGES = { diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/Explore.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/Explore.enum.ts index 5b2e8a09a918..1ac016a00288 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/Explore.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/Explore.enum.ts @@ -26,4 +26,5 @@ export enum ExplorePageTabs { GLOSSARY = 'glossaries', TAG = 'tags', DASHBOARD_DATA_MODEL = 'dashboardDataModel', + STORED_PROCEDURE = 'storedProcedure', } 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 b2ad0e6dc327..82d580f091d3 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 @@ -48,6 +48,7 @@ export enum EntityType { USER_NAME = 'username', CHART = 'chart', SAMPLE_DATA = 'sampleData', + STORED_PROCEDURE = 'storedProcedure', } export enum AssetsType { @@ -161,6 +162,8 @@ export enum EntityTabs { INGESTIONS = 'ingestions', CONNECTION = 'connection', SQL = 'sql', + STORED_PROCEDURE = 'stored_procedure', + CODE = 'code', } export enum EntityAction { 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 895461e4449f..71ba160ccd0b 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 @@ -134,6 +134,7 @@ "closed-task-plural": "Closed Tasks", "closed-this-task-lowercase": "closed this task", "cloud-config-source": "Cloud Config Source", + "code": "Code", "collapse-all": "Collapse All", "column": "Column", "column-entity": "Column {{entity}}", @@ -877,6 +878,7 @@ "stopped": "Stopped", "storage": "Storage", "storage-plural": "Storages", + "stored-procedure": "Stored Procedure", "sub-team-plural": "Sub Teams", "submit": "Submit", "success": "Success", 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 4f50d7f72359..2d616a5887ae 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 @@ -134,6 +134,7 @@ "closed-task-plural": "Tareas cerradas", "closed-this-task-lowercase": "cerró esta tarea", "cloud-config-source": "Fuente de configuración en el cloud", + "code": "Code", "collapse-all": "Contraer todo", "column": "Columna", "column-entity": "Columna {{entity}}", @@ -877,6 +878,7 @@ "stopped": "Stopped", "storage": "Storage", "storage-plural": "Storages", + "stored-procedure": "Stored Procedure", "sub-team-plural": "Sub Equipos", "submit": "Enviar", "success": "Éxito", 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 7b4ce3db0d6d..6aec2161d7a7 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 @@ -134,6 +134,7 @@ "closed-task-plural": "Tâches Clôturées", "closed-this-task-lowercase": "fermer cette tâche", "cloud-config-source": "Source de Config Cloud", + "code": "Code", "collapse-all": "Tout Réduire", "column": "Colonne", "column-entity": "{{entity}} Colonnes", @@ -877,6 +878,7 @@ "stopped": "Arrêté", "storage": "Stockage", "storage-plural": "Stockages", + "stored-procedure": "Stored Procedure", "sub-team-plural": "Sous Equipes", "submit": "Envoyer", "success": "Succès", 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 a59e1f7e71c8..7493190da311 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 @@ -134,6 +134,7 @@ "closed-task-plural": "終了したタスク", "closed-this-task-lowercase": "このタスクを終了する", "cloud-config-source": "Cloud Config Source", + "code": "Code", "collapse-all": "全て折り畳む", "column": "カラム", "column-entity": "カラム {{entity}}", @@ -877,6 +878,7 @@ "stopped": "Stopped", "storage": "Storage", "storage-plural": "Storages", + "stored-procedure": "Stored Procedure", "sub-team-plural": "サブチーム", "submit": "Submit", "success": "成功", 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 39f66ca9b193..fce5061f049a 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 @@ -134,6 +134,7 @@ "closed-task-plural": "Tarefas fechadas", "closed-this-task-lowercase": "esta tarefa foi fechada", "cloud-config-source": "Origem de configurações de Cloud", + "code": "Code", "collapse-all": "Recolher todas", "column": "Coluna", "column-entity": "Coluna {{entity}}", @@ -877,6 +878,7 @@ "stopped": "Parado", "storage": "Storage", "storage-plural": "Storages", + "stored-procedure": "Stored Procedure", "sub-team-plural": "Sub-equipes", "submit": "Enviar", "success": "Sucesso", 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 e65392f0587d..1bd134d1e8b5 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 @@ -134,6 +134,7 @@ "closed-task-plural": "Закрытые задачи", "closed-this-task-lowercase": "закрыть задачу", "cloud-config-source": "Источник облачной конфигурации", + "code": "Code", "collapse-all": "Свернуть все", "column": "Столбец", "column-entity": "Столбец {{entity}}", @@ -877,6 +878,7 @@ "stopped": "Остановлено", "storage": "Хранилище", "storage-plural": "Хранилища", + "stored-procedure": "Stored Procedure", "sub-team-plural": "Подгруппы", "submit": "Подтвердить", "success": "Успешно", 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 c9b4c69afcc9..8595875eb0dc 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 @@ -134,6 +134,7 @@ "closed-task-plural": "已关闭任务", "closed-this-task-lowercase": "关闭此任务", "cloud-config-source": "云配置源", + "code": "Code", "collapse-all": "全部折叠", "column": "列", "column-entity": "列{{entity}}", @@ -877,6 +878,7 @@ "stopped": "已停止", "storage": "存储", "storage-plural": "存储", + "stored-procedure": "Stored Procedure", "sub-team-plural": "子团队", "submit": "提交", "success": "成功", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx index f92cf16fd948..812ac60c656e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx @@ -139,6 +139,8 @@ const CustomEntityDetailV1 = () => { case ENTITY_PATH.containers: return PAGE_HEADERS.CONTAINER_CUSTOM_ATTRIBUTES; + case ENTITY_PATH.storedprocedure: + return PAGE_HEADERS.STORED_PROCEDURE_CUSTOM_ATTRIBUTES; default: return PAGE_HEADERS.TABLES_CUSTOM_ATTRIBUTES; } 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 a4bb371b78a6..05dd76c12219 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 @@ -40,6 +40,7 @@ import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel'; import { isEmpty, isString, isUndefined, toString } from 'lodash'; import { observer } from 'mobx-react'; import { EntityTags, PagingResponse } from 'Models'; +import StoredProcedureTab from 'pages/StoredProcedure/StoredProcedureTab'; import React, { FunctionComponent, useCallback, @@ -50,12 +51,14 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; +import { ListDataModelParams } from 'rest/dashboardAPI'; import { getDatabaseSchemaDetailsByFQN, patchDatabaseSchemaDetails, restoreDatabaseSchema, } from 'rest/databaseAPI'; import { getFeedCount, postThread } from 'rest/feedsAPI'; +import { getStoredProceduresList } from 'rest/storedProceduresAPI'; import { getTableList, TableListParams } from 'rest/tableAPI'; import { getEntityMissingError } from 'utils/CommonUtils'; import { getDatabaseSchemaVersionPath } from 'utils/RouterUtils'; @@ -64,6 +67,7 @@ import { default as appState } from '../../AppState'; import { getDatabaseSchemaDetailsPath, INITIAL_PAGING_VALUE, + pagingObject, } from '../../constants/constants'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; import { CreateThread } from '../../generated/api/feed/createThread'; @@ -73,6 +77,7 @@ import { getEntityFeedLink, getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; +import { StoredProcedureData } from './DatabaseSchemaPage.interface'; import SchemaTablesTab from './SchemaTablesTab'; const DatabaseSchemaPage: FunctionComponent = () => { @@ -109,11 +114,26 @@ const DatabaseSchemaPage: FunctionComponent = () => { const [currentTablesPage, setCurrentTablesPage] = useState(INITIAL_PAGING_VALUE); + const [storedProcedure, setStoredProcedure] = useState({ + data: [], + isLoading: false, + deleted: false, + paging: pagingObject, + currentPage: INITIAL_PAGING_VALUE, + }); + const handleShowDeletedTables = (value: boolean) => { setShowDeletedTables(value); setCurrentTablesPage(INITIAL_PAGING_VALUE); }; + const handleShowDeletedStoredProcedure = (value: boolean) => { + setStoredProcedure((prev) => ({ + ...prev, + currentPage: INITIAL_PAGING_VALUE, + deleted: value, + })); + }; const { version: currentVersion } = useMemo( () => databaseSchema, [databaseSchema] @@ -197,6 +217,28 @@ const DatabaseSchemaPage: FunctionComponent = () => { } }, [databaseSchemaFQN]); + const fetchStoreProcedureDetails = useCallback( + async (params?: ListDataModelParams) => { + try { + setStoredProcedure((prev) => ({ ...prev, isLoading: true })); + const { data, paging } = await getStoredProceduresList({ + service: getDecodedFqn(databaseSchemaFQN), + fields: 'owner,tags,followers', + include: storedProcedure.deleted + ? Include.Deleted + : Include.NonDeleted, + ...params, + }); + setStoredProcedure((prev) => ({ ...prev, data, paging })); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setStoredProcedure((prev) => ({ ...prev, isLoading: false })); + } + }, + [databaseSchemaFQN, storedProcedure.deleted] + ); + const getSchemaTables = useCallback( async (params?: TableListParams) => { setTableDataLoading(true); @@ -460,6 +502,25 @@ const DatabaseSchemaPage: FunctionComponent = () => { [] ); + const storedProcedurePagingHandler = useCallback( + async (cursorType: string | number, activePage?: number) => { + const pagingString = { + [cursorType]: + storedProcedure.paging[ + cursorType as keyof typeof storedProcedure.paging + ], + }; + + await fetchStoreProcedureDetails(pagingString); + + setStoredProcedure((prev) => ({ + ...prev, + currentPage: activePage ?? INITIAL_PAGING_VALUE, + })); + }, + [storedProcedure.paging] + ); + useEffect(() => { fetchDatabaseSchemaPermission(); }, [databaseSchemaFQN]); @@ -467,6 +528,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { useEffect(() => { if (viewDatabaseSchemaPermission) { fetchDatabaseSchemaDetails(); + fetchStoreProcedureDetails({ limit: 0 }); getEntityFeedCount(); } }, [viewDatabaseSchemaPermission, databaseSchemaFQN]); @@ -580,6 +642,25 @@ const DatabaseSchemaPage: FunctionComponent = () => { ), }, + { + label: ( + + ), + key: EntityTabs.STORED_PROCEDURE, + children: ( + + ), + }, ]; if (isPermissionsLoading) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.interface.ts new file mode 100644 index 000000000000..c230eca0a407 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.interface.ts @@ -0,0 +1,23 @@ +/* + * 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 { Paging } from 'generated/type/paging'; +import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage'; + +export interface StoredProcedureData { + isLoading: boolean; + deleted: boolean; + data: ServicePageData[]; + paging: Paging; + currentPage: number; +} 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 7293a34cacf4..56d50932c712 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 @@ -54,6 +54,7 @@ import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { Database } from 'generated/entity/data/database'; import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Pipeline } from 'generated/entity/data/pipeline'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { Topic } from 'generated/entity/data/topic'; import { DashboardConnection } from 'generated/entity/services/dashboardService'; import { DatabaseService } from 'generated/entity/services/databaseService'; @@ -123,7 +124,8 @@ export type ServicePageData = | Mlmodel | Pipeline | Container - | DashboardDataModel; + | DashboardDataModel + | StoredProcedure; const ServiceDetailsPage: FunctionComponent = () => { const { t } = useTranslation(); 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 new file mode 100644 index 000000000000..e7bed79f15de --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -0,0 +1,688 @@ +/* + * 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 { Card, Col, Row, Space, Tabs } from 'antd'; +import { AxiosError } from 'axios'; +import { useActivityFeedProvider } 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 { CustomPropertyProps } from 'components/common/CustomPropertyTable/CustomPropertyTable.interface'; +import DescriptionV1 from 'components/common/description/DescriptionV1'; +import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder'; +import PageLayoutV1 from 'components/containers/PageLayoutV1'; +import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; +import EntityLineageComponent from 'components/Entity/EntityLineage/EntityLineage.component'; +import Loader from 'components/Loader/Loader'; +import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; +import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider'; +import { + OperationPermission, + ResourceEntity, +} from 'components/PermissionProvider/PermissionProvider.interface'; +import { withActivityFeed } from 'components/router/withActivityFeed'; +import SchemaEditor from 'components/schema-editor/SchemaEditor'; +import { SourceType } from 'components/searched-data/SearchedData.interface'; +import TabsLabel from 'components/TabsLabel/TabsLabel.component'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; +import { DisplayType } from 'components/Tag/TagsViewer/TagsViewer.interface'; +import { + getStoredProcedureDetailPath, + getVersionPath, +} from 'constants/constants'; +import { CSMode } from 'enums/codemirror.enum'; +import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum'; +import { EntityTabs, EntityType } from 'enums/entity.enum'; +import { compare } from 'fast-json-patch'; +import { CreateThread, ThreadType } from 'generated/api/feed/createThread'; +import { + StoredProcedure, + StoredProcedureCodeObject, +} from 'generated/entity/data/storedProcedure'; +import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel'; +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 { postThread } from 'rest/feedsAPI'; +import { + addStoredProceduresFollower, + getStoredProceduresDetailsByFQN, + patchStoredProceduresDetails, + removeStoredProceduresFollower, + restoreStoredProcedures, +} from 'rest/storedProceduresAPI'; +import { + getCurrentUserId, + getFeedCounts, + sortTagsCaseInsensitive, +} from 'utils/CommonUtils'; +import { getEntityName } from 'utils/EntityUtils'; +import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; +import { STORED_PROCEDURE_DEFAULT_FIELDS } from 'utils/StoredProceduresUtils'; +import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils'; +import { showErrorToast, showSuccessToast } from 'utils/ToastUtils'; + +const StoredProcedurePage = () => { + const { t } = useTranslation(); + const USER_ID = getCurrentUserId(); + const history = useHistory(); + const { storedProcedureFQN, tab: activeTab = EntityTabs.CODE } = + useParams<{ storedProcedureFQN: string; tab: string }>(); + + const { getEntityPermissionByFqn } = usePermissionProvider(); + const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); + + const [isLoading, setIsLoading] = useState(true); + const [storedProcedure, setStoredProcedure] = useState(); + const [storedProcedurePermissions, setStoredProcedurePermissions] = + useState(DEFAULT_ENTITY_PERMISSION); + const [isEdit, setIsEdit] = useState(false); + + const [feedCount, setFeedCount] = useState(0); + const [threadLink, setThreadLink] = useState(''); + + const [threadType, setThreadType] = useState( + ThreadType.Conversation + ); + + const { + id: storedProcedureId = '', + followers, + owner, + tags, + tier, + version, + code, + description, + deleted, + entityName, + entityFQN, + } = useMemo(() => { + return { + ...storedProcedure, + tier: getTierTags(storedProcedure?.tags ?? []), + tags: getTagsWithoutTier(storedProcedure?.tags ?? []), + entityName: getEntityName(storedProcedure), + entityFQN: storedProcedure?.fullyQualifiedName ?? '', + code: + (storedProcedure?.storedProcedureCode as StoredProcedureCodeObject) + ?.code ?? '', + }; + }, [storedProcedure]); + + const { isFollowing } = useMemo(() => { + return { + isFollowing: followers?.some(({ id }) => id === USER_ID), + }; + }, [followers, USER_ID]); + + const fetchResourcePermission = useCallback(async () => { + try { + const permission = await getEntityPermissionByFqn( + ResourceEntity.STORED_PROCEDURE, + storedProcedureFQN + ); + + setStoredProcedurePermissions(permission); + } catch (error) { + showErrorToast( + t('server.fetch-entity-permissions-error', { + entity: t('label.resource-permission-lowercase'), + }) + ); + } finally { + setIsLoading(false); + } + }, [getEntityPermissionByFqn]); + + const getEntityFeedCount = () => { + getFeedCounts( + EntityType.STORED_PROCEDURE, + storedProcedureFQN, + setFeedCount + ); + }; + + const fetchStoredProcedureDetails = async () => { + setIsLoading(true); + try { + const response = await getStoredProceduresDetailsByFQN( + storedProcedureFQN, + STORED_PROCEDURE_DEFAULT_FIELDS + ); + + setStoredProcedure(response); + } catch (error) { + // Error here + } finally { + setIsLoading(false); + } + }; + + const versionHandler = useCallback(() => { + version && + history.push( + getVersionPath( + EntityType.STORED_PROCEDURE, + storedProcedureFQN, + version + '' + ) + ); + }, [storedProcedureFQN, version]); + + const saveUpdatedStoredProceduresData = useCallback( + (updatedData: StoredProcedure) => { + if (!storedProcedure) { + return updatedData; + } + const jsonPatch = compare(storedProcedure ?? '', updatedData); + + return patchStoredProceduresDetails(storedProcedureId ?? '', jsonPatch); + }, + [storedProcedure] + ); + + const handleStoreProcedureUpdate = async ( + updatedData: StoredProcedure, + key: keyof StoredProcedure + ) => { + try { + const res = await saveUpdatedStoredProceduresData(updatedData); + + setStoredProcedure((previous) => { + if (!previous) { + return; + } + if (key === 'tags') { + return { + ...previous, + version: res.version, + [key]: sortTagsCaseInsensitive(res.tags ?? []), + }; + } + + return { + ...previous, + version: res.version, + [key]: res[key], + }; + }); + + getEntityFeedCount(); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + + const followEntity = useCallback(async () => { + try { + const res = await addStoredProceduresFollower(storedProcedureId, USER_ID); + const { newValue } = res.changeDescription.fieldsAdded[0]; + const newFollowers = [...(followers ?? []), ...newValue]; + setStoredProcedure((prev) => { + if (!prev) { + return prev; + } + + return { ...prev, followers: newFollowers }; + }); + getEntityFeedCount(); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-follow-error', { + entity: getEntityName(storedProcedure), + }) + ); + } + }, [USER_ID, followers, storedProcedure, storedProcedureId]); + + const unFollowEntity = useCallback(async () => { + try { + const res = await removeStoredProceduresFollower( + storedProcedureId, + USER_ID + ); + const { oldValue } = res.changeDescription.fieldsDeleted[0]; + setStoredProcedure((pre) => { + if (!pre) { + return pre; + } + + return { + ...pre, + followers: pre.followers?.filter( + (follower) => follower.id !== oldValue[0].id + ), + }; + }); + getEntityFeedCount(); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.entity-unfollow-error', { + entity: getEntityName(storedProcedure), + }) + ); + } + }, [USER_ID, storedProcedureId]); + + const handleDisplayNameUpdate = async (data: EntityName) => { + if (!storedProcedure) { + return; + } + const updatedData = { ...storedProcedure, displayName: data.displayName }; + await handleStoreProcedureUpdate(updatedData, 'displayName'); + }; + + const handleFollow = useCallback(async () => { + isFollowing ? await unFollowEntity() : await followEntity(); + }, [isFollowing]); + + const handleUpdateOwner = useCallback( + async (newOwner?: StoredProcedure['owner']) => { + if (!storedProcedure) { + return; + } + const updatedEntityDetails = { + ...storedProcedure, + owner: newOwner + ? { + ...owner, + ...newOwner, + } + : undefined, + }; + await handleStoreProcedureUpdate(updatedEntityDetails, 'owner'); + }, + [owner, storedProcedure] + ); + + const handleToggleDelete = () => { + setStoredProcedure((prev) => { + if (!prev) { + return prev; + } + + return { ...prev, deleted: !prev?.deleted }; + }); + }; + + const handleRestoreStoredProcedures = async () => { + try { + await restoreStoredProcedures(storedProcedureId); + showSuccessToast( + t('message.restore-entities-success', { + entity: t('label.stored-procedure'), + }), + 2000 + ); + handleToggleDelete(); + } catch (error) { + showErrorToast( + error as AxiosError, + t('message.restore-entities-error', { + entity: t('label.stored-procedure'), + }) + ); + } + }; + + const onTierUpdate = useCallback( + async (newTier?: string) => { + if (storedProcedure) { + const tierTag: StoredProcedure['tags'] = newTier + ? [ + ...getTagsWithoutTier(tags ?? []), + { + tagFQN: newTier, + labelType: LabelType.Manual, + state: State.Confirmed, + }, + ] + : getTagsWithoutTier(tags ?? []); + const updatedDetails = { + ...storedProcedure, + tags: tierTag, + }; + + await handleStoreProcedureUpdate(updatedDetails, 'tags'); + } + }, + [storedProcedure, tags] + ); + + const afterDeleteAction = useCallback( + (isSoftDelete?: boolean) => + isSoftDelete ? handleToggleDelete() : history.push('/'), + [] + ); + + const handleTabChange = (activeKey: EntityTabs) => { + if (activeKey !== activeTab) { + history.push(getStoredProcedureDetailPath(storedProcedureFQN, activeKey)); + } + }; + + 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 onThreadLinkSelect = (link: string, threadType?: ThreadType) => { + setThreadLink(link); + if (threadType) { + setThreadType(threadType); + } + }; + + const handleTagSelection = async (selectedTags: EntityTags[]) => { + const updatedTags: TagLabel[] | undefined = selectedTags?.map((tag) => ({ + source: tag.source, + tagFQN: tag.tagFQN, + labelType: LabelType.Manual, + state: State.Confirmed, + })); + + if (updatedTags && storedProcedure) { + const updatedTags = [...(tier ? [tier] : []), ...selectedTags]; + const updatedData = { ...storedProcedure, tags: updatedTags }; + await handleStoreProcedureUpdate(updatedData, 'tags'); + } + }; + + const createThread = async (data: CreateThread) => { + try { + await postThread(data); + getEntityFeedCount(); + } catch (error) { + showErrorToast( + error as AxiosError, + t('server.create-entity-error', { + entity: t('label.conversation'), + }) + ); + } + }; + + const onThreadPanelClose = () => { + setThreadLink(''); + }; + + const onExtensionUpdate = async (updatedData: StoredProcedure) => { + await handleStoreProcedureUpdate(updatedData, 'extension'); + }; + + const tabs = useMemo( + () => [ + { + label: ( + + ), + key: EntityTabs.CODE, + children: ( + + +
+ + + + + +
+ + + + + + + + +
+ ), + }, + { + label: ( + + ), + key: EntityTabs.ACTIVITY_FEED, + children: ( + + ), + }, + { + label: , + key: EntityTabs.LINEAGE, + children: ( + + ), + }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: ( + + ), + }, + ], + [ + code, + tags, + isEdit, + deleted, + feedCount, + activeTab, + entityFQN, + entityName, + description, + storedProcedure, + storedProcedureFQN, + storedProcedurePermissions, + ] + ); + + useEffect(() => { + if (storedProcedureFQN) { + fetchResourcePermission(); + } + }, [storedProcedureFQN]); + + useEffect(() => { + if ( + storedProcedurePermissions.ViewAll || + storedProcedurePermissions.ViewBasic + ) { + fetchStoredProcedureDetails(); + getEntityFeedCount(); + } + }, [storedProcedureFQN, storedProcedurePermissions]); + + if (isLoading) { + return ; + } + + if ( + !( + storedProcedurePermissions.ViewAll || storedProcedurePermissions.ViewBasic + ) + ) { + return ; + } + + if (!storedProcedure) { + return ; + } + + return ( + + + + + + + {/* Entity Tabs */} + + + handleTabChange(activeKey as EntityTabs) + } + /> + + + {threadLink ? ( + + ) : null} + + + ); +}; + +export default withActivityFeed(StoredProcedurePage); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx new file mode 100644 index 000000000000..92710f4cc3c4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedureTab.tsx @@ -0,0 +1,124 @@ +/* + * 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, Switch, Table, Typography } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder'; +import NextPrevious from 'components/common/next-previous/NextPrevious'; +import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer'; +import Loader from 'components/Loader/Loader'; +import { PAGE_SIZE } from 'constants/constants'; +import { EntityType } from 'enums/entity.enum'; +import { isEmpty } from 'lodash'; +import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage'; +import React, { useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { getEntityName } from 'utils/EntityUtils'; +import { getEncodedFqn } from 'utils/StringsUtils'; +import { getEntityLink } from 'utils/TableUtils'; +import { StoredProcedureTabProps } from './storedProcedure.interface'; + +const StoredProcedureTab = ({ + storedProcedure, + pagingHandler, + fetchStoredProcedure, + onShowDeletedStoreProcedureChange, +}: StoredProcedureTabProps) => { + const { t } = useTranslation(); + const { data, isLoading, deleted, paging, currentPage } = storedProcedure; + + const tableColumn: ColumnsType = useMemo( + () => [ + { + title: t('label.name'), + dataIndex: 'name', + key: 'name', + width: 350, + render: (_, record) => ( + + {getEntityName(record)} + + ), + }, + { + title: t('label.description'), + dataIndex: 'description', + key: 'description', + render: (text: string) => + isEmpty(text) ? ( + + {t('label.no-description')} + + ) : ( + + ), + }, + ], + [] + ); + + useEffect(() => { + fetchStoredProcedure(); + }, [deleted]); + + return ( + + + + + {t('label.deleted')} + {' '} + + + , + }} + locale={{ + emptyText: , + }} + pagination={false} + rowKey="id" + size="small" + /> + + + + {paging && paging.total > PAGE_SIZE && ( + + )} + + + ); +}; + +export default StoredProcedureTab; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/storedProcedure.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/storedProcedure.interface.ts new file mode 100644 index 000000000000..b0758f16b019 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/storedProcedure.interface.ts @@ -0,0 +1,20 @@ +/* + * 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 { StoredProcedureData } from 'pages/DatabaseSchemaPage/DatabaseSchemaPage.interface'; + +export interface StoredProcedureTabProps { + storedProcedure: StoredProcedureData; + fetchStoredProcedure: () => void; + pagingHandler: (cursorValue: string | number, activePage?: number) => void; + onShowDeletedStoreProcedureChange: (value: boolean) => void; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts index c8e7112ed071..90fef6d2ea30 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts @@ -15,6 +15,7 @@ import { Container } from 'generated/entity/data/container'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { Database } from 'generated/entity/data/database'; import { DatabaseSchema } from 'generated/entity/data/databaseSchema'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { Pipeline } from '../../generated/entity/data/pipeline'; @@ -28,6 +29,7 @@ export type EntityData = | Pipeline | Mlmodel | Container + | StoredProcedure | Database | DatabaseSchema | DashboardDataModel; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts new file mode 100644 index 000000000000..53271f5e34b8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts @@ -0,0 +1,153 @@ +/* + * 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 { AxiosResponse } from 'axios'; +import { Operation } from 'fast-json-patch'; +import { StoredProcedure } from 'generated/entity/data/storedProcedure'; +import { EntityHistory } from 'generated/type/entityHistory'; +import { EntityReference } from 'generated/type/entityReference'; +import { Include } from 'generated/type/include'; +import { PagingResponse, RestoreRequestType } from 'Models'; +import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage'; +import { getURLWithQueryFields } from 'utils/APIUtils'; +import { ListDataModelParams } from './dashboardAPI'; +import APIClient from './index'; + +const URL = '/storedProcedures'; + +const configOptionsForPatch = { + headers: { 'Content-type': 'application/json-patch+json' }, +}; + +const configOptions = { + headers: { 'Content-type': 'application/json' }, +}; + +export const getStoredProceduresList = async (params?: ListDataModelParams) => { + const response = await APIClient.get>(URL, { + params, + }); + + return response.data; +}; + +export const getStoredProceduresDetails = async ( + id: string, + arrQueryFields: string | string[] +) => { + const url = getURLWithQueryFields(`${URL}/${id}`, arrQueryFields); + + const response = await APIClient.get(url); + + return response.data; +}; + +export const getStoredProceduresByName = async ( + name: string, + fields: string | string[], + include: Include = Include.NonDeleted +) => { + const response = await APIClient.get( + `${URL}/name/${name}?fields=${fields}`, + { + params: { + include, + }, + } + ); + + return response.data; +}; + +export const getStoredProceduresDetailsByFQN = async ( + storedProceduresName: string, + arrQueryFields?: string | string[], + include = Include.All +) => { + const url = `${getURLWithQueryFields( + `${URL}/name/${storedProceduresName}`, + arrQueryFields, + `include=${include}` + )}`; + + const response = await APIClient.get(url); + + return response.data; +}; + +export const patchStoredProceduresDetails = async ( + id: string, + data: Operation[] +) => { + const response = await APIClient.patch< + Operation[], + AxiosResponse + >(`${URL}/${id}`, data, configOptionsForPatch); + + return response.data; +}; + +export const addStoredProceduresFollower = async ( + id: string, + userId: string +) => { + const response = await APIClient.put< + string, + AxiosResponse<{ + changeDescription: { fieldsAdded: { newValue: EntityReference[] }[] }; + }> + >(`${URL}/${id}/followers`, userId, configOptions); + + return response.data; +}; + +export const removeStoredProceduresFollower = async ( + id: string, + userId: string +) => { + const response = await APIClient.delete< + string, + AxiosResponse<{ + changeDescription: { fieldsDeleted: { oldValue: EntityReference[] }[] }; + }> + >(`${URL}/${id}/followers/${userId}`, configOptions); + + return response.data; +}; + +export const getStoredProceduresVersionsList = async (id: string) => { + const url = `${URL}/${id}/versions`; + + const response = await APIClient.get(url); + + return response.data; +}; + +export const getStoredProceduresVersion = async ( + id: string, + version: string +) => { + const url = `${URL}/${id}/versions/${version}`; + + const response = await APIClient.get(url); + + return response.data; +}; + +export const restoreStoredProcedures = async (id: string) => { + const response = await APIClient.put< + RestoreRequestType, + AxiosResponse + >(`${URL}/restore`, { id }); + + return response.data; +}; 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 917a661e2309..7438f4deffa2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -55,6 +55,7 @@ import { getDataModelDetailsPath, getMlModelDetailsPath, getPipelineDetailsPath, + getStoredProcedureDetailPath, getTableTabPath, getTeamAndUserDetailsPath, getTopicDetailsPath, @@ -837,6 +838,11 @@ export const getEntityDetailLink = ( case EntityType.USER_NAME: path = getUserPath(fqn, tab, subTab); + break; + + case EntityType.STORED_PROCEDURE: + path = getStoredProcedureDetailPath(fqn, tab, subTab); + break; } 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 ee338fecf2e7..f1699545473e 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 @@ -20,7 +20,10 @@ import { DataAssetHeaderInfo, DataAssetsHeaderProps, } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; -import { getDashboardDetailsPath } from 'constants/constants'; +import { + getDashboardDetailsPath, + NO_DATA_PLACEHOLDER, +} from 'constants/constants'; import { EntityType } from 'enums/entity.enum'; import { Container } from 'generated/entity/data/container'; import { Dashboard } from 'generated/entity/data/dashboard'; @@ -29,6 +32,10 @@ import { Database } from 'generated/entity/data/database'; import { DatabaseSchema } from 'generated/entity/data/databaseSchema'; import { Mlmodel } from 'generated/entity/data/mlmodel'; import { Pipeline } from 'generated/entity/data/pipeline'; +import { + StoredProcedure, + StoredProcedureCodeObject, +} from 'generated/entity/data/storedProcedure'; import { Table } from 'generated/entity/data/table'; import { Topic } from 'generated/entity/data/topic'; import { DashboardService } from 'generated/entity/services/dashboardService'; @@ -39,7 +46,7 @@ import { MlmodelService } from 'generated/entity/services/mlmodelService'; import { PipelineService } from 'generated/entity/services/pipelineService'; import { StorageService } from 'generated/entity/services/storageService'; import { t } from 'i18next'; -import { isUndefined } from 'lodash'; +import { isObject, isUndefined } from 'lodash'; import React from 'react'; import { getBreadcrumbForContainer, @@ -324,6 +331,28 @@ export const getDataAssetsHeaderInfo = ( break; + case EntityType.STORED_PROCEDURE: + const storedProcedureDetails = dataAsset as StoredProcedure; + + returnData.extraInfo = ( + <> + {isObject(storedProcedureDetails.storedProcedureCode) && ( + + )} + + ); + + returnData.breadcrumbs = getBreadcrumbForTable(dataAsset as Table); + + break; + case EntityType.TABLE: default: const tableDetails = dataAsset as Table; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx index 28b9cd075523..56a7fe682967 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -1327,6 +1327,8 @@ export const getParamByEntityType = (entityType: EntityType): string => { return 'databaseSchemaFQN'; case EntityType.DASHBOARD_DATA_MODEL: return 'dashboardDataModelFQN'; + case EntityType.STORED_PROCEDURE: + return 'storedProcedureFQN'; default: return 'entityFQN'; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index 49a938f242ad..e6d8bd98ace4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -38,10 +38,22 @@ import { Database } from 'generated/entity/data/database'; import { DatabaseSchema } from 'generated/entity/data/databaseSchema'; import { GlossaryTerm } from 'generated/entity/data/glossaryTerm'; import { Mlmodel } from 'generated/entity/data/mlmodel'; +import { + StoredProcedure, + StoredProcedureCodeObject, +} from 'generated/entity/data/storedProcedure'; import { Topic } from 'generated/entity/data/topic'; import i18next from 'i18next'; import { EntityFieldThreadCount } from 'interface/feed.interface'; -import { get, isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash'; +import { + get, + isEmpty, + isNil, + isObject, + isUndefined, + lowerCase, + startCase, +} from 'lodash'; import { Bucket, EntityDetailUnion } from 'Models'; import React, { Fragment } from 'react'; import { Link } from 'react-router-dom'; @@ -129,6 +141,7 @@ export const getEntityTags = ( case EntityType.DASHBOARD: case EntityType.TOPIC: case EntityType.MLMODEL: + case EntityType.STORED_PROCEDURE: case EntityType.DASHBOARD_DATA_MODEL: { return entityDetail.tags || []; } @@ -558,6 +571,88 @@ export const getEntityOverview = ( return overview; } + case ExplorePageTabs.STORED_PROCEDURE: { + const { fullyQualifiedName, owner, tags, storedProcedureCode } = + entityDetail as StoredProcedure; + const [service, database, schema] = getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ).split(FQN_SEPARATOR_CHAR); + + const tier = getTierFromTableTags(tags || []); + + const overview = [ + { + name: i18next.t('label.owner'), + value: + getOwnerNameWithProfilePic(owner) || + i18next.t('label.no-entity', { + entity: i18next.t('label.owner'), + }), + url: getOwnerValue(owner as EntityReference), + isLink: owner?.name ? true : false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18next.t('label.service'), + value: service || NO_DATA, + url: getServiceDetailsPath( + service, + ServiceCategory.DATABASE_SERVICES + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18next.t('label.database'), + value: database || NO_DATA, + url: getDatabaseDetailsPath( + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18next.t('label.schema'), + value: schema || NO_DATA, + url: getDatabaseSchemaDetailsPath( + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18next.t('label.tier'), + value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ...(isObject(storedProcedureCode) + ? [ + { + name: i18next.t('label.language'), + value: + (storedProcedureCode as StoredProcedureCodeObject).language ?? + NO_DATA, + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ] + : []), + ]; + + return overview; + } + default: return []; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx index e1c61361bbcd..3ef66686476b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx @@ -42,6 +42,7 @@ import { ReactComponent as TopicIcon } from '../../src/assets/svg/topic-grey.svg import { ReactComponent as UsersIcon } from '../../src/assets/svg/user.svg'; import { ReactComponent as CustomLogoIcon } from '../assets/svg/ic-custom-logo.svg'; import { ReactComponent as StorageIcon } from '../assets/svg/ic-storage.svg'; +import { ReactComponent as StoredProcedureIcon } from '../assets/svg/ic-stored-procedure.svg'; import { userPermissions } from '../utils/PermissionsUtils'; export interface MenuListItem { @@ -252,6 +253,12 @@ export const getGlobalSettingsMenuWithPermission = ( key: 'customAttributes.containers', icon: , }, + { + label: i18next.t('label.stored-procedure'), + isProtected: Boolean(isAdminUser), + key: 'customAttributes.storedProcedure', + icon: , + }, ], }, { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx new file mode 100644 index 000000000000..5e79cac2022c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -0,0 +1,15 @@ +/* + * 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 { TabSpecificField } from 'enums/entity.enum'; + +export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNER}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.EXTENSION}`; 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 92f99610e433..7eea729798c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -53,6 +53,7 @@ import { getMlModelPath, getPipelineDetailsPath, getServiceDetailsPath, + getStoredProcedureDetailsPath, getTableDetailsPath, getTableTabPath, getTagsDetailsPath, @@ -249,6 +250,9 @@ export const getEntityLink = ( case EntityType.DASHBOARD_DATA_MODEL: return getDataModelDetailsPath(getDecodedFqn(fullyQualifiedName)); + case EntityType.STORED_PROCEDURE: + return getStoredProcedureDetailsPath(getDecodedFqn(fullyQualifiedName)); + case EntityType.TEST_CASE: return `${getTableTabPath( getTableFQNFromColumnFQN(fullyQualifiedName), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts index 35dcdf30d226..497f3f4f3af3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts @@ -40,6 +40,7 @@ import { getUserSuggestions } from 'rest/miscAPI'; import { getMlModelByFQN } from 'rest/mlModelAPI'; import { getPipelineByFqn } from 'rest/pipelineAPI'; import { getContainerByFQN } from 'rest/storageAPI'; +import { getStoredProceduresDetailsByFQN } from 'rest/storedProceduresAPI'; import { getTableDetailsByFQN } from 'rest/tableAPI'; import { getTopicByFqn } from 'rest/topicsAPI'; import { @@ -74,6 +75,7 @@ import { getEntityFQN, getEntityType } from './FeedUtils'; import { defaultFields as MlModelFields } from './MlModelDetailsUtils'; import { defaultFields as PipelineFields } from './PipelineDetailsUtils'; import { serviceTypeLogo } from './ServiceUtils'; +import { STORED_PROCEDURE_DEFAULT_FIELDS } from './StoredProceduresUtils'; import { getEntityLink } from './TableUtils'; import { showErrorToast } from './ToastUtils'; @@ -269,6 +271,7 @@ export const TASK_ENTITIES = [ EntityType.CONTAINER, EntityType.DATABASE_SCHEMA, EntityType.DASHBOARD_DATA_MODEL, + EntityType.STORED_PROCEDURE, ]; export const getBreadCrumbList = ( @@ -353,6 +356,15 @@ export const getBreadCrumbList = ( return [service(ServiceCategory.STORAGE_SERVICES), activeEntity]; } + case EntityType.STORED_PROCEDURE: { + return [ + service(ServiceCategory.DATABASE_SERVICES), + database, + databaseSchema, + activeEntity, + ]; + } + default: return []; } @@ -447,6 +459,18 @@ export const fetchEntityDetail = ( break; + case EntityType.STORED_PROCEDURE: + getStoredProceduresDetailsByFQN( + entityFQN, + STORED_PROCEDURE_DEFAULT_FIELDS + ) + .then((res) => { + setEntityData(res); + }) + .catch((err: AxiosError) => showErrorToast(err)); + + break; + default: break; }