diff --git a/dashboard/components.json b/dashboard/components.json new file mode 100644 index 00000000..71a10880 --- /dev/null +++ b/dashboard/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index abaf39ff..724fd3a6 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -8,14 +8,19 @@ "name": "exosphere-dashboard", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@reactflow/node-resizer": "^2.2.14", "@types/uuid": "^10.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "lucide-react": "^0.544.0", "next": "^15.5.3", "react": "^19.1.0", "react-dom": "^19.1.1", "reactflow": "^11.11.4", "recharts": "^3.2.0", + "tailwind-merge": "^3.3.1", "uuid": "^13.0.0" }, "devDependencies": { @@ -28,6 +33,7 @@ "eslint": "^9", "eslint-config-next": "15.5.3", "tailwindcss": "^4", + "tw-animate-css": "^1.3.8", "typescript": "^5" } }, @@ -970,6 +976,279 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -1783,7 +2062,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -2782,6 +3061,17 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -2798,7 +3088,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -5213,7 +5502,6 @@ "version": "0.544.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", - "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -6520,6 +6808,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", @@ -6658,6 +6955,15 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index b9655112..6b494cd4 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -9,14 +9,19 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@reactflow/node-resizer": "^2.2.14", "@types/uuid": "^10.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "lucide-react": "^0.544.0", "next": "^15.5.3", "react": "^19.1.0", "react-dom": "^19.1.1", "reactflow": "^11.11.4", "recharts": "^3.2.0", + "tailwind-merge": "^3.3.1", "uuid": "^13.0.0" }, "devDependencies": { @@ -29,6 +34,7 @@ "eslint": "^9", "eslint-config-next": "15.5.3", "tailwindcss": "^4", + "tw-animate-css": "^1.3.8", "typescript": "^5" } } diff --git a/dashboard/src/app/api/namespaces/route.ts b/dashboard/src/app/api/namespaces/route.ts new file mode 100644 index 00000000..b6c5f373 --- /dev/null +++ b/dashboard/src/app/api/namespaces/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000'; +const API_KEY = process.env.EXOSPHERE_API_KEY; + +export async function GET() { + try { + if (!API_KEY) { + return NextResponse.json({ error: 'API key not configured' }, { status: 500 }); + } + + const response = await fetch(`${API_BASE_URL}/v0/namespaces`, { + headers: { + 'X-API-Key': API_KEY, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`State manager API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching namespaces:', error); + return NextResponse.json( + { error: 'Failed to fetch namespaces' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/dashboard/src/app/api/node-run-details/route.ts b/dashboard/src/app/api/node-run-details/route.ts new file mode 100644 index 00000000..da642ec0 --- /dev/null +++ b/dashboard/src/app/api/node-run-details/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000'; +const API_KEY = process.env.EXOSPHERE_API_KEY; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const namespace = searchParams.get('namespace'); + const graphName = searchParams.get('graphName'); + const runId = searchParams.get('runId'); + const nodeId = searchParams.get('nodeId'); + + if (!namespace || !graphName || !runId || !nodeId) { + return NextResponse.json({ error: 'Namespace, graphName, runId, and nodeId are required' }, { status: 400 }); + } + + if (!API_KEY) { + return NextResponse.json({ error: 'API key not configured' }, { status: 500 }); + } + + const response = await fetch(`${API_BASE_URL}/v0/namespace/${encodeURIComponent(namespace)}/graph/${encodeURIComponent(graphName)}/run/${encodeURIComponent(runId)}/node/${encodeURIComponent(nodeId)}`, { + headers: { + 'X-API-Key': API_KEY, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + // Propagate upstream error with original status and body + let upstreamBody: string; + const contentType = response.headers.get('Content-Type'); + + try { + if (contentType?.includes('application/json')) { + const jsonData = await response.json(); + upstreamBody = JSON.stringify(jsonData); + } else { + upstreamBody = await response.text(); + } + } catch { + upstreamBody = `Upstream error: ${response.status} ${response.statusText}`; + } + + return new Response(upstreamBody, { + status: response.status, + headers: { + 'Content-Type': contentType || 'text/plain', + }, + }); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching node run details:', error); + return NextResponse.json( + { error: 'Failed to fetch node run details' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css index 357474b8..353c9d91 100644 --- a/dashboard/src/app/globals.css +++ b/dashboard/src/app/globals.css @@ -1,14 +1,120 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* Custom Font Face Declarations */ +@font-face { + font-family: "Dark Grotesque"; + src: url("/DarkerGrotesque-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} :root { - --background: #ffffff; - --foreground: #171717; --accent: #031035; --accent-light: #0a1a4a; --accent-lighter: #1a2a5a; --accent-lightest: #2a3a6a; + --radius: 0.625rem; + + /* Updated Color Palette based on #031035 and accents */ + --background: #031035; /* Deep navy primary */ + --foreground: #f8fafc; /* Light text */ + --card: #0a1a4a; /* Slightly lighter navy for cards */ + --card-foreground: #f8fafc; + --popover: #0a1a4a; + --popover-foreground: #f8fafc; + --primary: #87ceeb; /* Sky blue accent */ + --primary-foreground: #031035; + --secondary: #1a2a5a; /* Medium navy */ + --secondary-foreground: #f8fafc; + --muted: #2a3a6a; /* Muted navy */ + --muted-foreground: #94a3b8; /* Light gray text */ + --accent-foreground: #f8fafc; + --destructive: #ff6b8a; /* Pink accent for errors */ + --border: #1a2a5a; /* Navy border */ + --input: #1a2a5a; /* Navy input background */ + --ring: #87ceeb; /* Sky blue focus ring */ + --chart-1: #87ceeb; /* Sky blue */ + --chart-2: #4ade80; /* Green accent */ + --chart-3: #fbbf24; /* Yellow accent */ + --chart-4: #ff6b8a; /* Pink accent */ + --chart-5: #a78bfa; /* Purple accent */ + --sidebar: #0a1a4a; + --sidebar-foreground: #f8fafc; + --sidebar-primary: #87ceeb; + --sidebar-primary-foreground: #031035; + --sidebar-accent: #1a2a5a; + --sidebar-accent-foreground: #f8fafc; + --sidebar-border: #1a2a5a; + --sidebar-ring: #87ceeb; +} + +/* Custom Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--muted); + border-radius: var(--radius); +} + +::-webkit-scrollbar-thumb { + background: var(--chart-1); + border-radius: var(--radius); + transition: background 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent-lightest); +} + +::-webkit-scrollbar-corner { + background: var(--muted); +} + +/* Firefox scrollbar styling */ +* { + scrollbar-width: thin; + scrollbar-color: var(--border) var(--muted); } +.react-flow__node{ + border-radius: 0.625rem; + border: 1px solid #87ceeb; + background-color: #2a3a6a; + color: #f8fafc; + font-size: 0.875rem; + font-weight: 500; + text-align: center; + transition: all 0.3s ease; + &:hover{ + background-color: #6579b4; + } + &:active{ + background-color: #6579b4; + } + &:focus{ + outline: none; + } + &:disabled{ + background-color: #2a3a6a; + cursor: not-allowed; + } + &:focus-visible{ + outline: none; + } + &:focus-within{ + outline: 2px solid #87ceeb; + } +} + + + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); @@ -16,19 +122,60 @@ --color-accent-light: var(--accent-light); --color-accent-lighter: var(--accent-lighter); --color-accent-lightest: var(--accent-lightest); - --font-sans: var(--font-geist-sans); + --font-sans: "Dark Grotesque", sans-serif; --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -/* @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +@layer base { + * { + @apply border-border outline-ring/50; + } + html { + font-size: 120%; /* Increased from default 16px */ + } + body { + @apply bg-background text-foreground font-sans; + } + + /* Custom select dropdown styling for better dark theme support */ + select { + color-scheme: dark; + } + + select option { + background-color: var(--popover); + color: var(--popover-foreground); } -} */ - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; } diff --git a/dashboard/src/app/page.tsx b/dashboard/src/app/page.tsx index 621b7ead..879b2982 100644 --- a/dashboard/src/app/page.tsx +++ b/dashboard/src/app/page.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState, useEffect } from 'react'; +import Image from 'next/image'; import { GraphTemplateBuilder } from '@/components/GraphTemplateBuilder'; import { NamespaceOverview } from '@/components/NamespaceOverview'; import { RunsTable } from '@/components/RunsTable'; @@ -13,16 +14,23 @@ import { UpsertGraphTemplateResponse, } from '@/types/state-manager'; import { - GitBranch, BarChart3, AlertCircle, Filter } from 'lucide-react'; +// Shadcn components +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Select } from '@/components/ui/select'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + export default function Dashboard() { const [activeTab, setActiveTab] = useState< 'overview' | 'graph' |'runs'>('overview'); const [namespace, setNamespace] = useState('default'); - const [graphName, setGraphName] = useState('test-graph'); + const [availableNamespaces, setAvailableNamespaces] = useState([]); + const [graphName] = useState('test-graph'); const [graphTemplate, setGraphTemplate] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -34,21 +42,53 @@ export default function Dashboard() { const [selectedGraphTemplate, setSelectedGraphTemplate] = useState(null); const [isGraphModalOpen, setIsGraphModalOpen] = useState(false); - // Fetch configuration on component mount + // Fetch configuration and namespaces on component mount useEffect(() => { - const fetchConfig = async () => { + const fetchConfigAndNamespaces = async () => { try { - const response = await fetch('/api/config'); - if (response.ok) { - const config = await response.json(); - setNamespace(config.defaultNamespace); + // Fetch both configuration and available namespaces + const [configResponse, namespacesData] = await Promise.all([ + fetch('/api/config'), + clientApiService.getNamespaces() + ]); + + const availableNamespaces = namespacesData.namespaces || []; + setAvailableNamespaces(availableNamespaces); + + // Determine which namespace to select + let selectedNamespace = 'default'; + + if (configResponse.ok) { + const config = await configResponse.json(); + const defaultNamespace = config.defaultNamespace; + + // If default namespace exists in available namespaces, use it + if (availableNamespaces.includes(defaultNamespace)) { + selectedNamespace = defaultNamespace; + } else if (availableNamespaces.length > 0) { + // If default namespace doesn't exist but we have other namespaces, select the first one + selectedNamespace = availableNamespaces[0]; + } + // If no namespaces available from database, keep 'default' as fallback + } else if (availableNamespaces.length > 0) { + // If config fetch failed but we have namespaces, select the first one + selectedNamespace = availableNamespaces[0]; } - } catch (error) { - console.warn('Failed to fetch config, using default namespace'); + + setNamespace(selectedNamespace); + + // If no namespaces available from database, add the selected namespace to the list + if (availableNamespaces.length === 0) { + setAvailableNamespaces([selectedNamespace]); + } + } catch (err) { + console.warn('Failed to fetch config or namespaces, using defaults:', err); + setNamespace('default'); + setAvailableNamespaces(['default']); } }; - fetchConfig(); + fetchConfigAndNamespaces(); }, []); const handleSaveGraphTemplate = async (template: UpsertGraphTemplateRequest) => { @@ -75,6 +115,8 @@ export default function Dashboard() { try { setIsLoading(true); const graphTemplate = await clientApiService.getGraphTemplate(namespace, graphName); + graphTemplate.name = graphName; + graphTemplate.namespace = namespace; setSelectedGraphTemplate(graphTemplate); setIsGraphModalOpen(true); } catch (err) { @@ -89,125 +131,133 @@ export default function Dashboard() { setSelectedGraphTemplate(null); }; - const tabs = [ - { id: 'overview', label: 'Overview', icon: BarChart3 }, - { id: 'graph', label: 'Graph Template', icon: GitBranch }, - { id: 'runs', label: 'Runs', icon: Filter } - ] as const; - return ( -
+
{/* Header */} -
+
-
- Exosphere Logo -

Exosphere Dashboard

+
+ Exosphere Logo +
+

Exosphere Dashboard

+

AI Workflow State Manager

+
- Namespace: - Namespace: +
- {/* Navigation Tabs */} - - {/* Main Content */} -
+
{/* Error Display */} {error && ( -
-
- -
-

Error

-
{error}
-
-
-
+ + + {error} + )} {/* Loading Indicator */} {isLoading && ( -
+
-
- Processing... +
+ Processing...
-
+ )} + {/* Navigation Tabs */} + setActiveTab(value as 'overview' | 'runs')} className="w-full"> + + + + Overview + + {/* + + Graph Template + */} + + + Runs + + - {activeTab === 'graph' && ( -
-
-

Graph Template Builder

- {graphTemplate && ( - - )} -
- -
- )} + + + + + + + - {activeTab === 'overview' && ( - - )} + + + +
+ Graph Template Builder + + Design and configure your AI workflow graph templates + +
+ {graphTemplate && ( + + )} +
+ + + +
+
- {activeTab === 'runs' && ( - - )} + + + + + + + +
{/* Modals */} diff --git a/dashboard/src/components/GraphTemplateBuilder.tsx b/dashboard/src/components/GraphTemplateBuilder.tsx index 1dd2a7e8..83ab2472 100644 --- a/dashboard/src/components/GraphTemplateBuilder.tsx +++ b/dashboard/src/components/GraphTemplateBuilder.tsx @@ -21,7 +21,6 @@ export const GraphTemplateBuilder: React.FC = ({ const [secrets, setSecrets] = React.useState>( graphTemplate?.secrets || {} ); - const [editingNode, setEditingNode] = React.useState(null); const addNode = () => { const newNode: NodeTemplate = { @@ -32,7 +31,6 @@ export const GraphTemplateBuilder: React.FC = ({ next_nodes: [] }; setNodes([...nodes, newNode]); - setEditingNode(nodes.length); }; const updateNode = (index: number, updates: Partial) => { @@ -66,8 +64,10 @@ export const GraphTemplateBuilder: React.FC = ({ }; const removeSecret = (key: string) => { - const { [key]: removed, ...remaining } = secrets; - setSecrets(remaining); + const newSecrets = Object.fromEntries( + Object.entries(secrets).filter(([k]) => k !== key) + ); + setSecrets(newSecrets); }; const handleSave = () => { @@ -76,16 +76,6 @@ export const GraphTemplateBuilder: React.FC = ({ } }; - const getNodeConnections = (nodeIndex: number) => { - const node = nodes[nodeIndex]; - if (!node) return []; - - return node.next_nodes.map(nextNodeId => { - const nextNodeIndex = nodes.findIndex(n => n.identifier === nextNodeId); - return { from: nodeIndex, to: nextNodeIndex, label: nextNodeId }; - }); - }; - return (
@@ -100,189 +90,193 @@ export const GraphTemplateBuilder: React.FC = ({ )}
-
- {/* Nodes Section */} -
-
-
-

Nodes

- {!readOnly && ( - - )} -
- -
- {nodes.map((node, index) => ( -
-
-

- Node {index + 1}: {node.identifier} -

- {!readOnly && ( - - )} -
+ {/* Nodes Section */} +
+
+

Workflow Nodes

+ {!readOnly && ( + + )} +
-
-
- - updateNode(index, { node_name: e.target.value })} - disabled={readOnly} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#031035]" - /> -
+
+ {nodes.map((node, index) => ( +
+
+

Node {index + 1}

+ {!readOnly && ( + + )} +
-
- - updateNode(index, { namespace: e.target.value })} - disabled={readOnly} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#031035]" - /> -
+
+
+ + updateNode(index, { node_name: e.target.value })} + disabled={readOnly} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100" + placeholder="Enter node name" + /> +
-
- - updateNode(index, { identifier: e.target.value })} - disabled={readOnly} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
+
+ + updateNode(index, { namespace: e.target.value })} + disabled={readOnly} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100" + placeholder="Enter namespace" + /> +
-
- - updateNode(index, { - next_nodes: e.target.value.split(',').map(s => s.trim()).filter(Boolean) - })} - disabled={readOnly} - placeholder="node1, node2, node3" - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#031035]" - /> -
-
+
+ + updateNode(index, { identifier: e.target.value })} + disabled={readOnly} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100" + placeholder="Enter unique identifier" + /> +
-
- -