From e6efae82ae20660f60e505a8377214660f057507 Mon Sep 17 00:00:00 2001 From: Lucas Fernandez Date: Mon, 18 Sep 2023 15:59:10 +0200 Subject: [PATCH] Revamp empty state in Model Serving Global --- docs/architecture.md | 8 +--- .../integration/hooks/useFetchState.spec.ts | 17 +++---- .../clusterSettings/ClusterSettings.spec.ts | 5 +-- .../modelServing/ModelServingGlobal.spec.ts | 42 ++++++++++++----- .../ModelServingGlobal.stories.tsx | 45 +++++++++++++++++++ .../modelServing/ServingRuntimeList.spec.ts | 7 ++- .../pages/projects/ProjectDetails.spec.ts | 9 ++-- .../pages/projects/ProjectView.spec.ts | 13 ++---- frontend/src/__tests__/integration/utils.ts | 2 + .../screens/global/EmptyModelServing.tsx | 26 +++++++---- frontend/tsconfig.json | 3 +- 11 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 frontend/src/__tests__/integration/utils.ts diff --git a/docs/architecture.md b/docs/architecture.md index 564a3af313..7e10452388 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -196,9 +196,7 @@ export const EditModel = { import { test, expect } from '@playwright/test'; test('Create project', async ({ page }) => { - await page.goto( - './iframe.html?id=tests-stories-pages-projects-projectview--create-project&viewMode=story', - ); + await page.goto(navigateToStory('projects-projectview', 'create-project')); // wait for page to load await page.waitForSelector('text=Create data science project'); @@ -217,9 +215,7 @@ test('Create project', async ({ page }) => { To run storybook UI: `cd ./frontend && npm run storybook` ```ts -await page.goto( - './iframe.html?id=tests-stories-pages-projects-projectview--create-project&viewMode=story', -); +await page.goto(navigateToStory('projects-projectview', 'create-project')); ``` 6. Wait for the page to load and the story to settle before performing any assertions or actions. Use `page.waitForSelector()` to wait for a specific element to appear as an indication of the story being loaded. diff --git a/frontend/src/__tests__/integration/hooks/useFetchState.spec.ts b/frontend/src/__tests__/integration/hooks/useFetchState.spec.ts index 940f18088e..eae447c826 100644 --- a/frontend/src/__tests__/integration/hooks/useFetchState.spec.ts +++ b/frontend/src/__tests__/integration/hooks/useFetchState.spec.ts @@ -1,9 +1,8 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; test('Success', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-hooks-usefetchstate--success&viewMode=story', - ); + await page.goto(navigateToStory('hooks-usefetchstate', 'success')); // wait 2 seconds to settle await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -16,9 +15,7 @@ test('Success', async ({ page }) => { }); test('Failure', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-hooks-usefetchstate--failure&viewMode=story', - ); + await page.goto(navigateToStory('hooks-usefetchstate', 'failure')); // wait 2 seconds to settle await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -34,9 +31,7 @@ test('Failure', async ({ page }) => { }); test('Stable', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-hooks-usefetchstate--stable&viewMode=story', - ); + await page.goto(navigateToStory('hooks-usefetchstate', 'stable')); // wait 2 seconds to settle await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -76,9 +71,7 @@ test('Stable', async ({ page }) => { }); test('Refresh rate', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-hooks-usefetchstate--refresh-rate&viewMode=story', - ); + await page.goto(navigateToStory('hooks-usefetchstate', 'refresh-rate')); // wait 2 seconds to settle await new Promise((resolve) => setTimeout(resolve, 2000)); diff --git a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts index 0a68a8faca..b4b861499a 100644 --- a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts +++ b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.spec.ts @@ -1,9 +1,8 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; test('Cluster settings', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-clustersettings-clustersettings--default&viewMode=story', - ); + await page.goto(navigateToStory('pages-clustersettings-clustersettings', 'default')); // wait for page to load await page.waitForSelector('text=Save changes'); const submitButton = page.locator('[data-id="submit-cluster-settings"]'); diff --git a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.spec.ts b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.spec.ts index dee2ce86ae..7a8aa986e6 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.spec.ts +++ b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.spec.ts @@ -1,10 +1,36 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; -test('Delete model', async ({ page }) => { +test('Empty State No Serving Runtime', async ({ page }) => { + await page.goto( + navigateToStory('pages-modelserving-modelservingglobal', 'empty-state-no-serving-runtime'), + ); + + // wait for page to load + await page.waitForSelector('text=No deployed models yet'); + + // Test that the button is enabled + await expect(page.getByRole('button', { name: 'Go to the Projects page' })).toBeTruthy(); +}); + +test('Empty State No Inference Service', async ({ page }) => { await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-modelservingglobal--delete-model&viewMode=story', + navigateToStory('pages-modelserving-modelservingglobal', 'empty-state-no-inference-service'), ); + // wait for page to load + await page.waitForSelector('text=No deployed models'); + + // Test that the button is enabled + await page.getByRole('button', { name: 'Deploy model' }).click(); + + // test that you can not submit on empty + await expect(await page.getByRole('button', { name: 'Deploy' })).toBeDisabled(); +}); + +test('Delete model', async ({ page }) => { + await page.goto(navigateToStory('pages-modelserving-modelservingglobal', 'delete-model')); + // wait for page to load await page.waitForSelector('text=Delete deployed model?'); @@ -19,9 +45,7 @@ test('Delete model', async ({ page }) => { }); test('Edit model', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-modelservingglobal--edit-model&viewMode=story', - ); + await page.goto(navigateToStory('pages-modelserving-modelservingglobal', 'edit-model')); // wait for page to load await page.waitForSelector('text=Deploy model'); @@ -53,9 +77,7 @@ test('Edit model', async ({ page }) => { }); test('Create model', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-modelservingglobal--deploy-model&viewMode=story', - ); + await page.goto(navigateToStory('pages-modelserving-modelservingglobal', 'deploy-model')); // wait for page to load await page.waitForSelector('text=Deploy model'); @@ -94,9 +116,7 @@ test('Create model', async ({ page }) => { }); test('Create model error', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-modelservingglobal--deploy-model&viewMode=story', - ); + await page.goto(navigateToStory('pages-modelserving-modelservingglobal', 'deploy-model')); // wait for page to load await page.waitForSelector('text=Deploy model'); diff --git a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx index 488b034124..348d608ea5 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx +++ b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx @@ -63,6 +63,51 @@ const Template: StoryFn = (args) => ( ); +export const EmptyStateNoServingRuntime: StoryObj = { + render: Template, + + parameters: { + msw: { + handlers: [ + rest.get( + 'api/k8s/apis/serving.kserve.io/v1alpha1/namespaces/test-project/servingruntimes', + (req, res, ctx) => res(ctx.json(mockK8sResourceList([]))), + ), + rest.get( + 'api/k8s/apis/serving.kserve.io/v1beta1/namespaces/test-project/inferenceservices', + (req, res, ctx) => res(ctx.json(mockK8sResourceList([]))), + ), + rest.get('/api/k8s/apis/project.openshift.io/v1/projects', (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockProjectK8sResource({})]))), + ), + ], + }, + }, +}; + +export const EmptyStateNoInferenceServices: StoryObj = { + render: Template, + + parameters: { + msw: { + handlers: [ + rest.get( + 'api/k8s/apis/serving.kserve.io/v1alpha1/namespaces/test-project/servingruntimes', + (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockServingRuntimeK8sResource({})]))), + ), + rest.get( + 'api/k8s/apis/serving.kserve.io/v1beta1/namespaces/test-project/inferenceservices', + (req, res, ctx) => res(ctx.json(mockK8sResourceList([]))), + ), + rest.get('/api/k8s/apis/project.openshift.io/v1/projects', (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockProjectK8sResource({})]))), + ), + ], + }, + }, +}; + export const EditModel: StoryObj = { render: Template, diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts index 486bc8233c..585f794d0d 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts @@ -1,9 +1,8 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; test('Deploy model', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-servingruntimelist--deploy-model&viewMode=story', - ); + await page.goto(navigateToStory('pages-modelserving-servingruntimelist', 'deploy-model')); // wait for page to load await page.waitForSelector('text=Deploy model'); @@ -38,7 +37,7 @@ test('Deploy model', async ({ page }) => { test('Legacy Serving Runtime', async ({ page }) => { await page.goto( - './iframe.html?args=&id=tests-integration-pages-modelserving-servingruntimelist--list-available-models&viewMode=story', + navigateToStory('pages-modelserving-servingruntimelist', 'list-available-models'), ); // wait for page to load diff --git a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts index bb17017d8e..a89ca6bf86 100644 --- a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts +++ b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts @@ -1,9 +1,8 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; test('Empty project', async ({ page }) => { - await page.goto( - './iframe.html?args=&id=tests-integration-pages-projects-projectdetails--empty-details-page&viewMode=story', - ); + await page.goto(navigateToStory('pages-projects-projectdetails', 'empty-details-page')); // wait for page to load await page.waitForSelector('text=No model servers'); @@ -16,9 +15,7 @@ test('Empty project', async ({ page }) => { }); test('Non-empty project', async ({ page }) => { - await page.goto( - './iframe.html?id=tests-integration-pages-projects-projectdetails--default&viewMode=story', - ); + await page.goto(navigateToStory('pages-projects-projectdetails', 'default')); // wait for page to load await page.waitForSelector('text=Test Notebook'); diff --git a/frontend/src/__tests__/integration/pages/projects/ProjectView.spec.ts b/frontend/src/__tests__/integration/pages/projects/ProjectView.spec.ts index 7f4800427b..3ded1a4dd2 100644 --- a/frontend/src/__tests__/integration/pages/projects/ProjectView.spec.ts +++ b/frontend/src/__tests__/integration/pages/projects/ProjectView.spec.ts @@ -1,9 +1,8 @@ import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; test('Create project', async ({ page }) => { - await page.goto( - './iframe.html?id=tests-integration-pages-projects-projectview--create-project&viewMode=story', - ); + await page.goto(navigateToStory('pages-projects-projectview', 'create-project')); // wait for page to load await page.waitForSelector('text=Create data science project'); @@ -52,9 +51,7 @@ test('Create project', async ({ page }) => { }); test('Edit project', async ({ page }) => { - await page.goto( - './iframe.html?id=tests-integration-pages-projects-projectview--edit-project&viewMode=story', - ); + await page.goto(navigateToStory('pages-projects-projectview', 'edit-project')); // wait for page to load await page.waitForSelector('text=Edit data science project'); @@ -71,9 +68,7 @@ test('Edit project', async ({ page }) => { }); test('Delete project', async ({ page }) => { - await page.goto( - './iframe.html?id=tests-integration-pages-projects-projectview--delete-project&viewMode=story', - ); + await page.goto(navigateToStory('pages-projects-projectview', 'delete-project')); // wait for page to load await page.waitForSelector('text=Delete project?'); diff --git a/frontend/src/__tests__/integration/utils.ts b/frontend/src/__tests__/integration/utils.ts new file mode 100644 index 0000000000..6c7a2a8f22 --- /dev/null +++ b/frontend/src/__tests__/integration/utils.ts @@ -0,0 +1,2 @@ +export const navigateToStory = (folder: string, storyId: string) => + `./iframe.html?args=&id=tests-integration-${folder}--${storyId}&viewMode=story`; diff --git a/frontend/src/pages/modelServing/screens/global/EmptyModelServing.tsx b/frontend/src/pages/modelServing/screens/global/EmptyModelServing.tsx index 4bd7471bd9..afe0a54481 100644 --- a/frontend/src/pages/modelServing/screens/global/EmptyModelServing.tsx +++ b/frontend/src/pages/modelServing/screens/global/EmptyModelServing.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; -import { Button, EmptyState, EmptyStateBody, EmptyStateIcon, Title } from '@patternfly/react-core'; -import { PlusCircleIcon } from '@patternfly/react-icons'; +import { + Button, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, + Title, +} from '@patternfly/react-core'; +import { PlusCircleIcon, WrenchIcon } from '@patternfly/react-icons'; import { useNavigate } from 'react-router-dom'; import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; import ServeModelButton from './ServeModelButton'; @@ -13,16 +20,17 @@ const EmptyModelServing: React.FC = () => { if (servingRuntimes.length === 0) { return ( - - + + - No model servers + No deployed models yet - Before deploying a model, you must first configure a model server. + To get started, deploy a model from the Models and model servers section + of a project. - ); @@ -32,7 +40,7 @@ const EmptyModelServing: React.FC = () => { - No deployed models. + No deployed models To get started, use existing model servers to serve a model. diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 32c295a529..e584ca32d3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -16,8 +16,9 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, + "baseUrl": "./src", "paths": { - "~/*": ["./src/*"] + "~/*": ["./*"] }, "importHelpers": true, "skipLibCheck": true