Repository: opendatahub-io/odh-dashboard
Pull request #4144: Base model serving package extension
Author: login: emilys314
Labels: name: lgtm
name: approved
Created: 2025-05-02T14:28:25Z | Merged: 2025-05-05T22:53:45Z

<!--- If this is a non-code change, this template is not required; reference any issues or top-level descriptions as needed -->
<!--- All code change PRs should relate to an issue, reference it here; see example below -->
<!--- https://issues.redhat.com/browse/RHOAIENG-123456 -->

Depends on https://github.com/opendatahub-io/odh-dashboard/pull/4138

Closes https://issues.redhat.com/browse/RHOAIENG-24577

## Description
<!--- Describe your changes in detail; the what, the why, any findings, etc -->
<!--- Include any screenshots of changed UI; Include any gifs if it was a flow / UX change -->
Set's up the `frontend/packages/modelServing` with a simple obj for the project details tab, and is toggled with the `disableModelServingPlugin` flag. 

![image](https://github.com/user-attachments/assets/a6fbe131-20fb-4282-aabf-afb3ea416260)


## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
Turn on `disableModelServingPlugin` to `false`, navigate to the models projecte details tab and see some boilerplate text

## Test Impact
<!--- What tests have you done to cover the implemented functionality -->
<!--- If tests are not applicable, explain why here -->
none yet, it's dev only boilerplate and is defaulted to disabled

## Request review criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->

Self checklist (all need to be checked):
- [X] The developer has manually tested the changes and verified that the changes work
- [X] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [X] The developer has added tests or explained why testing cannot be added (unit or cypress tests for related changes)

If you have UI changes: 
<!--- You can ignore these if you are doing manifest, backend, internal logic, etc changes; aka non-UI / visual changes -->
- [ ] Included any necessary screenshots or gifs if it was a UI change.
- [ ] Included tags to the UX team if it was a UI/UX change.

After the PR is posted & before it merges:
- [ ] The developer has tested their solution on a cluster by using the image produced by the PR to `main`



Diff:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 753edb931f..2812f7426c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,6 +14,8 @@
       ],
       "dependencies": {
         "@monaco-editor/react": "^4.6.0",
+        "@odh-dashboard/model-serving": "0.0.0",
+        "@odh-dashboard/plugin-core": "0.0.0",
         "@openshift/dynamic-plugin-sdk": "^5.0.1",
         "@openshift/dynamic-plugin-sdk-utils": "^5.0.1",
         "@patternfly/patternfly": "^6.2.0",
@@ -4485,10 +4487,18 @@
       "resolved": "src",
       "link": true
     },
+    "node_modules/@odh-dashboard/model-serving": {
+      "resolved": "packages/modelServing",
+      "link": true
+    },
     "node_modules/@odh-dashboard/plugin-core": {
       "resolved": "packages/plugin-core",
       "link": true
     },
+    "node_modules/@odh-dashboard/template": {
+      "resolved": "packages/template",
+      "link": true
+    },
     "node_modules/@odh-dashboard/tsconfig": {
       "resolved": "packages/tsconfig",
       "link": true
@@ -26998,10 +27008,18 @@
         "url": "https://github.com/sponsors/colinhacks"
       }
     },
+    "packages/modelServing": {
+      "name": "@odh-dashboard/model-serving",
+      "version": "0.0.0"
+    },
     "packages/plugin-core": {
       "name": "@odh-dashboard/plugin-core",
       "version": "0.0.0"
     },
