diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-mlflow.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-mlflow.png new file mode 100644 index 00000000000..beeb3f8aec7 Binary files /dev/null and b/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-mlflow.png differ diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-scikit.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-scikit.png new file mode 100644 index 00000000000..dd812b69a2f Binary files /dev/null and b/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-scikit.png differ diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/mlmodal.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/mlmodal.svg new file mode 100644 index 00000000000..ede4e0eff79 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/mlmodal.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/mlModelAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/mlModelAPI.ts index 538996f684c..cc02e1a5e4b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/mlModelAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/mlModelAPI.ts @@ -12,6 +12,7 @@ */ import { AxiosResponse } from 'axios'; +import { isNil } from 'lodash'; import { Mlmodel } from '../generated/entity/data/mlmodel'; import { getURLWithQueryFields } from '../utils/APIUtils'; import APIClient from './index'; @@ -25,6 +26,39 @@ export const getMlModelByFQN: Function = ( return APIClient.get(url); }; +export const getMlmodels = ( + serviceName: string, + paging: string, + arrQueryFields: string[] +): Promise => { + const url = `${getURLWithQueryFields( + `/mlmodels`, + arrQueryFields + )}&service=${serviceName}${paging ? paging : ''}`; + + return APIClient.get(url); +}; + +export const getAllMlModal = ( + paging: string, + arrQueryFields: string, + limit?: number +): Promise => { + const searchParams = new URLSearchParams(); + + if (!isNil(limit)) { + searchParams.set('limit', `${limit}`); + } + + const url = getURLWithQueryFields( + `/mlmodels`, + arrQueryFields, + `${searchParams.toString()}${paging ? `&${paging}` : ''}` + ); + + return APIClient.get(url); +}; + export const patchMlModelDetails: Function = ( id: string, data: Mlmodel diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddService/Steps/SelectServiceType.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddService/Steps/SelectServiceType.tsx index 5973f0729c3..963e5d34360 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddService/Steps/SelectServiceType.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddService/Steps/SelectServiceType.tsx @@ -14,7 +14,10 @@ import classNames from 'classnames'; import { startCase } from 'lodash'; import React, { useEffect, useState } from 'react'; -import { serviceTypes } from '../../../constants/services.const'; +import { + excludedService, + serviceTypes, +} from '../../../constants/services.const'; import { ServiceCategory } from '../../../enums/service.enum'; import { errorMsg, getServiceLogo } from '../../../utils/CommonUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils'; @@ -51,7 +54,11 @@ const SelectServiceType = ({ ? serviceCategory : allCategory[0]; setCategory(selectedCategory); - setSelectedConnectors(serviceTypes[selectedCategory]); + setSelectedConnectors( + serviceTypes[selectedCategory].filter( + (service) => !excludedService.find((e) => e === service) + ) + ); }, [serviceCategory]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx index 32c61a4981b..d0d4db93d48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx @@ -621,7 +621,7 @@ const Entitylineage: FunctionComponent = ({ }; } else { const updatedColumnsLineage: ColumnLineage[] = - currentEdge.columnsLineage.map((l) => { + currentEdge.columnsLineage?.map((l) => { if (l.toColumn === targetHandle) { return { ...l, @@ -633,7 +633,7 @@ const Entitylineage: FunctionComponent = ({ } return l; - }); + }) || []; if ( !updatedColumnsLineage.find((l) => l.toColumn === targetHandle) ) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx index 9b768154a37..cd9310c9f03 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx @@ -26,14 +26,17 @@ import React, { import AppState from '../../AppState'; import { getDashboardDetailsPath, + getServiceDetailsPath, getTeamAndUserDetailsPath, } from '../../constants/constants'; import { EntityType } from '../../enums/entity.enum'; +import { ServiceCategory } from '../../enums/service.enum'; import { OwnerType } from '../../enums/user.enum'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { EntityReference } from '../../generated/type/entityReference'; import { LabelType, State, TagLabel } from '../../generated/type/tagLabel'; import { getEntityName, getEntityPlaceHolder } from '../../utils/CommonUtils'; +import { serviceTypeLogo } from '../../utils/ServiceUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; import Description from '../common/description/Description'; import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; @@ -83,8 +86,19 @@ const MlModelDetail: FC = ({ const mlModelTags = useMemo(() => { return getTagsWithoutTier(mlModelDetail.tags || []); }, [mlModelDetail.tags]); - const slashedMlModelName: TitleBreadcrumbProps['titleLinks'] = [ + { + name: mlModelDetail.service.name || '', + url: mlModelDetail.service.name + ? getServiceDetailsPath( + mlModelDetail.service.name, + ServiceCategory.ML_MODAL_SERVICES + ) + : '', + imgSrc: mlModelDetail.serviceType + ? serviceTypeLogo(mlModelDetail.serviceType || '') + : undefined, + }, { name: getEntityName(mlModelDetail as unknown as EntityReference), url: '', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx index 7765827d7cf..703879eb53c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx @@ -30,6 +30,7 @@ type Props = { countDashboards: number; countPipelines: number; countServices: number; + countMlModal: number; countTables: number; countTopics: number; countTeams: number; @@ -47,6 +48,7 @@ type Summary = { const MyAssetStats: FunctionComponent = ({ countDashboards, countPipelines, + countMlModal, countServices, countTables, countTopics, @@ -85,6 +87,13 @@ const MyAssetStats: FunctionComponent = ({ link: getExplorePathWithSearch(undefined, 'pipelines'), dataTestId: 'pipelines', }, + mlModal: { + icon: Icons.MLMODAL, + data: 'ML Models', + count: countMlModal, + link: getExplorePathWithSearch(undefined, 'mlmodels'), + dataTestId: 'mlmodels', + }, service: { icon: Icons.SERVICE, data: 'Services', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx index 838e073e050..a13cefcd47d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx @@ -38,6 +38,7 @@ const mockProp = { countTeams: 7, countTopics: 13, countUsers: 100, + countMlModal: 2, }; describe('Test MyDataHeader Component', () => { @@ -51,14 +52,14 @@ describe('Test MyDataHeader Component', () => { expect(myDataHeader).toBeInTheDocument(); }); - it('Should have 7 data summary details', () => { + it('Should have 8 data summary details', () => { const { container } = render(, { wrapper: MemoryRouter, }); const dataSummary = getAllByTestId(container, /-summary$/); - expect(dataSummary.length).toBe(7); + expect(dataSummary.length).toBe(8); }); it('OnClick it should redirect to respective page', () => { @@ -72,6 +73,7 @@ describe('Test MyDataHeader Component', () => { const topics = getByTestId(container, 'topics'); const dashboards = getByTestId(container, 'dashboards'); const pipelines = getByTestId(container, 'pipelines'); + const mlmodel = getByTestId(container, 'mlmodels'); const service = getByTestId(container, 'service'); const user = getByTestId(container, 'user'); const terms = getByTestId(container, 'terms'); @@ -80,6 +82,7 @@ describe('Test MyDataHeader Component', () => { expect(topics).toHaveAttribute('href', '/explore/topics/'); expect(dashboards).toHaveAttribute('href', '/explore/dashboards/'); expect(pipelines).toHaveAttribute('href', '/explore/pipelines/'); + expect(mlmodel).toHaveAttribute('href', '/explore/mlmodels/'); expect(service).toHaveAttribute('href', '/services'); expect(user).toHaveAttribute( 'href', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx index abc925100be..94399f057ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx @@ -51,6 +51,7 @@ const MyData: React.FC = ({ countTeams, countUsers, ownedData, + countMlModal, followedData, feedData, feedFilter, @@ -108,6 +109,7 @@ const MyData: React.FC = ({
= ({ case ServiceCategory.PIPELINE_SERVICES: { connSch = getPipelineConfig(serviceType as PipelineServiceType); + break; + } + case ServiceCategory.ML_MODAL_SERVICES: { + connSch = getMlmodelConfig(serviceType as MlModelServiceType); + break; } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx index f1963ffac8f..5af4f368bd6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx @@ -23,11 +23,13 @@ import { EntityType } from '../../enums/entity.enum'; import { DashboardServiceType } from '../../generated/entity/services/dashboardService'; import { DatabaseServiceType } from '../../generated/entity/services/databaseService'; import { MessagingServiceType } from '../../generated/entity/services/messagingService'; +import { MlModelServiceType } from '../../generated/entity/services/mlmodelService'; import { PipelineServiceType } from '../../generated/entity/services/pipelineService'; import { ConfigData } from '../../interface/service.interface'; import { getDashboardConfig } from '../../utils/DashboardServiceUtils'; import { getDatabaseConfig } from '../../utils/DatabaseServiceUtils'; import { getMessagingConfig } from '../../utils/MessagingServiceUtils'; +import { getMlmodelConfig } from '../../utils/MlmodelServiceUtils'; import { getPipelineConfig } from '../../utils/PipelineServiceUtils'; import PopOver from '../common/popover/PopOver'; @@ -143,6 +145,10 @@ const ServiceConnectionDetails = ({ case EntityType.PIPELINE_SERVICE: setSchema(getPipelineConfig(serviceFQN as PipelineServiceType).schema); + break; + case EntityType.MLMODEL_SERVICE: + setSchema(getMlmodelConfig(serviceFQN as MlModelServiceType).schema); + break; } }, [serviceCategory, serviceFQN]); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/services.const.ts b/openmetadata-ui/src/main/resources/ui/src/constants/services.const.ts index 6f129dff0cd..c2906ad572b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/services.const.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/services.const.ts @@ -30,6 +30,7 @@ import kafka from '../assets/img/service-icon-kafka.png'; import looker from '../assets/img/service-icon-looker.png'; import mariadb from '../assets/img/service-icon-mariadb.png'; import metabase from '../assets/img/service-icon-metabase.png'; +import mlflow from '../assets/img/service-icon-mlflow.png'; import mssql from '../assets/img/service-icon-mssql.png'; import oracle from '../assets/img/service-icon-oracle.png'; import postgres from '../assets/img/service-icon-post.png'; @@ -41,6 +42,7 @@ import query from '../assets/img/service-icon-query.png'; import redash from '../assets/img/service-icon-redash.png'; import redshift from '../assets/img/service-icon-redshift.png'; import salesforce from '../assets/img/service-icon-salesforce.png'; +import scikit from '../assets/img/service-icon-scikit.png'; import singlestore from '../assets/img/service-icon-singlestore.png'; import snowflakes from '../assets/img/service-icon-snowflakes.png'; import mysql from '../assets/img/service-icon-sql.png'; @@ -57,6 +59,7 @@ import topicDefault from '../assets/svg/topic.svg'; import { DashboardServiceType } from '../generated/entity/services/dashboardService'; import { DatabaseServiceType } from '../generated/entity/services/databaseService'; import { MessagingServiceType } from '../generated/entity/services/messagingService'; +import { MlModelServiceType } from '../generated/entity/services/mlmodelService'; import { PipelineServiceType } from '../generated/entity/services/pipelineService'; export const NoDataFoundPlaceHolder = noDataFound; @@ -90,6 +93,8 @@ export const DRUID = druid; export const DYNAMODB = dynamodb; export const SINGLESTORE = singlestore; export const SALESFORCE = salesforce; +export const MLFLOW = mlflow; +export const SCIKIT = scikit; export const DELTALAKE = deltalake; export const DEFAULT_SERVICE = iconDefaultService; @@ -103,12 +108,13 @@ export const PIPELINE_DEFAULT = pipelineDefault; export const PLUS = plus; export const NOSERVICE = noService; - +export const excludedService = [MlModelServiceType.Sklearn]; export const serviceTypes: Record> = { databaseServices: Object.values(DatabaseServiceType), messagingServices: Object.values(MessagingServiceType), dashboardServices: Object.values(DashboardServiceType), pipelineServices: Object.values(PipelineServiceType), + mlmodelServices: Object.values(MlModelServiceType), }; export const arrServiceTypes: Array = [ @@ -116,6 +122,7 @@ export const arrServiceTypes: Array = [ 'messagingServices', 'dashboardServices', 'pipelineServices', + 'mlmodelServices', ]; export const servicesDisplayName = { @@ -123,6 +130,7 @@ export const servicesDisplayName = { messagingServices: 'Messaging Service', dashboardServices: 'Dashboard Service', pipelineServices: 'Pipeline Service', + mlmodelServices: 'ML Model Service', }; export const STEPS_FOR_ADD_SERVICE: Array = [ 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 5d59d7cbdb3..bf447ab5afa 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 @@ -25,6 +25,7 @@ export enum EntityType { MESSAGING_SERVICE = 'messagingService', DASHBOARD_SERVICE = 'dashboardService', PIPELINE_SERVICE = 'pipelineService', + MLMODEL_SERVICE = 'mlmodelService', WEBHOOK = 'webhook', MLMODEL = 'mlmodel', TYPE = 'type', diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/service.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/service.enum.ts index 2fe58b931ce..be04e029459 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/service.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/service.enum.ts @@ -16,6 +16,7 @@ export enum ServiceCategory { MESSAGING_SERVICES = 'messagingServices', DASHBOARD_SERVICES = 'dashboardServices', PIPELINE_SERVICES = 'pipelineServices', + ML_MODAL_SERVICES = 'mlmodelServices', } export enum IngestionType { diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/service.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/service.interface.ts index b0a592fb16f..b3c26958b0f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/service.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/service.interface.ts @@ -15,6 +15,7 @@ import { DynamicObj, Paging } from 'Models'; import { DashboardService } from '../generated/entity/services/dashboardService'; import { DatabaseService } from '../generated/entity/services/databaseService'; import { MessagingService } from '../generated/entity/services/messagingService'; +import { MlmodelService } from '../generated/entity/services/mlmodelService'; import { PipelineService } from '../generated/entity/services/pipelineService'; export interface IngestionSchedule { @@ -60,13 +61,15 @@ export interface EditObj { export type ServiceDataObj = { name: string } & Partial & Partial & Partial & - Partial; + Partial & + Partial; export type DataService = | DatabaseService | MessagingService | DashboardService - | PipelineService; + | PipelineService + | MlmodelService; export interface ServiceResponse { data: Array; @@ -76,4 +79,5 @@ export interface ServiceResponse { export type ConfigData = Partial & Partial & Partial & - Partial; + Partial & + Partial; diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts index faf272d26e2..1148bf1281d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts @@ -399,7 +399,8 @@ declare module 'Models' { | 'databaseServices' | 'messagingServices' | 'dashboardServices' - | 'pipelineServices'; + | 'pipelineServices' + | 'mlmodelServices'; export type SampleData = { columns: Array; diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Service.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Service.mock.ts index 9f8d14291fc..7f7d7ef1128 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/Service.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Service.mock.ts @@ -41,9 +41,46 @@ export const mockServiceDetails = { href: 'http://pipelineServices', }, }, + { + collection: { + name: 'mlmodelServices', + documentation: 'MlModel service collection', + href: 'http://localhost:8585/api/v1/services/mlmodelServices', + }, + }, ], }; +export const mockMlmodelService = { + data: { + data: [ + { + id: 'b59a9acb-6c90-481e-afd9-ec0f208c4f35', + name: 'mlflow_svc', + fullyQualifiedName: 'mlflow_svc', + serviceType: 'Mlflow', + description: 'description for mlflow_svc', + version: 0.4, + updatedAt: 1655890983668, + updatedBy: 'anonymous', + connection: { + config: { + type: 'Mlflow', + registryUri: 'http://localhost:8088', + trackingUri: 'http://localhost:8088', + supportsMetadataExtraction: null, + }, + }, + href: 'http://localhost:8585/api/v1/services/mlmodelServices/b59a9acb-6c90-481e-afd9-ec0f208c4f35', + deleted: false, + }, + ], + paging: { + total: 1, + }, + }, +}; + export const mockDatabaseService = { data: { data: [ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx index c3bdafb36cf..c193c8a3cb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx @@ -22,6 +22,7 @@ import AppState from '../../AppState'; import { getAllDashboards } from '../../axiosAPIs/dashboardAPI'; import { getFeedsWithFilter, postFeedById } from '../../axiosAPIs/feedsAPI'; import { fetchSandboxConfig, searchData } from '../../axiosAPIs/miscAPI'; +import { getAllMlModal } from '../../axiosAPIs/mlModelAPI'; import { getAllPipelines } from '../../axiosAPIs/pipelineAPI'; import { getAllTables } from '../../axiosAPIs/tableAPI'; import { getTeams } from '../../axiosAPIs/teamsAPI'; @@ -60,6 +61,7 @@ const MyDataPage = () => { const [countTopics, setCountTopics] = useState(); const [countDashboards, setCountDashboards] = useState(); const [countPipelines, setCountPipelines] = useState(); + const [countMlModal, setCountMlModal] = useState(); const [countUsers, setCountUsers] = useState(); const [countTeams, setCountTeams] = useState(); @@ -171,6 +173,23 @@ const MyDataPage = () => { ); setCountDashboards(0); }); + + // limit=0 will fetch empty data list with total count + getAllMlModal('', '', 0) + .then((res) => { + if (res.data) { + setCountMlModal(res.data.paging.total); + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; + } + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['unexpected-server-response'] + ); + setCountMlModal(0); + }); }; const fetchTeamsAndUsersCount = () => { @@ -415,10 +434,12 @@ const MyDataPage = () => { !isUndefined(countDashboards) && !isUndefined(countPipelines) && !isUndefined(countTeams) && + !isUndefined(countMlModal) && !isUndefined(countUsers) ? ( ({ ), })); +jest.mock('../../axiosAPIs/mlModelAPI', () => ({ + getAllMlModal: jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + data: [], + }, + }) + ), +})); + jest.mock('../../axiosAPIs/userAPI', () => ({ getUsers: jest.fn().mockImplementation(() => Promise.resolve({ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx index fc699c4c950..0049dead94a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx @@ -28,6 +28,7 @@ import { triggerIngestionPipelineById, } from '../../axiosAPIs/ingestionPipelineAPI'; import { fetchAirflowConfig } from '../../axiosAPIs/miscAPI'; +import { getMlmodels } from '../../axiosAPIs/mlModelAPI'; import { getPipelines } from '../../axiosAPIs/pipelineAPI'; import { getServiceByFQN, updateService } from '../../axiosAPIs/serviceAPI'; import { getTopics } from '../../axiosAPIs/topicsAPI'; @@ -57,6 +58,7 @@ import { ServiceCategory } from '../../enums/service.enum'; import { OwnerType } from '../../enums/user.enum'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Database } from '../../generated/entity/data/database'; +import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { Pipeline } from '../../generated/entity/data/pipeline'; import { Topic } from '../../generated/entity/data/topic'; import { DatabaseService } from '../../generated/entity/services/databaseService'; @@ -129,6 +131,8 @@ const ServicePage: FunctionComponent = () => { return 'Topics'; case ServiceCategory.PIPELINE_SERVICES: return 'Pipelines'; + case ServiceCategory.ML_MODAL_SERVICES: + return 'Models'; case ServiceCategory.DATABASE_SERVICES: default: return 'Databases'; @@ -420,6 +424,26 @@ const ServicePage: FunctionComponent = () => { }); }; + const fetchMlModal = (paging = '') => { + setIsloading(true); + getMlmodels(serviceFQN, paging, ['owner', 'tags']) + .then((res: AxiosResponse) => { + if (res.data.data) { + setData(res.data.data); + setPaging(res.data.paging); + setInstanceCount(res.data.paging.total); + setIsloading(false); + } else { + setData([]); + setPaging(pagingObject); + setIsloading(false); + } + }) + .catch(() => { + setIsloading(false); + }); + }; + const getAirflowStatus = () => { return new Promise((resolve, reject) => { checkAirflowStatus() @@ -456,6 +480,11 @@ const ServicePage: FunctionComponent = () => { break; } + case ServiceCategory.ML_MODAL_SERVICES: { + fetchMlModal(paging); + + break; + } default: break; } @@ -472,6 +501,9 @@ const ServicePage: FunctionComponent = () => { case ServiceCategory.PIPELINE_SERVICES: return getEntityLink(SearchIndex.PIPELINE, fqn); + case ServiceCategory.ML_MODAL_SERVICES: + return getEntityLink(SearchIndex.MLMODEL, fqn); + case ServiceCategory.DATABASE_SERVICES: default: return `/database/${fqn}`; @@ -520,6 +552,16 @@ const ServicePage: FunctionComponent = () => { ); } + case ServiceCategory.ML_MODAL_SERVICES: { + return ( + <> + Model Name + Description + Owner + Tags + + ); + } default: return <>; } @@ -594,6 +636,24 @@ const ServicePage: FunctionComponent = () => { ); } + case ServiceCategory.ML_MODAL_SERVICES: { + const mlmodal = data as Mlmodel; + + return ( + + {mlmodal.tags && mlmodal.tags?.length > 0 ? ( + + ) : ( + '--' + )} + + ); + } default: return <>; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/services/index.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/services/index.test.tsx index 03c24ec4438..aedd806a5ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/services/index.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/services/index.test.tsx @@ -37,6 +37,7 @@ import { mockLookerService, mockMessagingService, mockMetabaseService, + mockMlmodelService, mockPipelineService, mockPowerBIService, mockPulsarService, @@ -81,6 +82,9 @@ jest.mock('../../axiosAPIs/serviceAPI', () => ({ case 'pipelineServices': return Promise.resolve(mockPipelineService); + case 'mlmodelServices': + return Promise.resolve(mockMlmodelService); + default: return Promise.resolve(mockDashboardService); } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/services/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/services/index.tsx index 6e38f2a0e31..aea05403186 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/services/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/services/index.tsx @@ -45,6 +45,7 @@ import { ServiceCategory } from '../../enums/service.enum'; import { DashboardService } from '../../generated/entity/services/dashboardService'; import { DatabaseService } from '../../generated/entity/services/databaseService'; import { MessagingService } from '../../generated/entity/services/messagingService'; +import { MlmodelService } from '../../generated/entity/services/mlmodelService'; import { PipelineService } from '../../generated/entity/services/pipelineService'; import { EntityReference } from '../../generated/type/entityReference'; import { Paging } from '../../generated/type/paging'; @@ -71,6 +72,7 @@ type ServiceRecord = { messagingServices: Array; dashboardServices: Array; pipelineServices: Array; + mlmodelServices: Array; }; type ServicePagingRecord = { @@ -78,6 +80,7 @@ type ServicePagingRecord = { messagingServices: Paging; dashboardServices: Paging; pipelineServices: Paging; + mlmodelServices: Paging; }; export type ApiData = { @@ -104,12 +107,14 @@ const ServicesPage = () => { messagingServices: pagingObject, dashboardServices: pagingObject, pipelineServices: pagingObject, + mlmodelServices: pagingObject, }); const [services, setServices] = useState({ databaseServices: [], messagingServices: [], dashboardServices: [], pipelineServices: [], + mlmodelServices: [], }); const [serviceList, setServiceList] = useState>([]); const [isLoading, setIsLoading] = useState(false); @@ -121,6 +126,7 @@ const ServicesPage = () => { messagingServices: 0, dashboardServices: 0, pipelineServices: 0, + mlmodelServices: 0, }); const [currentPage, setCurrentPage] = useState(1); @@ -170,6 +176,7 @@ const ServicesPage = () => { messagingServices: servicePaging.messagingServices.total || 0, dashboardServices: servicePaging.dashboardServices.total || 0, pipelineServices: servicePaging.pipelineServices.total || 0, + mlmodelServices: servicePaging.mlmodelServices.total || 0, }); setServiceList( serviceRecord[serviceName] as unknown as Array @@ -190,14 +197,14 @@ const ServicesPage = () => { } } } - setIsLoading(false); }) .catch((err: AxiosError) => { showErrorToast( err, jsonData['api-error-messages']['fetch-services-error'] ); - }); + }) + .finally(() => setIsLoading(false)); } }; @@ -255,7 +262,7 @@ const ServicesPage = () => { onClick={() => { handleTabChange(tab.name); }}> -

+

{tab.displayName}

@@ -322,6 +329,31 @@ const ServicesPage = () => { ); } + + case ServiceCategory.ML_MODAL_SERVICES: { + const mlmodel = service as unknown as MlmodelService; + + return ( + <> +
+ + + {mlmodel.connection.config?.registryUri} + +
+
+ + + {mlmodel.connection.config?.trackingUri} + +
+ + ); + } default: { return <>; } @@ -416,7 +448,7 @@ const ServicesPage = () => { {serviceList.map((service, index) => (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx index 77a2d2c92ed..e3fa2da0b87 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx @@ -127,6 +127,7 @@ const TourPage = () => { return ( edge.toEntity === n.id); if (!isUndefined(edge.lineageDetails)) { - edge.lineageDetails.columnsLineage.forEach((e) => { + edge.lineageDetails.columnsLineage?.forEach((e) => { const toColumn = e.toColumn || ''; if (e.fromColumns && e.fromColumns.length > 0) { e.fromColumns.forEach((fromColumn) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts new file mode 100644 index 00000000000..61bb61215aa --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { cloneDeep } from 'lodash'; +import { COMMON_UI_SCHEMA } from '../constants/services.const'; +import { MlModelServiceType } from '../generated/entity/services/mlmodelService'; +import mlflowConnection from '../jsons/connectionSchemas/connections/mlmodel/mlflowConnection.json'; +import sklearnConnection from '../jsons/connectionSchemas/connections/mlmodel/sklearnConnection.json'; + +export const getMlmodelConfig = (type: MlModelServiceType) => { + let schema = {}; + const uiSchema = { ...COMMON_UI_SCHEMA }; + switch (type) { + case MlModelServiceType.Mlflow: { + schema = mlflowConnection; + + break; + } + case MlModelServiceType.Sklearn: { + schema = sklearnConnection; + + break; + } + } + + return cloneDeep({ schema, uiSchema }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index e98d8d7977f..cfb5c63e31a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -52,6 +52,7 @@ import { LOOKER, MARIADB, METABASE, + MLFLOW, MSSQL, MYSQL, ORACLE, @@ -64,6 +65,7 @@ import { REDASH, REDSHIFT, SALESFORCE, + SCIKIT, serviceTypes, SINGLESTORE, SNOWFLAKE, @@ -76,6 +78,7 @@ import { } from '../constants/services.const'; import { ServiceCategory } from '../enums/service.enum'; import { ConnectionType } from '../generated/api/services/ingestionPipelines/testServiceConnection'; +import { MlModelServiceType } from '../generated/entity/data/mlmodel'; import { DashboardServiceType } from '../generated/entity/services/dashboardService'; import { DatabaseServiceType } from '../generated/entity/services/databaseService'; import { PipelineType as IngestionPipelineType } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; @@ -186,6 +189,12 @@ export const serviceTypeLogo = (type: string) => { case PipelineServiceType.Prefect: return PREFECT; + + case MlModelServiceType.Mlflow: + return MLFLOW; + + case MlModelServiceType.Sklearn: + return SCIKIT; default: { let logo; if (serviceTypes.messagingServices.includes(type)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index f191be1b01b..4e9b8a8456c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -110,6 +110,7 @@ import LogoMonogram from '../assets/svg/logo-monogram.svg'; import Logo from '../assets/svg/logo.svg'; import IconManageColor from '../assets/svg/manage-color.svg'; import IconMinus from '../assets/svg/minus.svg'; +import IconMlModal from '../assets/svg/mlmodal.svg'; import IconPaperPlanePrimary from '../assets/svg/paper-plane-primary.svg'; import IconPaperPlane from '../assets/svg/paper-plane.svg'; import IconPendingBadge from '../assets/svg/pending-badge.svg'; @@ -238,6 +239,7 @@ export const Icons = { EXTERNAL_LINK_GREY: 'external-link-grey', PROFILER: 'icon-profiler', PIPELINE: 'pipeline', + MLMODAL: 'mlmodal', PIPELINE_GREY: 'pipeline-grey', DBTMODEL_GREY: 'dbtmodel-grey', DBTMODEL_LIGHT_GREY: 'dbtmodel-light-grey', @@ -570,6 +572,10 @@ const SVGIcons: FunctionComponent = ({ case Icons.TOPIC: IconComponent = IconTopic; + break; + case Icons.MLMODAL: + IconComponent = IconMlModal; + break; case Icons.DASHBOARD: IconComponent = IconDashboard; diff --git a/openmetadata-ui/src/main/resources/ui/tailwind.config.js b/openmetadata-ui/src/main/resources/ui/tailwind.config.js index d26842f6ede..c71ee154597 100644 --- a/openmetadata-ui/src/main/resources/ui/tailwind.config.js +++ b/openmetadata-ui/src/main/resources/ui/tailwind.config.js @@ -173,6 +173,7 @@ module.exports = { 'screen-xxl': '2160px', 'full-hd': '1080px', 600: '600px', + 700: '700px', }, minWidth: { badgeCount: '30px',