diff --git a/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts new file mode 100644 index 00000000000..8163439e1b3 --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/topicsAPI.ts @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +import { AxiosResponse } from 'axios'; +import { getURLWithQueryFields } from '../utils/APIUtils'; +import APIClient from './index'; + +export const getTopics: Function = ( + serviceName: string, + paging: string, + arrQueryFields: string +): Promise => { + const url = `${getURLWithQueryFields( + `/topics`, + arrQueryFields + )}&service=${serviceName}${paging ? paging : ''}`; + + return APIClient.get(url); +}; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx index 583bb11378b..16c14f823b3 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx @@ -19,7 +19,10 @@ import classNames from 'classnames'; import { ServiceTypes } from 'Models'; import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { serviceTypes } from '../../../constants/services.const'; -import { ServiceCategory } from '../../../enums/service.enum'; +import { + MessagingServiceType, + ServiceCategory, +} from '../../../enums/service.enum'; import { fromISOString } from '../../../utils/ServiceUtils'; import { Button } from '../../buttons/Button/Button'; import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview'; @@ -167,8 +170,8 @@ export const AddServiceModal: FunctionComponent = ({ // const [port, setPort] = useState(parseUrl?.port || ''); const [database, setDatabase] = useState(parseUrl?.database || ''); const [driverClass, setDriverClass] = useState(data?.driverClass || 'jdbc'); - const [broker, setBroker] = useState( - data?.brokers?.length ? data.brokers[0] : '' + const [brokers, setBrokers] = useState( + data?.brokers?.length ? data.brokers.join(', ') : '' ); const [schemaRegistry, setSchemaRegistry] = useState( data?.schemaRegistry || '' @@ -188,6 +191,13 @@ export const AddServiceModal: FunctionComponent = ({ }); const [sameNameError, setSameNameError] = useState(false); const markdownRef = useRef(); + + const getBrokerUrlPlaceholder = (): string => { + return selectService === MessagingServiceType.PULSAR + ? 'eg.: hostname:port' + : 'eg.: hostname1:port1, hostname2:port2'; + }; + const handleChangeFrequency = ( event: React.ChangeEvent ) => { @@ -289,7 +299,7 @@ export const AddServiceModal: FunctionComponent = ({ { setMsg = { ...setMsg, - broker: !broker, + broker: !brokers, }; } @@ -329,7 +339,10 @@ export const AddServiceModal: FunctionComponent = ({ { dataObj = { ...dataObj, - brokers: [broker], + brokers: + selectService === MessagingServiceType.PULSAR + ? [brokers] + : brokers.split(',').map((broker) => broker.trim()), schemaRegistry: schemaRegistry, }; } @@ -354,6 +367,7 @@ export const AddServiceModal: FunctionComponent = ({ className="tw-form-inputs tw-px-3 tw-py-1" id="url" name="url" + placeholder="eg.: username:password@hostname:port" type="text" value={url} onChange={handleValidation} @@ -416,6 +430,7 @@ export const AddServiceModal: FunctionComponent = ({ className="tw-form-inputs tw-px-3 tw-py-1" id="database" name="database" + placeholder="Enter database name" type="text" value={database} onChange={(e) => setDatabase(e.target.value)} @@ -460,9 +475,10 @@ export const AddServiceModal: FunctionComponent = ({ className="tw-form-inputs tw-px-3 tw-py-1" id="broker" name="broker" + placeholder={getBrokerUrlPlaceholder()} type="text" - value={broker} - onChange={(e) => setBroker(e.target.value)} + value={brokers} + onChange={(e) => setBrokers(e.target.value)} /> {showErrorMsg.broker && errorMsg('Broker url is required')} @@ -474,6 +490,7 @@ export const AddServiceModal: FunctionComponent = ({ className="tw-form-inputs tw-px-3 tw-py-1" id="schema-registry" name="schema-registry" + placeholder="eg.: hostname:port" type="text" value={schemaRegistry} onChange={(e) => setSchemaRegistry(e.target.value)} @@ -546,6 +563,7 @@ export const AddServiceModal: FunctionComponent = ({ className="tw-form-inputs tw-px-3 tw-py-1" id="name" name="name" + placeholder="Enter service name" type="text" value={name} onChange={handleValidation} diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx index f9b0b423f95..3e58c75667f 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/title-breadcrumb/title-breadcrumb.component.tsx @@ -33,15 +33,15 @@ const TitleBreadcrumb: FunctionComponent = ({ return (
  • + {link.imgSrc ? ( + + ) : null} {index < titleLinks.length - 1 ? ( <> - {link.imgSrc ? ( - - ) : null} {link.name} diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts index 0387ed6f31c..00cf422be18 100644 --- a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts +++ b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts @@ -36,6 +36,7 @@ const PLACEHOLDER_ROUTE_DATASET_FQN = ':datasetFQN'; const PLACEHOLDER_ROUTE_TOPIC_FQN = ':topicFQN'; const PLACEHOLDER_ROUTE_DATABASE_FQN = ':databaseFQN'; const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN'; +const PLACEHOLDER_ROUTE_SERVICE_TYPE = ':serviceType'; const PLACEHOLDER_ROUTE_SEARCHQUERY = ':searchQuery'; export const pagingObject = { after: '', before: '' }; @@ -101,7 +102,7 @@ export const ROUTES = { STORE: '/store', FEEDS: '/feeds', DUMMY: '/dummy', - SERVICE: `/service/${PLACEHOLDER_ROUTE_SERVICE_FQN}`, + SERVICE: `/service/${PLACEHOLDER_ROUTE_SERVICE_TYPE}/${PLACEHOLDER_ROUTE_SERVICE_FQN}`, SERVICES: '/services', USERS: '/users', SCORECARD: '/scorecard', @@ -129,9 +130,14 @@ export const getDatasetDetailsPath = ( return `${path}${columnName ? `#${columnName}` : ''}`; }; -export const getServiceDetailsPath = (serviceFQN: string) => { +export const getServiceDetailsPath = ( + serviceFQN: string, + serviceType: string +) => { let path = ROUTES.SERVICE; - path = path.replace(PLACEHOLDER_ROUTE_SERVICE_FQN, serviceFQN); + path = path + .replace(PLACEHOLDER_ROUTE_SERVICE_TYPE, serviceType) + .replace(PLACEHOLDER_ROUTE_SERVICE_FQN, serviceFQN); return path; }; diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/services.const.ts b/catalog-rest-service/src/main/resources/ui/src/constants/services.const.ts index 4ba2ff4f4d1..565698eb7af 100644 --- a/catalog-rest-service/src/main/resources/ui/src/constants/services.const.ts +++ b/catalog-rest-service/src/main/resources/ui/src/constants/services.const.ts @@ -29,6 +29,7 @@ import redshift from '../assets/img/service-icon-redshift.png'; import snowflakes from '../assets/img/service-icon-snowflakes.png'; import mysql from '../assets/img/service-icon-sql.png'; import plus from '../assets/svg/plus.svg'; +import { ServiceCategory } from '../enums/service.enum'; export const MYSQL = mysql; export const MSSQL = mssql; @@ -67,3 +68,14 @@ export const servicesDisplayName = { databaseServices: 'Database Service', messagingServices: 'Messaging Service', }; + +export const routeServiceTypes = [ + { + param: 'database', + type: ServiceCategory.DATABASE_SERVICES, + }, + { + param: 'messaging', + type: ServiceCategory.MESSAGING_SERVICES, + }, +]; diff --git a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts index 62b441743fd..73dd2d2e4da 100644 --- a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts +++ b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts @@ -50,12 +50,13 @@ declare module 'Models' { export type ServiceOption = { id: string; + brokers?: Array; description: string; ingestionSchedule?: { repeatFrequency: string; startDate: string; }; - jdbc: { connectionUrl: string; driverClass: string }; + jdbc?: { connectionUrl: string; driverClass: string }; name: string; serviceType: string; }; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/database-details/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/database-details/index.tsx index 65fe082fb5d..5c4392ee960 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/database-details/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/database-details/index.tsx @@ -115,7 +115,10 @@ const DatabaseDetails: FunctionComponent = () => { { name: resService.data.name, url: resService.data.name - ? getServiceDetailsPath(resService.data.name) + ? getServiceDetailsPath( + resService.data.name, + resService.data.serviceType + ) : '', imgSrc: resService.data.serviceType ? serviceTypeLogo(resService.data.serviceType) diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx index 03c84bce96c..2af56ede0de 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/my-data-details/index.tsx @@ -327,7 +327,10 @@ const MyDataDetailsPage = () => { { name: resService.data.name, url: resService.data.name - ? getServiceDetailsPath(resService.data.name) + ? getServiceDetailsPath( + resService.data.name, + resService.data.serviceType + ) : '', imgSrc: resService.data.serviceType ? serviceTypeLogo(resService.data.serviceType) diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/service/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/service/index.tsx index e94e81b0842..54eacfdcdbd 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/service/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/service/index.tsx @@ -19,27 +19,38 @@ import { AxiosError, AxiosResponse } from 'axios'; import classNames from 'classnames'; import { isNull, isUndefined } from 'lodash'; import { Database, Paging, ServiceOption } from 'Models'; -import React, { FunctionComponent, useEffect, useState } from 'react'; +import React, { Fragment, FunctionComponent, useEffect, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { getDatabases } from '../../axiosAPIs/databaseAPI'; import { getServiceByFQN, updateService } from '../../axiosAPIs/serviceAPI'; +import { getTopics } from '../../axiosAPIs/topicsAPI'; import NextPrevious from '../../components/common/next-previous/NextPrevious'; +import PopOver from '../../components/common/popover/PopOver'; import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer'; import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-breadcrumb.component'; import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface'; import PageContainer from '../../components/containers/PageContainer'; import Loader from '../../components/Loader/Loader'; import { ModalWithMarkdownEditor } from '../../components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import Tags from '../../components/tags/tags'; import { pagingObject } from '../../constants/constants'; +import { ServiceCategory } from '../../enums/service.enum'; +import { Topic } from '../../generated/entity/data/topic'; import useToastContext from '../../hooks/useToastContext'; import { isEven } from '../../utils/CommonUtils'; -import { getFrequencyTime, serviceTypeLogo } from '../../utils/ServiceUtils'; +import { + getFrequencyTime, + getServiceCategoryFromType, + serviceTypeLogo, +} from '../../utils/ServiceUtils'; import SVGIcons from '../../utils/SvgUtils'; import { getUsagePercentile } from '../../utils/TableUtils'; const ServicePage: FunctionComponent = () => { - const { serviceFQN } = useParams() as Record; - const [serviceName] = useState('databaseServices'); + const { serviceFQN, serviceType } = useParams() as Record; + const [serviceName, setServiceName] = useState( + getServiceCategoryFromType(serviceType) + ); const [slashedTableName, setSlashedTableName] = useState< TitleBreadcrumbProps['titleLinks'] >([]); @@ -47,7 +58,7 @@ const ServicePage: FunctionComponent = () => { const [description, setDescription] = useState(''); const [serviceDetails, setServiceDetails] = useState(); const [data, setData] = useState>([]); - const [isLoading, setIsloading] = useState(false); + const [isLoading, setIsloading] = useState(true); const [paging, setPaging] = useState(pagingObject); const showToast = useToastContext(); @@ -70,6 +81,179 @@ const ServicePage: FunctionComponent = () => { }); }; + const fetchTopics = (paging?: string) => { + setIsloading(true); + getTopics(serviceFQN, paging, ['owner', 'service', 'tags']) + .then((res: AxiosResponse) => { + if (res.data.data) { + setData(res.data.data); + setPaging(res.data.paging); + setIsloading(false); + } else { + setData([]); + setPaging(pagingObject); + setIsloading(false); + } + }) + .catch(() => { + setIsloading(false); + }); + }; + + const getOtherDetails = (paging?: string) => { + switch (serviceName) { + case ServiceCategory.DATABASE_SERVICES: { + fetchDatabases(paging); + + break; + } + case ServiceCategory.MESSAGING_SERVICES: { + fetchTopics(paging); + + break; + } + default: + break; + } + }; + + const getOptionalFields = (): JSX.Element => { + switch (serviceName) { + case ServiceCategory.DATABASE_SERVICES: { + return ( + + + Driver Class : + {' '} + + {serviceDetails?.jdbc?.driverClass || '--'} + + + + ); + } + case ServiceCategory.MESSAGING_SERVICES: { + return ( + + Brokers :{' '} + + {serviceDetails?.brokers?.length ? ( + <> + {serviceDetails.brokers.slice(0, 3).join(', ')} + {serviceDetails.brokers.length > 3 ? ( + + {serviceDetails.brokers + .slice(3) + .map((broker, index) => ( + + + {broker} + + + ))} + + } + position="bottom" + theme="light" + trigger="click"> + ... + + ) : null} + + ) : ( + '--' + )} + + + + ); + } + default: { + return <>; + } + } + }; + + const getTableHeaders = (): JSX.Element => { + switch (serviceName) { + case ServiceCategory.DATABASE_SERVICES: { + return ( + <> + Database Name + Description + Owner + Usage + + ); + } + case ServiceCategory.MESSAGING_SERVICES: { + return ( + <> + Topic Name + Description + Owner + Tags + + ); + } + default: + return <>; + } + }; + + const getOptionalTableCells = (data: Database | Topic) => { + switch (serviceName) { + case ServiceCategory.DATABASE_SERVICES: { + const database = data as Database; + + return ( + +

    + {getUsagePercentile( + database.usageSummary.weeklyStats.percentileRank + )} +

    + + ); + } + case ServiceCategory.MESSAGING_SERVICES: { + const topic = data as Topic; + + return ( + + {topic.tags && topic.tags?.length > 0 + ? topic.tags.map((tag, tagIndex) => ( + + + + )) + : '--'} + + ); + } + default: + return <>; + } + }; + + useEffect(() => { + setServiceName(getServiceCategoryFromType(serviceType)); + }, [serviceType]); + useEffect(() => { getServiceByFQN(serviceName, serviceFQN).then( (resService: AxiosResponse) => { @@ -84,13 +268,10 @@ const ServicePage: FunctionComponent = () => { activeTitle: true, }, ]); + getOtherDetails(); } ); - }, []); - - useEffect(() => { - fetchDatabases(); - }, [serviceFQN]); + }, [serviceFQN, serviceName]); const onCancel = () => { setIsEdit(false); @@ -129,7 +310,7 @@ const ServicePage: FunctionComponent = () => { const pagingString = `&${cursorType}=${ paging[cursorType as keyof typeof paging] }`; - fetchDatabases(pagingString); + getOtherDetails(pagingString); }; return ( @@ -142,17 +323,7 @@ const ServicePage: FunctionComponent = () => {
    - - - Driver Class : - {' '} - - {serviceDetails?.jdbc.driverClass || '--'} - - - • - - + {getOptionalFields()} Ingestion : @@ -163,7 +334,7 @@ const ServicePage: FunctionComponent = () => { ? getFrequencyTime( serviceDetails.ingestionSchedule.repeatFrequency ) - : 'N/A'} + : '--'} • @@ -223,12 +394,7 @@ const ServicePage: FunctionComponent = () => { className="tw-bg-white tw-w-full tw-mb-4" data-testid="database-tables"> - - Database Name - Description - Owner - Usage - + {getTableHeaders()} {data.length > 0 ? ( @@ -259,13 +425,7 @@ const ServicePage: FunctionComponent = () => {

    {database?.owner?.name || '--'}

    - -

    - {getUsagePercentile( - database.usageSummary.weeklyStats.percentileRank - )} -

    - + {getOptionalTableCells(database)} )) ) : ( diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/services/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/services/index.tsx index 6cb4bb5f1f2..aca9f6cc4ca 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/services/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/services/index.tsx @@ -292,7 +292,11 @@ const ServicesPage = () => { className="tw-card tw-flex tw-py-2 tw-px-3 tw-justify-between tw-text-grey-muted" key={index}>
    - +
    @@ -359,9 +363,9 @@ const ServicesPage = () => {
    ))}
    handleAddService()}> - + Add service

    Add new {servicesDisplayName[serviceName]}

    diff --git a/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts b/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts index 069b7711e69..1376d6954cc 100644 --- a/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts +++ b/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts @@ -1,5 +1,5 @@ import { AxiosResponse } from 'axios'; -import { ServiceCollection, ServiceData } from 'Models'; +import { ServiceCollection, ServiceData, ServiceTypes } from 'Models'; import { getServiceDetails, getServices } from '../axiosAPIs/serviceAPI'; import { ServiceDataObj } from '../components/Modals/AddServiceModal/AddServiceModal'; import { @@ -12,6 +12,7 @@ import { POSTGRES, PULSAR, REDSHIFT, + serviceTypes, SERVICE_DEFAULT, SNOWFLAKE, } from '../constants/services.const'; @@ -137,3 +138,18 @@ export const getAllServices = (): Promise> => { }); }); }; + +export const getServiceCategoryFromType = ( + type: string +): ServiceTypes | undefined => { + let serviceCategory; + for (const category in serviceTypes) { + if (serviceTypes[category as ServiceTypes].includes(type)) { + serviceCategory = category as ServiceTypes; + + break; + } + } + + return serviceCategory; +};