From 92c5b31d7e584f5563e6ccc2ce5cfb3edf731851 Mon Sep 17 00:00:00 2001 From: Schahin Rouhanizadeh Date: Fri, 1 Nov 2024 22:12:00 +0100 Subject: [PATCH] Introduce `Nativelink Web UI` It's still a work in progress and highly experimental --- .../vocabularies/TraceMachina/accept.txt | 4 + flake.nix | 3 + tools/pre-commit-hooks.nix | 1 + web/ui/.env.example | 1 + web/ui/.gitignore | 30 + web/ui/README.md | 38 + web/ui/bun.lockb | Bin 0 -> 77619 bytes web/ui/env.d.ts | 1 + web/ui/image.nix | 96 ++ web/ui/index.html | 14 + web/ui/package.json | 31 + web/ui/src/App.vue | 28 + web/ui/src/assets/base.css | 86 ++ web/ui/src/assets/logo-dark.svg | 1128 +++++++++++++++++ web/ui/src/assets/logo-light.svg | 1123 ++++++++++++++++ web/ui/src/assets/main.css | 84 ++ web/ui/src/assets/tailwind.css | 1 + web/ui/src/components/Footer.vue | 48 + web/ui/src/components/Notifications.vue | 13 + .../components/dashboard/BuildStatistics.vue | 18 + .../src/components/dashboard/RecentBuilds.vue | 69 + .../components/dashboard/RemoteCacheUsage.vue | 83 ++ web/ui/src/components/icons/IconCommunity.vue | 7 + .../components/icons/IconDocumentation.vue | 7 + web/ui/src/components/icons/IconEcosystem.vue | 7 + web/ui/src/components/icons/IconSupport.vue | 7 + web/ui/src/components/icons/IconTooling.vue | 19 + web/ui/src/main.ts | 15 + web/ui/src/router/index.ts | 30 + web/ui/src/stores/build.ts | 44 + web/ui/src/stores/data.ts | 9 + web/ui/src/stores/http.ts | 7 + web/ui/src/stores/ws.ts | 27 + web/ui/src/views/AsideView.vue | 113 ++ web/ui/src/views/BuildDetailView.vue | 53 + web/ui/src/views/BuildView.vue | 44 + web/ui/src/views/DashboardView.vue | 20 + web/ui/tsconfig.app.json | 14 + web/ui/tsconfig.json | 11 + web/ui/tsconfig.node.json | 19 + web/ui/vite.config.ts | 22 + 41 files changed, 3375 insertions(+) create mode 100644 web/ui/.env.example create mode 100644 web/ui/.gitignore create mode 100644 web/ui/README.md create mode 100755 web/ui/bun.lockb create mode 100644 web/ui/env.d.ts create mode 100644 web/ui/image.nix create mode 100644 web/ui/index.html create mode 100644 web/ui/package.json create mode 100644 web/ui/src/App.vue create mode 100644 web/ui/src/assets/base.css create mode 100644 web/ui/src/assets/logo-dark.svg create mode 100644 web/ui/src/assets/logo-light.svg create mode 100644 web/ui/src/assets/main.css create mode 100644 web/ui/src/assets/tailwind.css create mode 100644 web/ui/src/components/Footer.vue create mode 100644 web/ui/src/components/Notifications.vue create mode 100644 web/ui/src/components/dashboard/BuildStatistics.vue create mode 100644 web/ui/src/components/dashboard/RecentBuilds.vue create mode 100644 web/ui/src/components/dashboard/RemoteCacheUsage.vue create mode 100644 web/ui/src/components/icons/IconCommunity.vue create mode 100644 web/ui/src/components/icons/IconDocumentation.vue create mode 100644 web/ui/src/components/icons/IconEcosystem.vue create mode 100644 web/ui/src/components/icons/IconSupport.vue create mode 100644 web/ui/src/components/icons/IconTooling.vue create mode 100644 web/ui/src/main.ts create mode 100644 web/ui/src/router/index.ts create mode 100644 web/ui/src/stores/build.ts create mode 100644 web/ui/src/stores/data.ts create mode 100644 web/ui/src/stores/http.ts create mode 100644 web/ui/src/stores/ws.ts create mode 100644 web/ui/src/views/AsideView.vue create mode 100644 web/ui/src/views/BuildDetailView.vue create mode 100644 web/ui/src/views/BuildView.vue create mode 100644 web/ui/src/views/DashboardView.vue create mode 100644 web/ui/tsconfig.app.json create mode 100644 web/ui/tsconfig.json create mode 100644 web/ui/tsconfig.node.json create mode 100644 web/ui/vite.config.ts diff --git a/.github/styles/config/vocabularies/TraceMachina/accept.txt b/.github/styles/config/vocabularies/TraceMachina/accept.txt index 30d280a46..706bc95b4 100644 --- a/.github/styles/config/vocabularies/TraceMachina/accept.txt +++ b/.github/styles/config/vocabularies/TraceMachina/accept.txt @@ -67,3 +67,7 @@ Wainer Gert Bruer Eagan +Vue +Vite +VSCode +Vetur diff --git a/flake.nix b/flake.nix index f9f70a972..65b0553e4 100644 --- a/flake.nix +++ b/flake.nix @@ -279,6 +279,8 @@ }; }; + nativelink-ui = pkgs.callPackage ./web/ui/image.nix {inherit buildImage pullImage pkgs;}; + nativelink-worker-init = pkgs.callPackage ./tools/nativelink-worker-init.nix {inherit buildImage self nativelink-image;}; rbe-autogen = pkgs.callPackage ./local-remote-execution/rbe-autogen.nix { @@ -419,6 +421,7 @@ nativelink-worker-init nativelink-x86_64-linux publish-ghcr + nativelink-ui # not working yet ; default = nativelink; diff --git a/tools/pre-commit-hooks.nix b/tools/pre-commit-hooks.nix index e690341ae..86bed69e5 100644 --- a/tools/pre-commit-hooks.nix +++ b/tools/pre-commit-hooks.nix @@ -65,6 +65,7 @@ in { # Bun binary lockfile "web/platform/bun.lockb" + "web/ui/bun.lockb" ]; enable = true; types = ["binary"]; diff --git a/web/ui/.env.example b/web/ui/.env.example new file mode 100644 index 000000000..6a68b0f75 --- /dev/null +++ b/web/ui/.env.example @@ -0,0 +1 @@ +BRIDGE_URL= diff --git a/web/ui/.gitignore b/web/ui/.gitignore new file mode 100644 index 000000000..8ee54e8d3 --- /dev/null +++ b/web/ui/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/web/ui/README.md b/web/ui/README.md new file mode 100644 index 000000000..88f45f12c --- /dev/null +++ b/web/ui/README.md @@ -0,0 +1,38 @@ +# NativeLink Web UI + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript can't handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +bun install +``` + +### Compile and Hot-Reload for Development + +```sh +bun dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +bun run build +``` + +### Preview the build +```sh +bun preview +``` diff --git a/web/ui/bun.lockb b/web/ui/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..53384520769774fbfc9a3e02b9f16508af9ffd7b GIT binary patch literal 77619 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 diff --git a/web/ui/image.nix b/web/ui/image.nix new file mode 100644 index 000000000..302e71dc2 --- /dev/null +++ b/web/ui/image.nix @@ -0,0 +1,96 @@ +{ + pkgs, + buildImage, + pullImage, + ... +}: let + # Base image configuration for nginx:mainline-alpine-slim + imageParams = { + imageName = "nginx"; + imageDigest = "sha256:e9293c9bedb0db866e7d2b69e58131db4c2478e6cd216cdd99b134830703983a"; + sha256 = "sha256:63f36d95235a84d401bd3e061ffb0c25f72ca1d7b0ad154e583b7e25479d424f"; + description = "Nginx mainline Alpine slim image for serving Vue3 applications."; + title = "Nginx Alpine Slim for Vue3"; + }; + + # Derivation to build the Vue3 web UI using Bun + webUi = pkgs.stdenv.mkDerivation { + pname = "nativelink-web-ui"; + version = "1.0.0"; # Update as necessary + + src = ./.; # Path to your Vue3 project + + buildInputs = [pkgs.bun]; + + # Ensure Bun is available in the build environment + buildPhase = '' + bun install + bun run build + ''; + + # Install the built files and nginx configuration + installPhase = '' + mkdir -p $out/dist + cp -r dist/* $out/dist/ + + mkdir -p $out/etc/nginx + cat > $out/etc/nginx/nginx.conf < + + + + + + + Nativelink | Dashboard + + +
+ + + diff --git a/web/ui/package.json b/web/ui/package.json new file mode 100644 index 000000000..671074d47 --- /dev/null +++ b/web/ui/package.json @@ -0,0 +1,31 @@ +{ + "name": "nativelink-dashboard", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force" + }, + "dependencies": { + "pinia": "^2.2.4", + "vue": "^3.5.12", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0-alpha", + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.17.0", + "@vitejs/plugin-vue": "^5.1.4", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "@vue/tsconfig": "^0.5.1", + "npm-run-all2": "^7.0.1", + "tailwindcss": "^4.0.0-alpha", + "typescript": "~5.6.0", + "vite": "^5.4.10", + "vue-tsc": "^2.1.6" + } +} diff --git a/web/ui/src/App.vue b/web/ui/src/App.vue new file mode 100644 index 000000000..0dac2d4e7 --- /dev/null +++ b/web/ui/src/App.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/web/ui/src/assets/base.css b/web/ui/src/assets/base.css new file mode 100644 index 000000000..8816868a4 --- /dev/null +++ b/web/ui/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/web/ui/src/assets/logo-dark.svg b/web/ui/src/assets/logo-dark.svg new file mode 100644 index 000000000..6b6fa6294 --- /dev/null +++ b/web/ui/src/assets/logo-dark.svg @@ -0,0 +1,1128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/ui/src/assets/logo-light.svg b/web/ui/src/assets/logo-light.svg new file mode 100644 index 000000000..148fcdce8 --- /dev/null +++ b/web/ui/src/assets/logo-light.svg @@ -0,0 +1,1123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/ui/src/assets/main.css b/web/ui/src/assets/main.css new file mode 100644 index 000000000..c5f992b01 --- /dev/null +++ b/web/ui/src/assets/main.css @@ -0,0 +1,84 @@ +@import './base.css'; + +/* #app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} */ + +/* Custom scrollbar styles */ +.scrollbar::-webkit-scrollbar { + width: 8px; +} +.scrollbar::-webkit-scrollbar-track { + background: var(--tw-color-card); +} +.scrollbar::-webkit-scrollbar-thumb { + background-color: #4b5563; /* Gray-600 */ + border-radius: 4px; +} + +/* Hide scrollbar for no-scrollbar class */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Utility for radial gradient background */ +.bg-radial-gradient { + background-image: radial-gradient( + farthest-corner at 42.5% -20%, + rgba(40, 41, 79, 255) 5%, + rgba(11, 12, 17, 255) 65% + ); +} + +/* Visually hidden class for accessibility */ +.sr-only { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} + +/* Link styles */ +.link { + text-decoration: none; + color: inherit; +} diff --git a/web/ui/src/assets/tailwind.css b/web/ui/src/assets/tailwind.css new file mode 100644 index 000000000..f1d8c73cd --- /dev/null +++ b/web/ui/src/assets/tailwind.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/web/ui/src/components/Footer.vue b/web/ui/src/components/Footer.vue new file mode 100644 index 000000000..e52abe9bd --- /dev/null +++ b/web/ui/src/components/Footer.vue @@ -0,0 +1,48 @@ + diff --git a/web/ui/src/components/Notifications.vue b/web/ui/src/components/Notifications.vue new file mode 100644 index 000000000..ada7fa058 --- /dev/null +++ b/web/ui/src/components/Notifications.vue @@ -0,0 +1,13 @@ + diff --git a/web/ui/src/components/dashboard/BuildStatistics.vue b/web/ui/src/components/dashboard/BuildStatistics.vue new file mode 100644 index 000000000..665011c51 --- /dev/null +++ b/web/ui/src/components/dashboard/BuildStatistics.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/ui/src/components/dashboard/RecentBuilds.vue b/web/ui/src/components/dashboard/RecentBuilds.vue new file mode 100644 index 000000000..626354058 --- /dev/null +++ b/web/ui/src/components/dashboard/RecentBuilds.vue @@ -0,0 +1,69 @@ + + + diff --git a/web/ui/src/components/dashboard/RemoteCacheUsage.vue b/web/ui/src/components/dashboard/RemoteCacheUsage.vue new file mode 100644 index 000000000..45bf4109e --- /dev/null +++ b/web/ui/src/components/dashboard/RemoteCacheUsage.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/web/ui/src/components/icons/IconCommunity.vue b/web/ui/src/components/icons/IconCommunity.vue new file mode 100644 index 000000000..2dc8b0552 --- /dev/null +++ b/web/ui/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/web/ui/src/components/icons/IconDocumentation.vue b/web/ui/src/components/icons/IconDocumentation.vue new file mode 100644 index 000000000..6d4791cfb --- /dev/null +++ b/web/ui/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/web/ui/src/components/icons/IconEcosystem.vue b/web/ui/src/components/icons/IconEcosystem.vue new file mode 100644 index 000000000..c3a4f078c --- /dev/null +++ b/web/ui/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/web/ui/src/components/icons/IconSupport.vue b/web/ui/src/components/icons/IconSupport.vue new file mode 100644 index 000000000..7452834d3 --- /dev/null +++ b/web/ui/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/web/ui/src/components/icons/IconTooling.vue b/web/ui/src/components/icons/IconTooling.vue new file mode 100644 index 000000000..660598d7c --- /dev/null +++ b/web/ui/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/web/ui/src/main.ts b/web/ui/src/main.ts new file mode 100644 index 000000000..b7aa2974b --- /dev/null +++ b/web/ui/src/main.ts @@ -0,0 +1,15 @@ +import './assets/main.css' +import './assets/tailwind.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/web/ui/src/router/index.ts b/web/ui/src/router/index.ts new file mode 100644 index 000000000..f6d36dbfd --- /dev/null +++ b/web/ui/src/router/index.ts @@ -0,0 +1,30 @@ +import { createRouter, createWebHistory } from 'vue-router' +import DashboardView from '@/views/DashboardView.vue' +import BuildDetailView from '@/views/BuildDetailView.vue' + + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'dashboard', + component: DashboardView + }, + { + path: '/builds', + name: 'builds', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('@/views/BuildView.vue') + }, + { + path: '/builds/:buildId', + name: 'buildDetail', + component: BuildDetailView, + }, + ] +}) + +export default router diff --git a/web/ui/src/stores/build.ts b/web/ui/src/stores/build.ts new file mode 100644 index 000000000..e1bef4403 --- /dev/null +++ b/web/ui/src/stores/build.ts @@ -0,0 +1,44 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// Define the structure of a Build +export interface Build { + id: string + name: string + status: 'Success' | 'Failed' | 'In Progress' + startTime: string + endTime: string +} + +// Define the Pinia store +export const useBuildStore = defineStore('build', () => { + // Reactive array to hold build data + const builds = ref([ + { + id: 'build-001', + name: 'Build 001', + status: 'Success', + startTime: '2024-10-25 10:00 AM', + endTime: '2024-10-25 10:30 AM' + }, + { + id: 'build-002', + name: 'Build 002', + status: 'Failed', + startTime: '2024-10-26 11:00 AM', + endTime: '2024-10-26 11:45 AM' + }, + // Add more dummy builds as needed + ]) + + // Optional: Actions to manipulate builds + const addBuild = (build: Build) => { + builds.value.push(build) + } + + const removeBuild = (id: string) => { + builds.value = builds.value.filter(build => build.id !== id) + } + + return { builds, addBuild, removeBuild } +}) diff --git a/web/ui/src/stores/data.ts b/web/ui/src/stores/data.ts new file mode 100644 index 000000000..b59740d61 --- /dev/null +++ b/web/ui/src/stores/data.ts @@ -0,0 +1,9 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useDataStore = defineStore('data', () => { + const rawData = ref(0) + //TODO: Add raw data store, which retrieves data from httpStore and webSocketStore and push them in to buildStore + + return { rawData } +}) diff --git a/web/ui/src/stores/http.ts b/web/ui/src/stores/http.ts new file mode 100644 index 000000000..33feab908 --- /dev/null +++ b/web/ui/src/stores/http.ts @@ -0,0 +1,7 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// Define the Pinia store +export const useHTTPStore = defineStore('http', () => { + //TODO: Implement the HTTP Store +}) diff --git a/web/ui/src/stores/ws.ts b/web/ui/src/stores/ws.ts new file mode 100644 index 000000000..4cb036b0f --- /dev/null +++ b/web/ui/src/stores/ws.ts @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// Define the Pinia store +export const useWebSocketStore = defineStore('ws', () => { + const connect = () => { + const webSocket = new WebSocket('ws://localhost:888'); + // Listen for open events + webSocket.addEventListener("open", (event) => { + console.log("Connected to WebSocket:", event); + }); + // Listen for message events + webSocket.addEventListener("message", (event) => { + console.log("Message from server ", event.data); + // TODO: Send the data to the data store + }); + // Listen for close event + webSocket.addEventListener("close", (event) => { + console.log("Closed: ", event); + }); + // Listen for errors events + webSocket.addEventListener("error", (err) => { + console.log("Error: ", err); + }); + + }; +}) diff --git a/web/ui/src/views/AsideView.vue b/web/ui/src/views/AsideView.vue new file mode 100644 index 000000000..601f915e3 --- /dev/null +++ b/web/ui/src/views/AsideView.vue @@ -0,0 +1,113 @@ + + + diff --git a/web/ui/src/views/BuildDetailView.vue b/web/ui/src/views/BuildDetailView.vue new file mode 100644 index 000000000..8235ad56f --- /dev/null +++ b/web/ui/src/views/BuildDetailView.vue @@ -0,0 +1,53 @@ + + + diff --git a/web/ui/src/views/BuildView.vue b/web/ui/src/views/BuildView.vue new file mode 100644 index 000000000..6f7e65d9b --- /dev/null +++ b/web/ui/src/views/BuildView.vue @@ -0,0 +1,44 @@ + + + diff --git a/web/ui/src/views/DashboardView.vue b/web/ui/src/views/DashboardView.vue new file mode 100644 index 000000000..e25f93d34 --- /dev/null +++ b/web/ui/src/views/DashboardView.vue @@ -0,0 +1,20 @@ + + + diff --git a/web/ui/tsconfig.app.json b/web/ui/tsconfig.app.json new file mode 100644 index 000000000..e14c754d3 --- /dev/null +++ b/web/ui/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/ui/tsconfig.json b/web/ui/tsconfig.json new file mode 100644 index 000000000..66b5e5703 --- /dev/null +++ b/web/ui/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/ui/tsconfig.node.json b/web/ui/tsconfig.node.json new file mode 100644 index 000000000..f09406303 --- /dev/null +++ b/web/ui/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/web/ui/vite.config.ts b/web/ui/vite.config.ts new file mode 100644 index 000000000..28d6c804f --- /dev/null +++ b/web/ui/vite.config.ts @@ -0,0 +1,22 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +import tailwindcss from "@tailwindcss/vite"; + + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueJsx(), + tailwindcss() + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})