feat(ui): supported stored procedure in database (#13031)

* feat: supported stored procedure in database

* minor fixes

* fix sonar

* optimize stored procedure code and supported deleted flag for it

* feat(ui): supported code and activity tab in stored procedure (#13054)

* supported code and activity tab in stored procedure

* added support for task functionality

* feat(ui): supported lineage tab in stored procedure (#13060)

* supported code and activity tab in stored procedure

* added support for task functionality

* feat: supported lineage tab in stored procedure

* feat(ui): supported custom property tab in stored procedure and setting page (#13063)

* feat: supported custom property tab in stored procedure and setting page

* added extension field

* chanegs as per comments

* chanegs as per comments
This commit is contained in:
Ashish Gupta 2023-09-04 17:28:29 +05:30 committed by GitHub
parent db592a157e
commit fe7f2828b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1578 additions and 6 deletions

View File

@ -0,0 +1,19 @@
<svg viewBox="0 0 58 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="7" cy="6" r="4.5" stroke="currentColor" stroke-width="3"/>
<path d="M11 6H41.5" stroke="currentColor" stroke-width="3"/>
<path d="M37 1.5L40.7929 5.29289C41.1834 5.68342 41.1834 6.31658 40.7929 6.70711L37 10.5" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<circle cx="51" cy="38" r="4.5" stroke="currentColor" stroke-width="3"/>
<path d="M47 38H36" stroke="currentColor" stroke-width="3"/>
<path d="M40.5 42L36.7071 38.2071C36.3166 37.8166 36.3166 37.1834 36.7071 36.7929L40.5 33" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<circle cx="51" cy="6" r="4.5" stroke="currentColor" stroke-width="3"/>
<path d="M51.25 28L51.25 9" stroke="currentColor" stroke-width="3"/>
<path d="M56 25.75L52.2071 29.5429C51.8166 29.9334 51.1834 29.9334 50.7929 29.5429L47 25.75" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M2 38H32.5" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M21 27V33" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M21 44V50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M27 27V33" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M27 44V50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M33 27V33" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M33 44V50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 23H36C37.1046 23 38 23.8954 38 25V28H41V25C41 22.2386 38.7614 20 36 20H5C2.23858 20 0 22.2386 0 25V51C0 53.7614 2.23858 56 5 56H36C38.7614 56 41 53.7614 41 51V49H38V51C38 52.1046 37.1046 53 36 53H5C3.89543 53 3 52.1046 3 51V25C3 23.8954 3.89543 23 5 23Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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;

View File

@ -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 (
<StoredProcedureSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as StoredProcedure}
isLoading={isLoading}
tags={tags}
/>
);
default:
return null;
}

View File

@ -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 (
<SummaryPanelSkeleton loading={isLoading}>
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<Row gutter={[0, 4]}>
{entityInfo.map((info) => {
const isOwner = info.name === t('label.owner');
return info.visible?.includes(componentType) ? (
<Col key={info.name} span={24}>
<Row
className={classNames('', {
'p-b-md': isOwner,
})}
gutter={[16, 32]}>
{!isOwner ? (
<Col data-testid={`${info.name}-label`} span={8}>
<Typography.Text className="text-grey-muted">
{info.name}
</Typography.Text>
</Col>
) : null}
<Col data-testid={`${info.name}-value`} span={16}>
{info.isLink ? (
<Link
component={Typography.Link}
target="_self"
to={{ pathname: info.url }}>
{info.value}
</Link>
) : (
<Typography.Text
className={classNames('text-grey-muted', {
'text-grey-body': !isOwner,
})}>
{info.value}
</Typography.Text>
)}
</Col>
</Row>
</Col>
) : null;
})}
</Row>
</Col>
</Row>
<Divider className="m-y-xs" />
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ?? []}
/>
<Divider className="m-y-xs" />
{isObject(entityDetails.storedProcedureCode) && (
<Row className="m-md" gutter={[0, 8]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="column-header">
{t('label.code')}
</Typography.Text>
</Col>
<Col span={24}>
<SchemaEditor
editorClass="custom-code-mirror-theme custom-query-editor"
mode={{ name: CSMode.SQL }}
options={{
styleActiveLine: false,
readOnly: 'nocursor',
}}
value={
(
entityDetails.storedProcedureCode as StoredProcedureCodeObject
).code ?? ''
}
/>
</Col>
</Row>
)}
</>
</SummaryPanelSkeleton>
);
};
export default StoredProcedureSummary;

View File

@ -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;
}

View File

@ -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

View File

