diff --git a/.gitignore b/.gitignore index 1fa7219..19ace18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ +node_modules/ +dist/ .wrangler/ .npmrc +bun.lock +package-lock.json diff --git a/packages/webui/.gitignore b/packages/webui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/packages/webui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/webui/README.md b/packages/webui/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/packages/webui/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/packages/webui/eslint.config.js b/packages/webui/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/packages/webui/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/packages/webui/index.html b/packages/webui/index.html new file mode 100644 index 0000000..bf9c117 --- /dev/null +++ b/packages/webui/index.html @@ -0,0 +1,12 @@ + + + + + + Config Service + + +
+ + + diff --git a/packages/webui/package.json b/packages/webui/package.json new file mode 100644 index 0000000..bb06bab --- /dev/null +++ b/packages/webui/package.json @@ -0,0 +1,35 @@ +{ + "name": "webui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^1.8.0", + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/vite": "^4.2.3", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "tailwindcss": "^4.2.3", + "terser": "^5.46.1", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9", + "vite-plugin-singlefile": "^2.3.3" + } +} diff --git a/packages/webui/src/App.tsx b/packages/webui/src/App.tsx new file mode 100644 index 0000000..bc12875 --- /dev/null +++ b/packages/webui/src/App.tsx @@ -0,0 +1,34 @@ +import { useState, useCallback } from "react"; +import LoginPage from "./components/LoginPage"; +import Dashboard from "./components/Dashboard"; +import Toast from "./components/Toast"; +import type { Toast as ToastType } from "./types"; + +export default function App() { + const [authed, setAuthed] = useState(!!localStorage.getItem("config-token")); + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((message: string, type: "success" | "error") => { + setToasts((prev) => [...prev, { id: Date.now() + Math.random(), message, type }]); + }, []); + + const removeToast = useCallback((id: number) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + const logout = () => { + localStorage.removeItem("config-token"); + setAuthed(false); + }; + + return ( + <> + + {authed ? ( + + ) : ( + setAuthed(true)} /> + )} + + ); +} diff --git a/packages/webui/src/components/AddEditModal.tsx b/packages/webui/src/components/AddEditModal.tsx new file mode 100644 index 0000000..a7036d9 --- /dev/null +++ b/packages/webui/src/components/AddEditModal.tsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +import type { ConfigEntry } from "../types"; + +interface Props { + editKey?: string; + editEntry?: ConfigEntry; + onSave: (key: string, value: string, scope: string, env: boolean, secret: boolean) => void; + onClose: () => void; +} + +export default function AddEditModal({ editKey, editEntry, onSave, onClose }: Props) { + const [key, setKey] = useState(editKey || ""); + const [value, setValue] = useState(editEntry?.value || ""); + const [scope, setScope] = useState(editEntry?.scope || "personal"); + const [excludeEnv, setExcludeEnv] = useState(editEntry ? !editEntry.env : false); + const [secret, setSecret] = useState(editEntry?.secret || false); + + const isEdit = !!editKey; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave(key, value, scope, !excludeEnv, secret); + }; + + return ( +
+
e.stopPropagation()} + onSubmit={handleSubmit} + className="w-full max-w-lg bg-[#12141a] border border-[#1e2030] rounded-2xl p-6 shadow-2xl" + > +

+ {isEdit ? "Edit Key" : "Add Key"} +

+ + + setKey(e.target.value)} + disabled={isEdit} + className="w-full px-3 py-2 mb-4 bg-[#0a0a0c] border border-[#1e2030] rounded-lg text-white disabled:opacity-50 focus:outline-none focus:border-[#3b82f6] transition-colors" + style={{ fontFamily: "var(--font-mono)" }} + placeholder="MY_CONFIG_KEY" + required + /> + + + - - -
- - -
-
- - -
- - - -`; + return "\n\n \n \n \n Config Service\n \n \n \n \n
\n \n\n"; } diff --git a/scripts/build-ui.sh b/scripts/build-ui.sh new file mode 100755 index 0000000..c021574 --- /dev/null +++ b/scripts/build-ui.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +export PATH="$HOME/.bun/bin:$PATH" + +echo "Building webui..." +cd packages/webui +bun run build + +HTML=$(cat dist/index.html) + +echo "Generating ui.ts..." +cat > ../worker/src/ui.ts << 'TSEOF' +export function renderUI(): string { + return UI_HTML; +} +TSEOF + +# Use node to safely embed the HTML as a JS string +node -e " +const fs = require('fs'); +const html = fs.readFileSync('dist/index.html', 'utf8'); +const src = 'export function renderUI(): string {\n return ' + JSON.stringify(html) + ';\n}\n'; +fs.writeFileSync('../worker/src/ui.ts', src); +" + +echo "Done. Generated packages/worker/src/ui.ts"