+    "packages/template": {
+      "name": "@odh-dashboard/template",
+      "version": "0.0.0"
+    },
     "packages/tsconfig": {
       "name": "@odh-dashboard/tsconfig",
       "version": "0.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index 7413a9ef2f..f6f804d814 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -61,6 +61,7 @@
   "dependencies": {
     "@monaco-editor/react": "^4.6.0",
     "@odh-dashboard/plugin-core": "0.0.0",
+    "@odh-dashboard/model-serving": "0.0.0",
     "@openshift/dynamic-plugin-sdk": "^5.0.1",
     "@openshift/dynamic-plugin-sdk-utils": "^5.0.1",
     "@patternfly/patternfly": "^6.2.0",
diff --git a/frontend/packages/modelServing/package.json b/frontend/packages/modelServing/package.json
new file mode 100644
index 0000000000..ec5a16d9af
--- /dev/null
+++ b/frontend/packages/modelServing/package.json
@@ -0,0 +1,10 @@
+{
+  "private": true,
+  "name": "@odh-dashboard/model-serving",
+  "description": "Plugin to display model deployments. Used in combination with specific model serving platform extensions.",
+  "version": "0.0.0",
+  "exports": {
+    ".": "./src/extensions.ts",
+    "./extension-points": "./src/extension-points/index.ts"
+  }
+}
\ No newline at end of file
diff --git a/frontend/packages/modelServing/src/components/ModelsProjectDetailsTab.tsx b/frontend/packages/modelServing/src/components/ModelsProjectDetailsTab.tsx
new file mode 100644
index 0000000000..953ac4c97c
--- /dev/null
+++ b/frontend/packages/modelServing/src/components/ModelsProjectDetailsTab.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const ModelsProjectDetailsTab: React.FC = () => (
+  <>Model tab content. Platform selection, and view models table</>
+);
+
+export default ModelsProjectDetailsTab;
diff --git a/frontend/packages/modelServing/src/extension-points/index.ts b/frontend/packages/modelServing/src/extension-points/index.ts
new file mode 100644
index 0000000000..4e358e325a
--- /dev/null
+++ b/frontend/packages/modelServing/src/extension-points/index.ts
@@ -0,0 +1,11 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+
+export type ModelServingPlatform = Extension<
+  'model-serving.platform',
+  {
+    id: string;
+    name: string;
+  }
+>;
+export const isModelServingPlatform = (extension: Extension): extension is ModelServingPlatform =>
+  extension.type === 'model-serving.platform';
diff --git a/frontend/packages/modelServing/src/extensions.ts b/frontend/packages/modelServing/src/extensions.ts
new file mode 100644
index 0000000000..92f166ff24
--- /dev/null
+++ b/frontend/packages/modelServing/src/extensions.ts
@@ -0,0 +1,20 @@
+import type { ProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+// Allow this import as it consists of types and enums only.
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { SupportedArea } from '@odh-dashboard/internal/concepts/areas/types';
+
+const extensions: ProjectDetailsTab[] = [
+  {
+    type: 'app.project-details/tab',
+    properties: {
+      id: 'model-server', // same value as ProjectSectionID.MODEL_SERVER
+      title: 'Models',
+      component: () => import('./components/ModelsProjectDetailsTab'),
+    },
+    flags: {
+      required: [SupportedArea.PLUGIN_MODEL_SERVING],
+    },
+  },
+];
+
+export default extensions;
diff --git a/frontend/packages/modelServing/tsconfig.json b/frontend/packages/modelServing/tsconfig.json
new file mode 100644
index 0000000000..186318a0ed
--- /dev/null
+++ b/frontend/packages/modelServing/tsconfig.json
@@ -0,0 +1,3 @@
+{
+  "extends": "@odh-dashboard/tsconfig/tsconfig.json"
+}
diff --git a/frontend/packages/plugin-core/src/extension-points/index.ts b/frontend/packages/plugin-core/src/extension-points/index.ts
index f7eca72e8c..0eaf8261de 100644
--- a/frontend/packages/plugin-core/src/extension-points/index.ts
+++ b/frontend/packages/plugin-core/src/extension-points/index.ts
@@ -1,5 +1,6 @@
 export * from './navigation';
 export * from './status-provider';
+export * from './project-details';
 
 /**
  * ## Extension Point Definitions
diff --git a/frontend/packages/plugin-core/src/extension-points/project-details.tsx b/frontend/packages/plugin-core/src/extension-points/project-details.tsx
new file mode 100644
index 0000000000..1a34c095c5
--- /dev/null
+++ b/frontend/packages/plugin-core/src/extension-points/project-details.tsx
@@ -0,0 +1,14 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+import { ComponentCodeRef } from './types';
+
+export type ProjectDetailsTab = Extension<
+  'app.project-details/tab',
+  {
+    id: string;
+    title: string;
+    component: ComponentCodeRef;
+    // weight: num;
+  }
+>;
+export const isProjectDetailsTab = (extension: Extension): extension is ProjectDetailsTab =>
+  extension.type === 'app.project-details/tab';
diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts
index cefbb4cb4f..b0403812c1 100644
--- a/frontend/src/__mocks__/mockDashboardConfig.ts
+++ b/frontend/src/__mocks__/mockDashboardConfig.ts
@@ -228,6 +228,7 @@ export const mockDashboardConfig = ({
       disableNIMModelServing,
       disableAdminConnectionTypes: false,
       disableFineTuning,
+      disableModelServingPlugin: true,
     },
     notebookController: {
       enabled: !disableNotebookController,
diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts
index aae8102a35..f40552e9dc 100644
--- a/frontend/src/concepts/areas/const.ts
+++ b/frontend/src/concepts/areas/const.ts
@@ -42,6 +42,7 @@ export const allFeatureFlags: string[] = Object.keys({
   disableNIMModelServing: false,
   disableAdminConnectionTypes: false,
   disableFineTuning: true,
+  disableModelServingPlugin: true, // internal dev only
 } satisfies DashboardCommonConfig);
 
 export const SupportedAreasStateMap: SupportedAreasState = {
@@ -167,6 +168,10 @@ export const SupportedAreasStateMap: SupportedAreasState = {
       SupportedArea.MODEL_REGISTRY,
     ],
   },
+  [SupportedArea.PLUGIN_MODEL_SERVING]: {
+    featureFlags: ['disableModelServingPlugin'],
+    reliantAreas: [SupportedArea.MODEL_SERVING],
+  },
 };
 
 /** Maps each DataScienceStackComponent to its human-readable name **/
diff --git a/frontend/src/concepts/areas/types.ts b/frontend/src/concepts/areas/types.ts
index 421c72f78c..5d2932beab 100644
--- a/frontend/src/concepts/areas/types.ts
+++ b/frontend/src/concepts/areas/types.ts
@@ -70,6 +70,9 @@ export enum SupportedArea {
 
   /* Model catalog areas */
   MODEL_CATALOG = 'model-catalog',
+
+  /* Plugins */
+  PLUGIN_MODEL_SERVING = 'plugin-model-serving',
 }
 
 /** Components deployed by the Operator. Part of the DSC Status. */
diff --git a/frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx b/frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx
new file mode 100644
index 0000000000..a57a15bbb7
--- /dev/null
+++ b/frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { LazyCodeRefComponent, useExtensions } from '@odh-dashboard/plugin-core';
+import { isProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled';
+import { SectionDefinition } from '~/pages/projects/components/GenericHorizontalBar';
+import { ProjectSectionID } from '~/pages/projects/screens/detail/types';
+import ModelServingPlatform from '~/pages/modelServing/screens/projects/ModelServingPlatform';
+
+export const useModelServingTab = (): SectionDefinition[] => {
+  const modelServingEnabled = useModelServingEnabled();
+
+  const projectDetailsTabExtensions = useExtensions(isProjectDetailsTab);
+  const modelsProjectDetailsTab = projectDetailsTabExtensions.find(
+    (tab) => tab.properties.id === ProjectSectionID.MODEL_SERVER,
+  )?.properties.component;
+
+  const tab: SectionDefinition[] = modelServingEnabled
+    ? [
+        {
+          id: ProjectSectionID.MODEL_SERVER,
+          title: 'Models',
+          component: modelsProjectDetailsTab ? (
+            <LazyCodeRefComponent component={modelsProjectDetailsTab} />
+          ) : (
+            <ModelServingPlatform />
+          ),
+        },
+      ]
+    : [];
+
+  return tab;
+};
diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts
index abd8afe46c..d2a6c5a811 100644
--- a/frontend/src/k8sTypes.ts
+++ b/frontend/src/k8sTypes.ts
@@ -1223,6 +1223,7 @@ export type DashboardCommonConfig = {
   disableNIMModelServing: boolean;
   disableAdminConnectionTypes: boolean;
   disableFineTuning: boolean;
+  disableModelServingPlugin: boolean;
 };
 
 export type DashboardConfigKind = K8sResourceCommon & {
diff --git a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
index aeb41bfc80..9da2ea54af 100644
--- a/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
+++ b/frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
@@ -1,14 +1,13 @@
 import * as React from 'react';
 import { Breadcrumb, BreadcrumbItem, Flex, FlexItem, Truncate } from '@patternfly/react-core';
 import { Link, useSearchParams } from 'react-router-dom';
+import { useModelServingTab } from '~/concepts/projects/projectDetails/useModelServingTab';
 import ApplicationsPage from '~/pages/ApplicationsPage';
 import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext';
 import GenericHorizontalBar from '~/pages/projects/components/GenericHorizontalBar';
 import ProjectSharing from '~/pages/projects/projectSharing/ProjectSharing';
 import ProjectSettingsPage from '~/pages/projects/projectSettings/ProjectSettingsPage';
 import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas';
-import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled';
-import ModelServingPlatform from '~/pages/modelServing/screens/projects/ModelServingPlatform';
 import { ProjectObjectType, SectionType } from '~/concepts/design/utils';
 import { ProjectSectionID } from '~/pages/projects/screens/detail/types';
 import { AccessReviewResourceAttributes } from '~/k8sTypes';
@@ -39,7 +38,7 @@ const ProjectDetails: React.FC = () => {
   const biasMetricsAreaAvailable = useIsAreaAvailable(SupportedArea.BIAS_METRICS).status;
   const projectSharingEnabled = useIsAreaAvailable(SupportedArea.DS_PROJECTS_PERMISSIONS).status;
   const pipelinesEnabled = useIsAreaAvailable(SupportedArea.DS_PIPELINES).status;
-  const modelServingEnabled = useModelServingEnabled();
+  const modelServingTab = useModelServingTab();
   const [searchParams, setSearchParams] = useSearchParams();
   const state = searchParams.get('section');
   const [allowCreate, rbacLoaded] = useAccessReview({
@@ -106,15 +105,7 @@ const ProjectDetails: React.FC = () => {
                   },
                 ]
               : []),
-            ...(modelServingEnabled
-              ? [
-                  {
-                    id: ProjectSectionID.MODEL_SERVER,
-                    title: 'Models',
-                    component: <ModelServingPlatform />,
-                  },
-                ]
-              : []),
+            ...modelServingTab,
             {
               id: ProjectSectionID.CLUSTER_STORAGES,
               title: 'Cluster storage',
@@ -147,10 +138,10 @@ const ProjectDetails: React.FC = () => {
           [
             allowCreate,
             biasMetricsAreaAvailable,
-            modelServingEnabled,
             pipelinesEnabled,
             projectSharingEnabled,
             workbenchEnabled,
+            modelServingTab,
           ],
         )}
       />
diff --git a/frontend/src/plugins/extensions.ts b/frontend/src/plugins/extensions.ts
index 3bee06f4af..62b390f4ab 100644
--- a/frontend/src/plugins/extensions.ts
+++ b/frontend/src/plugins/extensions.ts
@@ -1,7 +1,12 @@
 import type { Extension } from '@openshift/dynamic-plugin-sdk';
+import modelServingExtensions from '@odh-dashboard/model-serving';
 import navigationExtensions from './extensions/navigation';
 import hardwareProfileExtensions from './extensions/hardware-profiles';
 
-const extensions: Extension[] = [...navigationExtensions, ...hardwareProfileExtensions];
+const extensions: Extension[] = [
+  ...modelServingExtensions,
+  ...navigationExtensions,
+  ...hardwareProfileExtensions,
+];
 
 export default extensions;


Reviews:
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:34:15Z
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:35:57Z
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:36:57Z
user: login: christianvogt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T14:38:33Z
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:38:56Z
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T14:40:40Z
user: login: christianvogt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T14:49:17Z
user: login: christianvogt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T14:54:32Z
user: login: andrewballantyne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T15:07:24Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:47:16Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:47:39Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:48:30Z
user: login: christianvogt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T16:30:00Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:56:27Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:57:44Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T18:20:20Z

Review comments:
diff_hunk: @@ -0,0 +1,11 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+
+export type ModelServingPlatform = Extension<
+  'app.model-serving/platform',
+  {
+    id: string;
+    name: string;
+  }
+>;
+export const isModelServingPlatform = (extension: Extension): extension is ModelServingPlatform =>
+  extension.type === 'app.model-serving/platform';
path: frontend/packages/modelServing/src/extension-points/index.ts
user: login: andrewballantyne
body: Extensions are namespaced to the source of the extension point -- in this case, you'd not use `app.` -- you'd use `model-serving.` (or something equiv)
created_at: 2025-05-02T14:34:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,10 @@
+{
+  "private": true,
+  "name": "@odh-dashboard/model-serving",
+  "description": "Plugin to display model deployments. Used in combination with specific model serving platform extensions.",
+  "version": "0.0.0",
+  "exports": {
+    ".": "./src/extensions.ts",
+    "./extension-points": "./src/extension-points/index.ts"
path: frontend/packages/modelServing/package.json
user: login: andrewballantyne
body: @christianvogt no way to generate this effort? We are doing `() => import('...')` seems kinda wasteful to need to do both. Future goal?
created_at: 2025-05-02T14:35:57Z
author_association: MEMBER
diff_hunk: @@ -70,6 +70,9 @@ export enum SupportedArea {
 
   /* Model catalog areas */
   MODEL_CATALOG = 'model-catalog',
+
+  /* Extensions */
+  MODEL_SERVING_EXTENSION = 'model-serving-extension',
path: frontend/src/concepts/areas/types.ts
user: login: andrewballantyne
body: @christianvogt do we have a good name for this? What are our terms?

* Extension Points
* Extensions
* Plugins

These all valid?
created_at: 2025-05-02T14:36:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,11 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+
+export type ModelServingPlatform = Extension<
+  'app.model-serving/platform',
+  {
+    id: string;
+    name: string;
+  }
+>;
+export const isModelServingPlatform = (extension: Extension): extension is ModelServingPlatform =>
+  extension.type === 'app.model-serving/platform';
path: frontend/packages/modelServing/src/extension-points/index.ts
user: login: christianvogt
body: eg `model-serving.platform` the portion after `/` is used when creating multiple related extensions.
created_at: 2025-05-02T14:38:33Z
author_association: COLLABORATOR
diff_hunk: @@ -111,7 +119,11 @@ const ProjectDetails: React.FC = () => {
                   {
                     id: ProjectSectionID.MODEL_SERVER,
                     title: 'Models',
-                    component: <ModelServingPlatform />,
+                    component: ModelsProjectDetailsTab ? (
+                      <ModelsProjectDetailsTab />
+                    ) : (
+                      <ModelServingPlatform />
+                    ),
                   },
path: frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
user: login: andrewballantyne
body: For sanity / separation reasons -- can we make all of this "model serving" section be a hook that determines if its enabled, what state it is in, and returns the array out of the hook? 

I fear mudding this up significantly as we move through the app -- hiding our "extension flag" seems like a good idea and that way when we still have to bug fix it's not so "noisy" everywhere.
created_at: 2025-05-02T14:38:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,11 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+
+export type ModelServingPlatform = Extension<
+  'app.model-serving/platform',
+  {
+    id: string;
+    name: string;
+  }
+>;
+export const isModelServingPlatform = (extension: Extension): extension is ModelServingPlatform =>
+  extension.type === 'app.model-serving/platform';
path: frontend/packages/modelServing/src/extension-points/index.ts
user: login: andrewballantyne
body: Yeah, sub-sections are optional -- you don't need to use them if it's a simple concept
created_at: 2025-05-02T14:40:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,10 @@
+{
+  "private": true,
+  "name": "@odh-dashboard/model-serving",
+  "description": "Plugin to display model deployments. Used in combination with specific model serving platform extensions.",
+  "version": "0.0.0",
+  "exports": {
+    ".": "./src/extensions.ts",
+    "./extension-points": "./src/extension-points/index.ts"
path: frontend/packages/modelServing/package.json
user: login: christianvogt
body: Not exactly sure what you mean by this, but the package must export the extensions to be consumed by the main app at this time since we don't have any build plugin to do it for us.
created_at: 2025-05-02T14:49:17Z
author_association: COLLABORATOR
diff_hunk: @@ -70,6 +70,9 @@ export enum SupportedArea {
 
   /* Model catalog areas */
   MODEL_CATALOG = 'model-catalog',
+
+  /* Extensions */
+  MODEL_SERVING_EXTENSION = 'model-serving-extension',
path: frontend/src/concepts/areas/types.ts
user: login: christianvogt
body: Probably should refer to the plugin as a whole.
created_at: 2025-05-02T14:54:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+{
+  "private": true,
+  "name": "@odh-dashboard/model-serving",
+  "description": "Plugin to display model deployments. Used in combination with specific model serving platform extensions.",
+  "version": "0.0.0",
+  "exports": {
+    ".": "./src/extensions.ts",
+    "./extension-points": "./src/extension-points/index.ts"
path: frontend/packages/modelServing/package.json
user: login: andrewballantyne
body: Definitely my mistake on what I was reading here... please ignore.
created_at: 2025-05-02T15:07:23Z
author_association: MEMBER
diff_hunk: @@ -111,7 +119,11 @@ const ProjectDetails: React.FC = () => {
                   {
                     id: ProjectSectionID.MODEL_SERVER,
                     title: 'Models',
-                    component: <ModelServingPlatform />,
+                    component: ModelsProjectDetailsTab ? (
+                      <ModelsProjectDetailsTab />
+                    ) : (
+                      <ModelServingPlatform />
+                    ),
                   },
path: frontend/src/pages/projects/screens/detail/ProjectDetails.tsx
user: login: emilys314
body: Moved it all into `frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx`
created_at: 2025-05-02T15:47:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,11 @@
+import { Extension } from '@openshift/dynamic-plugin-sdk';
+
+export type ModelServingPlatform = Extension<
+  'app.model-serving/platform',
+  {
+    id: string;
+    name: string;
+  }
+>;
+export const isModelServingPlatform = (extension: Extension): extension is ModelServingPlatform =>
+  extension.type === 'app.model-serving/platform';
path: frontend/packages/modelServing/src/extension-points/index.ts
user: login: emilys314
body: renamed to `model-serving.platform`
created_at: 2025-05-02T15:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,6 +70,9 @@ export enum SupportedArea {
 
   /* Model catalog areas */
   MODEL_CATALOG = 'model-catalog',
+
+  /* Extensions */
+  MODEL_SERVING_EXTENSION = 'model-serving-extension',
path: frontend/src/concepts/areas/types.ts
user: login: emilys314
body: Changed to ` PLUGIN_MODEL_SERVING = 'plugin-model-serving',`  and `disableModelServingPlugin`
created_at: 2025-05-02T15:48:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,32 @@
+import React from 'react';
+import { useResolvedExtensions } from '@odh-dashboard/plugin-core';
+import { isProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled';
+import { SectionDefinition } from '~/pages/projects/components/GenericHorizontalBar';
+import { ProjectSectionID } from '~/pages/projects/screens/detail/types';
+import ModelServingPlatform from '~/pages/modelServing/screens/projects/ModelServingPlatform';
+
+export const useModelServingTab = (): SectionDefinition[] => {
+  const modelServingEnabled = useModelServingEnabled();
+
+  const [projectDetailsTabExtensions] = useResolvedExtensions(isProjectDetailsTab);
+  const ModelsProjectDetailsTab = projectDetailsTabExtensions.find(
+    (tab) => tab.properties.id === ProjectSectionID.MODEL_SERVER,
+  )?.properties.component.default;
+
+  const tab: SectionDefinition[] = modelServingEnabled
+    ? [
+        {
+          id: ProjectSectionID.MODEL_SERVER,
+          title: 'Models',
+          component: ModelsProjectDetailsTab ? (
+            <ModelsProjectDetailsTab />
+          ) : (
+            <ModelServingPlatform />
+          ),
+        },
+      ]
+    : [];
path: frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx
user: login: christianvogt
body: We'll likely refactor this before we get more extensions however we typically want to avoid resolving all extensions unless absolutely necessary at the time of processing.

```suggestion
  const [projectDetailsTabExtensions] = useExtensions(isProjectDetailsTab);
  const modelsProjectDetailsTab = projectDetailsTabExtensions.find(
    (tab) => tab.properties.id === ProjectSectionID.MODEL_SERVER,
  )?.properties.component;

  const tab: SectionDefinition[] = modelServingEnabled
    ? [
        {
          id: ProjectSectionID.MODEL_SERVER,
          title: 'Models',
          component: modelsProjectDetailsTab ? (
            <LazyComponentRef component={modelsProjectDetailsTab} />
          ) : (
            <ModelServingPlatform />
          ),
        },
      ]
    : [];
```
created_at: 2025-05-02T16:24:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+import type { ProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+
+const extensions: ProjectDetailsTab[] = [
+  {
+    type: 'app.project-details/tab',
+    properties: {
+      id: 'model-server', // same value as ProjectSectionID.MODEL_SERVER
+      title: 'Models',
+      component: () => import('./components/ModelsProjectDetailsTab'),
+    },
+    flags: {
+      required: ['plugin-model-serving'], // same value as SupportedArea.PLUGIN_MODEL_SERVING,
path: frontend/packages/modelServing/src/extensions.ts
user: login: christianvogt
body: You can also import this for now:

```
// Allow this import as it consists of types and enums only.
// eslint-disable-next-line no-restricted-syntax
import { SupportedArea } from '@odh-dashboard/internal/concepts/areas/types';
```
created_at: 2025-05-02T16:27:22Z
author_association: COLLABORATOR
diff_hunk: @@ -56,8 +60,10 @@
   },
   "dependencies": {
     "@monaco-editor/react": "^4.6.0",
+    "@odh-dashboard/plugin-core": "0.0.0",
+    "@odh-dashboard/model-serving": "^0.0.0",
path: frontend/package.json
user: login: christianvogt
body: These versions aren't changing. They are fixed in our monorepo.
```suggestion
    "@odh-dashboard/model-serving": "0.0.0",
```
created_at: 2025-05-02T16:28:40Z
author_association: COLLABORATOR
diff_hunk: @@ -56,8 +60,10 @@
   },
   "dependencies": {
     "@monaco-editor/react": "^4.6.0",
+    "@odh-dashboard/plugin-core": "0.0.0",
+    "@odh-dashboard/model-serving": "^0.0.0",
path: frontend/package.json
user: login: emilys314
body: Gotcha, i did notice that detail earlier but wasn't 100% sure
created_at: 2025-05-02T17:56:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,32 @@
+import React from 'react';
+import { useResolvedExtensions } from '@odh-dashboard/plugin-core';
+import { isProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled';
+import { SectionDefinition } from '~/pages/projects/components/GenericHorizontalBar';
+import { ProjectSectionID } from '~/pages/projects/screens/detail/types';
+import ModelServingPlatform from '~/pages/modelServing/screens/projects/ModelServingPlatform';
+
+export const useModelServingTab = (): SectionDefinition[] => {
+  const modelServingEnabled = useModelServingEnabled();
+
+  const [projectDetailsTabExtensions] = useResolvedExtensions(isProjectDetailsTab);
+  const ModelsProjectDetailsTab = projectDetailsTabExtensions.find(
+    (tab) => tab.properties.id === ProjectSectionID.MODEL_SERVER,
+  )?.properties.component.default;
+
+  const tab: SectionDefinition[] = modelServingEnabled
+    ? [
+        {
+          id: ProjectSectionID.MODEL_SERVER,
+          title: 'Models',
+          component: ModelsProjectDetailsTab ? (
+            <ModelsProjectDetailsTab />
+          ) : (
+            <ModelServingPlatform />
+          ),
+        },
+      ]
+    : [];
path: frontend/src/concepts/projects/projectDetails/useModelServingTab.tsx
user: login: emilys314
body: Gotcha thanks for the info, so useExtension is just loading the chunk references, but resolving it is loading the chunk itself?
created_at: 2025-05-02T17:57:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,17 @@
+import type { ProjectDetailsTab } from '@odh-dashboard/plugin-core/extension-points';
+
+const extensions: ProjectDetailsTab[] = [
+  {
+    type: 'app.project-details/tab',
+    properties: {
+      id: 'model-server', // same value as ProjectSectionID.MODEL_SERVER
+      title: 'Models',
+      component: () => import('./components/ModelsProjectDetailsTab'),
+    },
+    flags: {
+      required: ['plugin-model-serving'], // same value as SupportedArea.PLUGIN_MODEL_SERVING,
path: frontend/packages/modelServing/src/extensions.ts
user: login: emilys314
body: I had to use a different eslint-disable `// eslint-disable-next-line import/no-extraneous-dependencies` because this is in `frontend/packages` and in your AppRoutes refactor yours is still in `frontend/src/plugins` so that why i wasn't fully sure if this is still okay to do
created_at: 2025-05-02T18:20:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-05-02T14:28:29Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-02T22:09:02Z
body: ## [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 0.00%. Comparing base [(`d162969`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/d1629693c50f91f88f2de508d197ba94608e43de?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) to head [(`5205d90`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/5205d9092cf1b291499aa3b8451ec905d6ba0c82?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> Report is 3 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144/graphs/tree.svg?width=650&height=150&src=pr&token=UQM7NLQUXJ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)

```diff
@@     Coverage Diff      @@
##   main   #4144   +/-   ##
============================
============================

```

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Last update [d162969...5205d90](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4144?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: emilys314
created_at: 2025-05-05T16:33:47Z
body: /hold

need to fix rebase 
author_association: CONTRIBUTOR
user: login: emilys314
created_at: 2025-05-05T17:32:06Z
body: /unhold
author_association: CONTRIBUTOR
user: login: christianvogt
created_at: 2025-05-05T20:30:10Z
body: /lgtm
/approve
author_association: COLLABORATOR
user: login: openshift-ci[bot]
created_at: 2025-05-05T20:30:19Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/opendatahub-io/odh-dashboard/pull/4144#issuecomment-2852258781" title="Approved">christianvogt</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=opendatahub-io%2Fodh-dashboard).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/opendatahub-io/odh-dashboard/blob/main/OWNERS)~~ [christianvogt]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: emilys314
created_at: 2025-05-05T20:42:21Z
body: /hold
author_association: CONTRIBUTOR
user: login: christianvogt
created_at: 2025-05-05T20:49:40Z
body: /lgtm
author_association: COLLABORATOR
user: login: emilys314
created_at: 2025-05-05T20:50:23Z
body: /unhold
author_association: CONTRIBUTOR