@ -72,6 +72,7 @@ export enum ResourceEntity {
QUERY = 'query',
DASHBOARD_DATA_MODEL = 'dashboardDataModel',
EVENT_SUBSCRIPTION = 'eventsubscription',
STORED_PROCEDURE = 'storedProcedure',
}
export interface PermissionContextType {

View File

@ -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;

View File

@ -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}
/>
<Route
exact
component={StoredProcedureDetailsPage}
path={ROUTES.STORED_PROCEDURE_DETAILS}
/>
<Route
exact
component={StoredProcedureDetailsPage}
path={ROUTES.STORED_PROCEDURE_DETAILS_WITH_TAB}
/>
<Route
exact
component={StoredProcedureDetailsPage}
path={ROUTES.STORED_PROCEDURE_DETAILS_WITH_SUB_TAB}
/>
<Route
exact
component={PipelineDetailsPage}

View File

@ -118,6 +118,12 @@ export const PAGE_HEADERS = {
entity: i18n.t('label.container-plural'),
}),
},
STORED_PROCEDURE_CUSTOM_ATTRIBUTES: {
header: i18n.t('label.stored-procedure'),
subHeader: i18n.t('message.define-custom-property-for-entity', {
entity: i18n.t('label.label.stored-procedure'),
}),
},
BOTS: {
header: i18n.t('label.bot-plural'),
subHeader: i18n.t('message.page-sub-header-for-bots'),

View File

@ -139,6 +139,7 @@ export const LOG_ENTITY_NAME = ':logEntityName';
export const KPI_NAME = ':kpiName';
export const PLACEHOLDER_ACTION = ':action';
export const PLACEHOLDER_ROUTE_DATA_MODEL_FQN = ':dashboardDataModelFQN';
export const PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN = ':storedProcedureFQN';
export const pagingObject = { after: '', before: '', total: 0 };
@ -264,6 +265,10 @@ export const ROUTES = {
CONTAINER_DETAILS_WITH_TAB: `/container/${PLACEHOLDER_ROUTE_ENTITY_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
CONTAINER_DETAILS_WITH_SUB_TAB: `/container/${PLACEHOLDER_ROUTE_ENTITY_FQN}/${PLACEHOLDER_ROUTE_TAB}/${PLACEHOLDER_ROUTE_SUB_TAB}`,
STORED_PROCEDURE_DETAILS: `/storedProcedure/${PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN}`,
STORED_PROCEDURE_DETAILS_WITH_TAB: `/storedProcedure/${PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
STORED_PROCEDURE_DETAILS_WITH_SUB_TAB: `/storedProcedure/${PLACEHOLDER_ROUTE_STORED_PROCEDURE_FQN}/${PLACEHOLDER_ROUTE_TAB}/${PLACEHOLDER_ROUTE_SUB_TAB}`,
USER_LIST: '/user-list',
CREATE_USER: '/create-user',
CREATE_USER_WITH_BOT: `/create-user/${PLACEHOLDER_USER_BOT}`,
@ -352,6 +357,19 @@ export const getTableDetailsPath = (tableFQN: string, columnName?: string) => {
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<string, string> = {
containers: 'container',
tags: 'tag',
glossaries: 'glossary',
storedprocedure: 'storedProcedure',
};
export const VALIDATION_MESSAGES = {

View File

@ -26,4 +26,5 @@ export enum ExplorePageTabs {
GLOSSARY = 'glossaries',
TAG = 'tags',
DASHBOARD_DATA_MODEL = 'dashboardDataModel',
STORED_PROCEDURE = 'storedProcedure',
}

View File

@ -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 {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "成功",

View File

@ -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",

View File

@ -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": "Успешно",

View File

@ -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": "成功",

View File

@ -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;
}

View File

@ -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<number>(INITIAL_PAGING_VALUE);
const [storedProcedure, setStoredProcedure] = useState<StoredProcedureData>({
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 = () => {
</ActivityFeedProvider>
),
},
{
label: (
<TabsLabel
count={storedProcedure.paging.total}
id={EntityTabs.STORED_PROCEDURE}
isActive={activeTab === EntityTabs.STORED_PROCEDURE}
name={t('label.stored-procedure')}
/>
),
key: EntityTabs.STORED_PROCEDURE,
children: (
<StoredProcedureTab
fetchStoredProcedure={fetchStoreProcedureDetails}
pagingHandler={storedProcedurePagingHandler}
storedProcedure={storedProcedure}
onShowDeletedStoreProcedureChange={handleShowDeletedStoredProcedure}
/>
),
},
];
if (isPermissionsLoading) {

View File

@ -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;
}

View File

@ -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();

View File

@ -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<boolean>(true);
const [storedProcedure, setStoredProcedure] = useState<StoredProcedure>();
const [storedProcedurePermissions, setStoredProcedurePermissions] =
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const [isEdit, setIsEdit] = useState(false);
const [feedCount, setFeedCount] = useState<number>(0);
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
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: (
<TabsLabel
data-testid={EntityTabs.CODE}
id={EntityTabs.CODE}
name={t('label.code')}
/>
),
key: EntityTabs.CODE,
children: (
<Row gutter={[0, 16]} wrap={false}>
<Col
className="p-t-sm m-l-lg tab-content-height p-r-lg"
flex="auto">
<div className="d-flex flex-col gap-4">
<DescriptionV1
description={description}
entityFqn={storedProcedureFQN}
entityName={entityName}
entityType={EntityType.STORED_PROCEDURE}
hasEditAccess={
storedProcedurePermissions.EditAll ||
storedProcedurePermissions.EditDescription
}
isEdit={isEdit}
isReadOnly={deleted}
owner={owner}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
<Card className="m-b-md">
<SchemaEditor
editorClass="custom-code-mirror-theme full-screen-editor-height"
mode={{ name: CSMode.SQL }}
options={{
styleActiveLine: false,
readOnly: 'nocursor',
}}
value={code}
/>
</Card>
</div>
</Col>
<Col
className="entity-tag-right-panel-container"
data-testid="entity-right-panel"
flex="320px">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV2
displayType={DisplayType.READ_MORE}
entityFqn={storedProcedureFQN}
entityType={EntityType.STORED_PROCEDURE}
permission={
(storedProcedurePermissions.EditAll ||
storedProcedurePermissions.EditTags) &&
!deleted
}
selectedTags={tags}
tagType={TagSource.Classification}
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV2
displayType={DisplayType.READ_MORE}
entityFqn={storedProcedureFQN}
entityType={EntityType.STORED_PROCEDURE}
permission={
(storedProcedurePermissions.EditAll ||
storedProcedurePermissions.EditTags) &&
!deleted
}
selectedTags={tags}
tagType={TagSource.Glossary}
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Space>
</Col>
</Row>
),
},
{
label: (
<TabsLabel
count={feedCount}
id={EntityTabs.ACTIVITY_FEED}
isActive={activeTab === EntityTabs.ACTIVITY_FEED}
name={t('label.activity-feed-and-task-plural')}
/>
),
key: EntityTabs.ACTIVITY_FEED,
children: (
<ActivityFeedTab
entityType={EntityType.STORED_PROCEDURE}
fqn={entityFQN}
onFeedUpdate={getEntityFeedCount}
onUpdateEntityDetails={fetchStoredProcedureDetails}
/>
),
},
{
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
key: EntityTabs.LINEAGE,
children: (
<EntityLineageComponent
deleted={deleted}
entity={storedProcedure as SourceType}
entityType={EntityType.STORED_PROCEDURE}
hasEditAccess={
storedProcedurePermissions.EditAll ||
storedProcedurePermissions.EditLineage
}
/>
),
},
{
label: (
<TabsLabel
id={EntityTabs.CUSTOM_PROPERTIES}
name={t('label.custom-property-plural')}
/>
),
key: EntityTabs.CUSTOM_PROPERTIES,
children: (
<CustomPropertyTable
entityDetails={
storedProcedure as CustomPropertyProps['entityDetails']
}
entityType={EntityType.STORED_PROCEDURE}
handleExtensionUpdate={onExtensionUpdate}
hasEditAccess={
storedProcedurePermissions.EditAll ||
storedProcedurePermissions.EditCustomFields
}
hasPermission={storedProcedurePermissions.ViewAll}
/>
),
},
],
[
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 <Loader />;
}
if (
!(
storedProcedurePermissions.ViewAll || storedProcedurePermissions.ViewBasic
)
) {
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
}
if (!storedProcedure) {
return <ErrorPlaceHolder />;
}
return (
<PageLayoutV1 className="bg-white" pageTitle={t('label.stored-procedure')}>
<Row gutter={[0, 12]}>
<Col className="p-x-lg" data-testid="entity-page-header" span={24}>
<DataAssetsHeader
afterDeleteAction={afterDeleteAction}
dataAsset={storedProcedure}
entityType={EntityType.STORED_PROCEDURE}
permissions={storedProcedurePermissions}
onDisplayNameUpdate={handleDisplayNameUpdate}
onFollowClick={handleFollow}
onOwnerUpdate={handleUpdateOwner}
onRestoreDataAsset={handleRestoreStoredProcedures}
onTierUpdate={onTierUpdate}
onVersionClick={versionHandler}
/>
</Col>
{/* Entity Tabs */}
<Col span={24}>
<Tabs
destroyInactiveTabPane
activeKey={activeTab ?? EntityTabs.CODE}
className="entity-details-page-tabs"
data-testid="tabs"
items={tabs}
onChange={(activeKey: string) =>
handleTabChange(activeKey as EntityTabs)
}
/>
</Col>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deleteFeed}
open={Boolean(threadLink)}
postFeedHandler={postFeed}
threadLink={threadLink}
threadType={threadType}
updateThreadHandler={updateFeed}
onCancel={onThreadPanelClose}
/>
) : null}
</Row>
</PageLayoutV1>
);
};
export default withActivityFeed(StoredProcedurePage);

