Skip to content

Commit

Permalink
feat: update design when running application (#214)
Browse files Browse the repository at this point in the history
* feat: update design when running application

Signed-off-by: lstocchi <[email protected]>

* fix: add tests

Signed-off-by: lstocchi <[email protected]>

* fix: fix rounded right corners

Signed-off-by: lstocchi <[email protected]>

---------

Signed-off-by: lstocchi <[email protected]>
  • Loading branch information
lstocchi authored Feb 6, 2024
1 parent 6c57b38 commit 4d9d79b
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 54 deletions.
1 change: 1 addition & 0 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class ApplicationManager {
const podInfo = await this.createApplicationPod(images, modelPath, taskUtil);

await this.runApplication(podInfo, taskUtil);
taskUtil.setStatus('running');
}

async runApplication(podInfo: PodInfo, taskUtil: RecipeStatusUtils) {
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/lib/NavPage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ test('NavPage should not have linear progress', async () => {
expect(content).toBeDefined();
expect(content.firstChild).toBeNull(); // no slot content provided
});

test('NavPage should have custom background', async () => {
// render the component
render(NavPage, { title: 'dummy', contentBackground: 'bg-white' });

const content = await screen.findByLabelText('content');
expect(content).toBeDefined();
expect(content).toHaveClass('bg-white');
});
3 changes: 2 additions & 1 deletion packages/frontend/src/lib/NavPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export let searchTerm = '';
export let searchEnabled = true;
export let loading = false;
export let icon: IconDefinition | undefined = undefined;
export let contentBackground = '';
</script>

<div class="flex flex-col w-full h-full shadow-pageheader">
Expand Down Expand Up @@ -58,7 +59,7 @@ export let icon: IconDefinition | undefined = undefined;
<div class="flex flex-row px-2 border-b border-charcoal-400">
<slot name="tabs" />
</div>
<div class="flex w-full h-full overflow-auto" role="region" aria-label="content">
<div class="flex w-full h-full {contentBackground} overflow-auto" role="region" aria-label="content">
{#if loading}
<LinearProgress/>
{:else}
Expand Down
20 changes: 20 additions & 0 deletions packages/frontend/src/lib/table/Table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,23 @@ test('Expect overflow-hidden', async () => {
expect(cells[5]).toHaveClass('overflow-hidden');
}
});

test('Expect custom background', async () => {
render(TestTable, {
headerBackground: 'bg-white',
});

// get the 4 rows (first is header)
const rows = await screen.findAllByRole('row');
expect(rows).toBeDefined();
expect(rows[0]).toHaveClass('bg-white');
});

test('Expect default background', async () => {
render(TestTable, {});

// get the 4 rows (first is header)
const rows = await screen.findAllByRole('row');
expect(rows).toBeDefined();
expect(rows[0]).toHaveClass('bg-charcoal-700');
});
3 changes: 2 additions & 1 deletion packages/frontend/src/lib/table/Table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export let data: any[];
export let columns: Column<any>[];
export let row: Row<any>;
export let defaultSortColumn: string | undefined = undefined;
export let headerBackground = 'bg-charcoal-700';
// number of selected items in the list
export let selectedItemsNumber: number = 0;
Expand Down Expand Up @@ -125,7 +126,7 @@ function setGridColumns() {
<!-- Table header -->
<div role="rowgroup">
<div
class="grid grid-table gap-x-0.5 mx-5 h-7 sticky top-0 bg-charcoal-700 text-xs text-gray-600 font-bold uppercase z-[2]"
class="grid grid-table gap-x-0.5 mx-5 h-7 sticky top-0 {headerBackground} text-xs text-gray-600 font-bold uppercase z-[2]"
role="row">
<div class="whitespace-nowrap justify-self-start" role="columnheader"></div>
{#if row.info.selectable}
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/lib/table/TestTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SimpleColumn from './SimpleColumn.svelte';
let table: Table;
let selectedItemsNumber: number;
export let headerBackground = 'bg-charcoal-700';
type Person = {
id: number;
Expand Down Expand Up @@ -62,5 +63,6 @@ const row = new Row<Person>({
data="{people}"
columns="{columns}"
row="{row}"
defaultSortColumn="Id">
defaultSortColumn="Id"
headerBackground={headerBackground}>
</Table>
70 changes: 70 additions & 0 deletions packages/frontend/src/pages/Recipe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import '@testing-library/jest-dom/vitest';
import { vi, test, expect } from 'vitest';
import { screen, render } from '@testing-library/svelte';
import catalog from '../../../backend/src/ai-user-test.json';
import Recipe from './Recipe.svelte';
import userEvent from '@testing-library/user-event';

const mocks = vi.hoisted(() => {
return {
getCatalogMock: vi.fn(),
getPullingStatusesMock: vi.fn(),
pullApplicationMock: vi.fn(),
};
});

Expand All @@ -15,6 +18,7 @@ vi.mock('../utils/client', async () => {
studioClient: {
getCatalog: mocks.getCatalogMock,
getPullingStatuses: mocks.getPullingStatusesMock,
pullApplication: mocks.pullApplicationMock,
},
rpcBrowser: {
subscribe: () => {
Expand All @@ -40,3 +44,69 @@ test('should display recipe information', async () => {
screen.getByText(recipe!.name);
screen.getByText(recipe!.readme);
});

test('should display default model information', async () => {
const recipe = catalog.recipes.find(r => r.id === 'recipe 1');
expect(recipe).not.toBeUndefined();

mocks.getCatalogMock.mockResolvedValue(catalog);
mocks.getPullingStatusesMock.mockResolvedValue(new Map());
render(Recipe, {
recipeId: 'recipe 1',
});
await new Promise(resolve => setTimeout(resolve, 200));

const modelInfo = screen.getByLabelText('model-selected');
expect(modelInfo.textContent).equal('Model 1');
const licenseBadge = screen.getByLabelText('license-model');
expect(licenseBadge.textContent).equal('?');
const defaultWarning = screen.getByLabelText('default-model-warning');
expect(defaultWarning.textContent).contains('This is the default, recommended model for this recipe.');
});

test('should open/close application details panel when clicking on toggle button', async () => {
const recipe = catalog.recipes.find(r => r.id === 'recipe 1');
expect(recipe).not.toBeUndefined();

mocks.getCatalogMock.mockResolvedValue(catalog);
mocks.getPullingStatusesMock.mockResolvedValue(new Map());
render(Recipe, {
recipeId: 'recipe 1',
});
await new Promise(resolve => setTimeout(resolve, 200));

const panelOpenDetails = screen.getByLabelText('toggle application details');
expect(panelOpenDetails).toHaveClass('hidden');
const panelAppDetails = screen.getByLabelText('application details panel');
expect(panelAppDetails).toHaveClass('block');

const btnShowPanel = screen.getByRole('button', { name: 'show application details' });
const btnHidePanel = screen.getByRole('button', { name: 'hide application details' });

await userEvent.click(btnHidePanel);

expect(panelAppDetails).toHaveClass('hidden');
expect(panelOpenDetails).toHaveClass('block');

await userEvent.click(btnShowPanel);

expect(panelAppDetails).toHaveClass('block');
expect(panelOpenDetails).toHaveClass('hidden');
});

test('should call runApplication execution when run application button is clicked', async () => {
const recipe = catalog.recipes.find(r => r.id === 'recipe 1');
expect(recipe).not.toBeUndefined();

mocks.getCatalogMock.mockResolvedValue(catalog);
mocks.getPullingStatusesMock.mockResolvedValue(new Map());
render(Recipe, {
recipeId: 'recipe 1',
});
await new Promise(resolve => setTimeout(resolve, 200));

const btnRunApplication = screen.getByRole('button', { name: 'Run application' });
await userEvent.click(btnRunApplication);

expect(mocks.pullApplicationMock).toBeCalledWith('recipe 1');
});
159 changes: 111 additions & 48 deletions packages/frontend/src/pages/Recipe.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import Card from '/@/lib/Card.svelte';
import MarkdownRenderer from '/@/lib/markdown/MarkdownRenderer.svelte';
import Fa from 'svelte-fa';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { faDownload, faRefresh } from '@fortawesome/free-solid-svg-icons';
import { faPlay, faRefresh } from '@fortawesome/free-solid-svg-icons';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import Button from '/@/lib/button/Button.svelte';
import { getDisplayName } from '/@/utils/versionControlUtils';
import { getIcon } from '/@/utils/categoriesUtils';
import RecipeModels from './RecipeModels.svelte';
import { catalog } from '/@/stores/catalog';
import { recipes } from '/@/stores/recipe';
import { recipes } from '/@/stores/recipe';
export let recipeId: string;
Expand All @@ -23,76 +23,139 @@ $: recipe = $catalog.recipes.find(r => r.id === recipeId);
$: categories = $catalog.categories;
$: recipeStatus = $recipes.get(recipeId);
let loading: boolean = false;
// this will be selected by the user, init with the default model (the first in the catalog recipe?)
$: selectedModelId = recipe?.models?.[0];
$: model = $catalog.models.find(m => m.id === selectedModelId);
const onPullingRequest = async () => {
loading = true;
await studioClient.pullApplication(recipeId);
}
const onClickRepository = () => {
if (recipe) {
studioClient.openURL(recipe.repository);
}
}
}
$: applicationDetailsPanel = 'block';
$: applicationDetailsPanelToggle = 'hidden';
function toggleApplicationDetailsPanel() {
if (applicationDetailsPanel === 'block') {
applicationDetailsPanel = 'hidden';
applicationDetailsPanelToggle = 'block';
} else {
applicationDetailsPanel = 'block';
applicationDetailsPanelToggle = 'hidden';
}
}
</script>

<NavPage title="{recipe?.name || ''}" icon="{getIcon(recipe?.icon)}" searchEnabled="{false}">
<NavPage title="{recipe?.name || ''}" icon="{getIcon(recipe?.icon)}" searchEnabled="{false}" contentBackground='bg-charcoal-500'>
<svelte:fragment slot="tabs">
<Tab title="Summary" url="{recipeId}" />
<Tab title="Models" url="{recipeId}/models" />
</svelte:fragment>
<svelte:fragment slot="content">
<Route path="/" breadcrumb="Summary" >
<div class="flex flex-row w-full">
<div class="flex flex-row w-full">
<Route path="/" breadcrumb="Summary" >
<div class="flex-grow p-5">
<MarkdownRenderer source="{recipe?.readme}"/>
</div>
<!-- Right column -->
<div class="border-l border-l-charcoal-400 px-5 max-w-80 min-w-80">
<Card classes="bg-charcoal-800 mt-5">
<div slot="content" class="text-base font-normal p-2">
<div class="text-base mb-2">Repository</div>
</div>
</Route>
<Route path="/models" breadcrumb="History">
<RecipeModels modelsIds={recipe?.models} />
</Route>
<!-- Right column -->
<div class="w-[375px] min-w-[375px] h-fit bg-charcoal-800 rounded-l-md mt-4 {applicationDetailsPanel}" aria-label="application details panel">
<div class="flex flex-col my-5 w-[340px] space-y-4 mx-auto">
<div class="w-full flex flex-row justify-between">
<span class="text-base">Application Details</span>
<button on:click={toggleApplicationDetailsPanel} aria-label="hide application details"><i class="fas fa-angle-right text-gray-900"></i></button>
</div>

<div class="w-full bg-charcoal-600 rounded-md p-4">
{#if recipeStatus !== undefined && recipeStatus.tasks.length > 0}
{#if recipeStatus.state === 'error'}
<Button
on:click={() => onPullingRequest()}
class="w-full p-2"
icon="{faRefresh}"
>Retry</Button>
{:else if recipeStatus.state === 'loading' || recipeStatus.state === 'running'}
<Button
inProgress={true}
class="w-[300px] p-2 mx-auto"
icon="{faPlay}"
>
{#if recipeStatus.state === 'loading'}Loading{:else}Running{/if}
</Button>
{/if}
{:else}
<Button
on:click={() => onPullingRequest()}
class="w-[300px] p-2 mx-auto"
icon="{faPlay}"
>
Run application
</Button>
{/if}

<div class="text-xs text-gray-700 mt-3">
This will git clone the application, download the model, build images, and run the application as a pod locally.
</div>
{#if recipeStatus !== undefined && recipeStatus.tasks.length > 0}
<div class="mt-4 text-sm font-normal px-4 py-2">
<TasksProgress tasks="{recipeStatus.tasks}"/>
</div>
{/if}
</div>

<div class="flex flex-col w-full space-y-4 bg-charcoal-600 p-4">
{#if model}
<div class="flex flex-col space-y-2">
<div class="text-base">Model</div>
<div class="flex flex-row space-x-2">
<div class="bg-charcoal-900 min-w-[200px] grow flex flex-col p-2 rounded-md space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm" aria-label="model-selected">{model?.name}</span>
{#if recipe?.models?.[0] === model.id}
<i class="fas fa-star fa-xs text-gray-900"></i>
{/if}
</div>
{#if model?.license}
<div class="flex flex-row space-x-2">
<div class="bg-charcoal-400 text-gray-600 text-xs font-thin px-2.5 py-0.5 rounded-md" aria-label="license-model">
{model.license}
</div>
</div>
{/if}
</div>
</div>
{#if recipe?.models?.[0] === model.id}
<div class="px-2 text-xs text-gray-700" aria-label="default-model-warning">
* This is the default, recommended model for this recipe.
You can swap for a different compatible model.
</div>
{/if}
</div>
{/if}
<div class="flex flex-col space-y-2">
<div class="text-base">Repository</div>
<div class="cursor-pointer flex text-nowrap items-center">
<Fa size="20" icon="{faGithub}"/>
<div class="ml-2">
<div class="text-sm ml-2">
<a on:click={onClickRepository}>{getDisplayName(recipe?.repository)}</a>
</div>
</div>
</div>
</Card>
{#if recipeStatus !== undefined && recipeStatus.tasks.length > 0}
<Card classes="bg-charcoal-800 mt-4">
<div slot="content" class="text-base font-normal p-2">
<div class="text-base mb-2">Repository</div>
<TasksProgress tasks="{recipeStatus.tasks}"/>
{#if recipeStatus.state === 'error'}
<Button
disabled="{loading}"
inProgress="{loading}"
on:click={() => onPullingRequest()}
class="w-full mt-4 p-2"
icon="{faRefresh}"
>Retry</Button>
{/if}
</div>
</Card>
{:else}
<Button
on:click={() => onPullingRequest()}
disabled="{loading}"
inProgress="{loading}"
class="w-full mt-4 p-2"
icon="{faDownload}"
>
{#if loading}Loading{:else}Pull application{/if}
</Button>
{/if}
</div>
</div>

</div>
</div>
</Route>
<Route path="/models" breadcrumb="History">
<RecipeModels modelsIds={recipe?.models} />
</Route>
<div class="bg-charcoal-800 mt-4 p-4 rounded-md h-fit {applicationDetailsPanelToggle}" aria-label="toggle application details">
<button on:click={toggleApplicationDetailsPanel} aria-label="show application details"><i class="fas fa-angle-left text-gray-900"></i></button>
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="subtitle">
<div class="mt-2">
Expand Down
Loading

0 comments on commit 4d9d79b

Please sign in to comment.