From 7e76b45ce37193317e2e0e7218bfba0a12ef545f Mon Sep 17 00:00:00 2001 From: Matthieu Jouis Date: Tue, 16 Jul 2024 14:21:09 +0200 Subject: [PATCH] connect dashboard tree to the API --- Makefile | 2 +- frontend/src/App.vue | 16 ++- frontend/src/components/FileTree.vue | 4 +- frontend/src/components/LoadingBars.vue | 94 ++++++++++++++++++ frontend/{ => src}/env.d.ts | 0 frontend/src/services/api.ts | 21 ++++ frontend/src/services/utils.ts | 3 + frontend/src/views/DashboardView.vue | 99 ++++++------------- .../components}/FileTree.spec.ts | 6 +- frontend/tests/services/api.spec.ts | 22 +++++ frontend/tests/test.utils.ts | 27 +++++ frontend/tests/views/dashboard.spec.ts | 45 +++++++++ frontend/tsconfig.app.json | 12 ++- frontend/tsconfig.node.json | 5 +- frontend/tsconfig.vitest.json | 12 ++- src/mandr/dashboard/webapp.py | 8 ++ 16 files changed, 289 insertions(+), 87 deletions(-) create mode 100644 frontend/src/components/LoadingBars.vue rename frontend/{ => src}/env.d.ts (100%) create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/services/utils.ts rename frontend/{src/components/__tests__ => tests/components}/FileTree.spec.ts (93%) create mode 100644 frontend/tests/services/api.spec.ts create mode 100644 frontend/tests/test.utils.ts create mode 100644 frontend/tests/views/dashboard.spec.ts diff --git a/Makefile b/Makefile index 64ce39bd..11637bb4 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ check-wip: python -m pytest tests serve-api: - python -m uvicorn mandr.dashboard.webapp:app --reload --reload-dir src --host 0.0.0.0 --timeout-graceful-shutdown 0 + python -m uvicorn mandr.dashboard.webapp:app --reload --reload-dir ./src --host 0.0.0.0 --timeout-graceful-shutdown 0 build-frontend: # build the SPA diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 44843a03..024bf5fa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,17 @@ diff --git a/frontend/src/components/FileTree.vue b/frontend/src/components/FileTree.vue index c1b22443..9625969f 100644 --- a/frontend/src/components/FileTree.vue +++ b/frontend/src/components/FileTree.vue @@ -9,12 +9,12 @@ export interface FileTreeNode { diff --git a/frontend/env.d.ts b/frontend/src/env.d.ts similarity index 100% rename from frontend/env.d.ts rename to frontend/src/env.d.ts diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 00000000..c9796005 --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,21 @@ +const BASE_URL = "http://localhost:8000/api"; + +function getErrorMessage(error: unknown) { + if (error instanceof Error) return error.message; + return String(error); +} + +function reportError(message: string) { + console.error(message); +} + +export async function getAllManderPaths(): Promise { + try { + const r = await fetch(`${BASE_URL}/mandrs`); + const paths = await r.json(); + return paths; + } catch (error) { + reportError(getErrorMessage(error)); + return []; + } +} diff --git a/frontend/src/services/utils.ts b/frontend/src/services/utils.ts new file mode 100644 index 00000000..0f532df4 --- /dev/null +++ b/frontend/src/services/utils.ts @@ -0,0 +1,3 @@ +export function sleep(delay: number): Promise { + return new Promise((resolve) => setTimeout(resolve, delay)); +} diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 57579b2d..ccf756e1 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -1,72 +1,37 @@ diff --git a/frontend/src/components/__tests__/FileTree.spec.ts b/frontend/tests/components/FileTree.spec.ts similarity index 93% rename from frontend/src/components/__tests__/FileTree.spec.ts rename to frontend/tests/components/FileTree.spec.ts index 5459beca..59904c9a 100644 --- a/frontend/src/components/__tests__/FileTree.spec.ts +++ b/frontend/tests/components/FileTree.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { mount } from "@vue/test-utils"; -import FileTree, { type FileTreeNode } from "../FileTree.vue"; +import FileTree, { type FileTreeNode } from "@/components/FileTree.vue"; function countLeaves(nodes: FileTreeNode[]): number { function countInNode(node: FileTreeNode): number { @@ -14,11 +14,11 @@ function countLeaves(nodes: FileTreeNode[]): number { } const allBranches = nodes.map((node) => countInNode(node)); - return allBranches.reduce((accumulator, node) => accumulator + node); + return allBranches.reduce((accumulator, leavesCount) => accumulator + leavesCount); } describe("FileTree", () => { - it("renders properly", () => { + it("Renders properly.", () => { const records: FileTreeNode[] = [ { label: "Punk Rock", diff --git a/frontend/tests/services/api.spec.ts b/frontend/tests/services/api.spec.ts new file mode 100644 index 00000000..a3f5e73d --- /dev/null +++ b/frontend/tests/services/api.spec.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; +import { getAllManderPaths } from "@/services/api"; + +import { createFetchResponse, mockedFecth } from "../test.utils"; + +describe("api", () => { + it("Can fetch the list of manders from the server.", async () => { + const paths = [ + "probabl-ai/demo-usecase/training/0", + "probabl-ai/test-mandr/0", + "probabl-ai/test-mandr/1", + "probabl-ai/test-mandr/2", + "probabl-ai/test-mandr/3", + "probabl-ai/test-mandr/4", + ]; + + mockedFecth.mockResolvedValue(createFetchResponse(paths)); + + const r = await getAllManderPaths(); + expect(r).toStrictEqual(paths); + }); +}); diff --git a/frontend/tests/test.utils.ts b/frontend/tests/test.utils.ts new file mode 100644 index 00000000..04212a28 --- /dev/null +++ b/frontend/tests/test.utils.ts @@ -0,0 +1,27 @@ +import { vi } from "vitest"; +import { mount, flushPromises } from "@vue/test-utils"; +import { type ComponentPublicInstance, defineComponent, Suspense, h } from "vue"; + +export const mockedFecth = vi.fn(); +global.fetch = mockedFecth; + +export function createFetchResponse(data: object) { + return { json: () => new Promise((resolve) => resolve(data)) }; +} + +export async function mountSuspens( + component: new () => ComponentPublicInstance, + options: any = {} +) { + const suspensedComponent = defineComponent({ + render: () => { + return h(Suspense, null, { + default: h(component), + fallback: h("div", "loading..."), + }); + }, + }); + const wrapper = mount(suspensedComponent, options); + await flushPromises(); + return wrapper; +} diff --git a/frontend/tests/views/dashboard.spec.ts b/frontend/tests/views/dashboard.spec.ts new file mode 100644 index 00000000..19058869 --- /dev/null +++ b/frontend/tests/views/dashboard.spec.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { mountSuspens, mockedFecth, createFetchResponse } from "../test.utils"; +import DashboardView from "@/views/DashboardView.vue"; +import FileTree from "@/components/FileTree.vue"; + +describe("DashboardView", () => { + it("Parse the list of fetched list of mander to an array of FileTreeNode.", async () => { + const paths = [ + "probabl-ai/demo-usecase/training/0", + "probabl-ai/test-mandr/0", + "probabl-ai/test-mandr/1", + "probabl-ai/test-mandr/2", + "probabl-ai/test-mandr/3", + "probabl-ai/test-mandr/4", + ]; + + mockedFecth.mockResolvedValue(createFetchResponse(paths)); + + const wrapper = await mountSuspens(DashboardView); + const fileTree = await wrapper.getComponent(FileTree); + expect(fileTree.props()).toEqual({ + nodes: [ + { + label: "probabl-ai", + children: [ + { + label: "demo-usecase", + children: [{ label: "training", children: [{ label: "0" }] }], + }, + { + label: "test-mandr", + children: [ + { label: "0" }, + { label: "1" }, + { label: "2" }, + { label: "3" }, + { label: "4" }, + ], + }, + ], + }, + ], + }); + }); +}); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index e14c754d..e213d1dd 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,14 +1,18 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], - "exclude": ["src/**/__tests__/*"], + "include": [ + "src/env.d.ts", + "src/**/*", + "src/**/*.vue" + ], "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index f0940630..6fffb82e 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -11,9 +11,10 @@ "composite": true, "noEmit": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "module": "ESNext", "moduleResolution": "Bundler", - "types": ["node"] + "types": [ + "node" + ] } } diff --git a/frontend/tsconfig.vitest.json b/frontend/tsconfig.vitest.json index 571995d1..68d5b1db 100644 --- a/frontend/tsconfig.vitest.json +++ b/frontend/tsconfig.vitest.json @@ -1,11 +1,17 @@ { "extends": "./tsconfig.app.json", - "exclude": [], + "include": [ + "src/**/*", + "src/**/*.vue", + "tests/**/*", + ], "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", - "lib": [], - "types": ["node", "jsdom"] + "types": [ + "node", + "jsdom" + ] } } diff --git a/src/mandr/dashboard/webapp.py b/src/mandr/dashboard/webapp.py index 70bce5d9..87f739b9 100644 --- a/src/mandr/dashboard/webapp.py +++ b/src/mandr/dashboard/webapp.py @@ -3,6 +3,7 @@ from pathlib import Path from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from mandr.infomander import ARTIFACTS_KEY, LOGS_KEY, VIEWS_KEY, InfoManderRepository @@ -11,6 +12,13 @@ _STATIC_PATH = _DASHBOARD_PATH / "static" app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) @app.get("/api/mandrs")