View File

@ -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<ServicePageData> = useMemo(
() => [
{
title: t('label.name'),
dataIndex: 'name',
key: 'name',
width: 350,
render: (_, record) => (
<Link
to={getEntityLink(
EntityType.STORED_PROCEDURE,
getEncodedFqn(record.fullyQualifiedName ?? '')
)}>
{getEntityName(record)}
</Link>
),
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
render: (text: string) =>
isEmpty(text) ? (
<Typography.Text className="text-grey-muted">
{t('label.no-description')}
</Typography.Text>
) : (
<RichTextEditorPreviewer markdown={text} />
),
},
],
[]
);
useEffect(() => {
fetchStoredProcedure();
}, [deleted]);
return (
<Row className="p-lg" data-testid="stored-procedure-table" gutter={[0, 16]}>
<Col className="d-flex justify-end" span={24}>
<Switch
checked={deleted}
data-testid="show-deleted-stored-procedure"
onClick={onShowDeletedStoreProcedureChange}
/>
<Typography.Text className="m-l-xs">
{t('label.deleted')}
</Typography.Text>{' '}
</Col>
<Col span={24}>
<Table
bordered
columns={tableColumn}
data-testid="data-models-table"
dataSource={data}
loading={{
spinning: isLoading,
indicator: <Loader size="small" />,
}}
locale={{
emptyText: <ErrorPlaceHolder className="m-y-md" />,
}}
pagination={false}
rowKey="id"
size="small"
/>
</Col>
<Col span={24}>
{paging && paging.total > PAGE_SIZE && (
<NextPrevious
currentPage={currentPage}
pageSize={PAGE_SIZE}
paging={paging}
pagingHandler={pagingHandler}
totalCount={paging.total}
/>
)}
</Col>
</Row>
);
};
export default StoredProcedureTab;

View File

@ -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;
}

View File

@ -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;

View File

@ -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<PagingResponse<ServicePageData[]>>(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<StoredProcedure>(url);
return response.data;
};
export const getStoredProceduresByName = async (
name: string,
fields: string | string[],
include: Include = Include.NonDeleted
) => {
const response = await APIClient.get<StoredProcedure>(
`${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<StoredProcedure>(url);
return response.data;
};
export const patchStoredProceduresDetails = async (
id: string,
data: Operation[]
) => {
const response = await APIClient.patch<
Operation[],
AxiosResponse<StoredProcedure>
>(`${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<EntityHistory>(url);
return response.data;
};
export const getStoredProceduresVersion = async (
id: string,
version: string
) => {
const url = `${URL}/${id}/versions/${version}`;
const response = await APIClient.get<StoredProcedure>(url);
return response.data;
};
export const restoreStoredProcedures = async (id: string) => {
const response = await APIClient.put<
RestoreRequestType,
AxiosResponse<StoredProcedure>
>(`${URL}/restore`, { id });
return response.data;
};

View File

@ -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;
}

View File

@ -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) && (
<ExtraInfoLabel
label={t('label.language')}
value={
(
storedProcedureDetails.storedProcedureCode as StoredProcedureCodeObject
).language ?? NO_DATA_PLACEHOLDER
}
/>
)}
</>
);
returnData.breadcrumbs = getBreadcrumbForTable(dataAsset as Table);
break;
case EntityType.TABLE:
default:
const tableDetails = dataAsset as Table;

View File

@ -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';
}

View File

@ -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 [];
}

View File

@ -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: <StorageIcon className="side-panel-icons" />,
},
{
label: i18next.t('label.stored-procedure'),
isProtected: Boolean(isAdminUser),
key: 'customAttributes.storedProcedure',
icon: <StoredProcedureIcon className="side-panel-icons" />,
},
],
},
{

View File

@ -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}`;

View File

@ -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),

View File

@ -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;
}