diff --git a/.changeset/empty-cars-cough.md b/.changeset/empty-cars-cough.md new file mode 100644 index 00000000..58753c59 --- /dev/null +++ b/.changeset/empty-cars-cough.md @@ -0,0 +1,5 @@ +--- +'@elek-io/client': minor +--- + +Added local API for developers to be able to recieve Project data locally e.g. for usage in websites during a build step. The local API is based on the OpenAPI specification. Meaning there is a swagger UI available locally when enabled in the users profile. The API can be used to query the data manually or use the [OpenAPI Generator CLI](https://openapi-generator.tech/) like so: `openapi-generator-cli generate -i ./openapi.json -g typescript-fetch -o ./src/api-client --openapi-normalizer SET_TAGS_FOR_ALL_OPERATIONS=elek-io`, where `SET_TAGS_FOR_ALL_OPERATIONS=elek-io` merges the tagged APIs into one for ease of use. diff --git a/package-lock.json b/package-lock.json index 5811061e..1cb3ff71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@elek-io/client", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@elek-io/client", - "version": "0.1.0", + "version": "0.2.0", "hasInstallScript": true, "license": "todo", "dependencies": { "@electron-toolkit/preload": "3.0.1", "@electron-toolkit/utils": "3.0.0", - "@elek-io/core": "0.11.0", + "@elek-io/core": "0.14.0", "@fontsource-variable/montserrat": "5.0.19", "@fontsource/roboto": "5.0.13", "@hookform/resolvers": "3.9.0", @@ -106,6 +106,17 @@ "node": ">=6.0.0" } }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.1.2.tgz", + "integrity": "sha512-tuDcV4aGAlY4eaZ8Qmf1efPL33hwJKdpCSbI6vJqXU5Wkz9IIyCrb3u3fExZyyMGzmLKcJH+CHI5UKvNBPlyjg==", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^3.20.2" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -1093,17 +1104,21 @@ } }, "node_modules/@elek-io/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@elek-io/core/-/core-0.11.0.tgz", - "integrity": "sha512-KJG5UMFf/JCwBQVSulKQe/FjoQzEUuE34x6bbiubtx4CV93qVkwEaQr0qceGDanx/Lj0tgQLYvmo68LXCBMopA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@elek-io/core/-/core-0.14.0.tgz", + "integrity": "sha512-EL8wP5zt2QBi+El4OmdiowKSlprKYNe0cQR3bvfv9J2JmwQRxDjP0ZKGiWm0k4Zf7LZ/CGwlkJs1hTS4JOxemw==", "dependencies": { + "@hono/node-server": "^1.13.1", + "@hono/swagger-ui": "^0.4.1", + "@hono/zod-openapi": "^0.16.0", "@sindresorhus/slugify": "2.2.1", "fs-extra": "11.2.0", - "mime": "^4.0.4", + "hono": "^4.6.3", + "mime": "4.0.4", "p-queue": "8.0.1", - "semver": "7.6.2", + "semver": "7.6.3", "uuid": "10.0.0", - "winston": "3.14.2", + "winston": "3.15.0", "winston-daily-rotate-file": "5.0.0", "zod": "3.23.8" }, @@ -1865,6 +1880,50 @@ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz", "integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ==" }, + "node_modules/@hono/node-server": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.13.1.tgz", + "integrity": "sha512-TSxE6cT5RHnawbjnveexVN7H2Dpn1YaLxQrCOLCUwD+hFbqbFsnJBgdWcYtASqtWVjA+Qgi8uqFug39GsHjo5A==", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hono/swagger-ui": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@hono/swagger-ui/-/swagger-ui-0.4.1.tgz", + "integrity": "sha512-kPaJatHffeYQ3yVkHo878hCqwfapqx54FczJVJ+eRWt8J4biyVVMIdCAJb6MyA8bcnHUoTmUpPc7OJAV1VTg2g==", + "peerDependencies": { + "hono": "*" + } + }, + "node_modules/@hono/zod-openapi": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-0.16.2.tgz", + "integrity": "sha512-MxlZzvHTXmOihKgIqYrEH5TysIs3Ep0PlTfmdaXe22HR0o2BULbpWW3F7W4I+2jAbt9rxLbxcs3aE2MXnIuAbw==", + "dependencies": { + "@asteasolutions/zod-to-openapi": "^7.1.0", + "@hono/zod-validator": "0.3.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "hono": ">=4.3.6", + "zod": "3.*" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.3.0.tgz", + "integrity": "sha512-7XcTk3yYyk6ldrO/VuqsroE7stvDZxHJQcpATRAyha8rUxJNBPV3+6waDrARfgEqxOVlzIadm3/6sE/dPseXgQ==", + "peerDependencies": { + "hono": ">=3.9.0", + "zod": "^3.19.1" + } + }, "node_modules/@hookform/devtools": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@hookform/devtools/-/devtools-4.3.1.tgz", @@ -9911,6 +9970,14 @@ "react-is": "^16.7.0" } }, + "node_modules/hono": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.3.tgz", + "integrity": "sha512-0LeEuBNFeSHGqZ9sNVVgZjB1V5fmhkBSB0hZrpqStSMLOWgfLy0dHOvrjbJh0H2khsjet6rbHfWTHY0kpYThKQ==", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -11479,6 +11546,14 @@ "fn.name": "1.x.x" } }, + "node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "dependencies": { + "yaml": "^2.5.0" + } + }, "node_modules/opentelemetry-instrumentation-fetch-node": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.0.tgz", @@ -12944,9 +13019,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -14346,9 +14421,9 @@ } }, "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", + "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -14539,9 +14614,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index b1130007..cf5c7739 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "dependencies": { "@electron-toolkit/preload": "3.0.1", "@electron-toolkit/utils": "3.0.0", - "@elek-io/core": "0.11.0", + "@elek-io/core": "0.14.0", "@fontsource-variable/montserrat": "5.0.19", "@fontsource/roboto": "5.0.13", "@hookform/resolvers": "3.9.0", diff --git a/src/main/index.ts b/src/main/index.ts index 6bd46146..c7cad491 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -30,19 +30,20 @@ Sentry.init({ class Main { public readonly customFileProtocol: string = 'elek-io-local-file'; - private allowedOriginsToLoadInternal: string[] = []; - private allowedOriginsToLoadExternal: string[] = [ + private allowedHostnamesToLoadInternal: string[] = []; + private allowedHostnamesToLoadExternal: string[] = [ this.customFileProtocol, - 'https://elek.io', - 'https://api.elek.io', - 'https://github.com', + 'localhost', + 'elek.io', + 'api.elek.io', + 'github.com', ]; private core: ElekIoCore | null = null; constructor() { // Allow the vite dev server to do HMR in development if (app.isPackaged === false && process.env['ELECTRON_RENDERER_URL']) { - this.allowedOriginsToLoadInternal.push( + this.allowedHostnamesToLoadInternal.push( process.env['ELECTRON_RENDERER_URL'] ); } @@ -75,6 +76,11 @@ class Main { this.core = new ElekIoCore({ log: { level: app.isPackaged ? 'info' : 'debug' }, }); + const user = await this.core.user.get(); + + if (user && user.localApi.isEnabled) { + this.core.api.start(user.localApi.port); + } this.registerCustomFileProtocol(); @@ -122,13 +128,13 @@ class Main { const parsedUrl = new URL(url); if ( - this.allowedOriginsToLoadExternal.includes(parsedUrl.origin) === false + this.allowedHostnamesToLoadExternal.includes(parsedUrl.hostname) === + false ) { - Sentry.captureException( - new SecurityError( - `Prevented navigation to untrusted, external origin "${parsedUrl}" from "${webContents.getURL()}"` - ) - ); + const errorMessage = `Prevented navigation to untrusted, external URL "${parsedUrl.toString()}" from "${webContents.getURL()}"`; + Sentry.captureException(new SecurityError(errorMessage)); + this.core?.logger.error(errorMessage); + return { action: 'deny' }; } @@ -143,27 +149,13 @@ class Main { * Creates a new window with security in mind and loads the frontend */ private async createWindow(): Promise { - if (!this.core) { - throw new Error( - 'Trying to create a new window but Core is not initialized.' - ); - } - const initialWindowSize = this.getInitialWindowSize(); - const user = await this.core.user.get(); const options: BrowserWindowConstructorOptions = { width: initialWindowSize.width, height: initialWindowSize.height, }; - if (user?.window) { - options.width = user.window.width; - options.height = user.window.height; - options.x = user.window.position.x; - options.y = user.window.position.y; - } - // Overwrite webPreferences to always load the correct preload script // and explicitly enable security features - although Electron > v28 should set these by default options.webPreferences = { @@ -186,8 +178,6 @@ class Main { this.onWindowWebContentsWillNavigate(window, event, urlToLoad) ); - window.on('close', (event) => this.onWindowClose(window, event)); - if (app.isPackaged) { // Client is in production // Load the static index.html directly @@ -250,61 +240,15 @@ class Main { const parsedUrl = new URL(urlToLoad); if ( - this.allowedOriginsToLoadInternal.includes(parsedUrl.origin) === false + this.allowedHostnamesToLoadInternal.includes(parsedUrl.origin) === false ) { event.preventDefault(); - Sentry.captureException( - new SecurityError( - `Prevented navigation to untrusted, internal origin "${urlToLoad}" from "${window.webContents.getURL()}"` - ) - ); + const errorMessage = `Prevented navigation to untrusted, internal URL "${parsedUrl.toString()}" from "${window.webContents.getURL()}"`; + Sentry.captureException(new SecurityError(errorMessage)); + this.core?.logger.error(errorMessage); } } - /** - * Saves the window size and position inside the users file before the window is closed - */ - private async onWindowClose( - window: BrowserWindow, - event: Electron.Event - ): Promise { - event.preventDefault(); - - if (!this.core) { - Sentry.captureException( - new Error('Trying to close the window but Core is not initialized.') - ); - window.destroy(); - return; - } - - const user = await this.core.user.get(); - const width = window.getSize()[0]; - const height = window.getSize()[1]; - const x = window.getPosition()[0]; - const y = window.getPosition()[1]; - - if (!user || !width || !height || !x || !y) { - window.destroy(); - return; - } - - await this.core.user.set({ - ...user, - window: { - width, - height, - position: { - x, - y, - }, - }, - }); - - window.destroy(); - return; - } - /** * Registers a custom file protocol to load files from the local file system */ @@ -355,17 +299,26 @@ class Main { ipcMain.handle('electron:dialog:showSaveDialog', async (_event, args) => { return await dialog.showSaveDialog(window, args); }); - ipcMain.handle('core:logger:debug', async (_event, args) => { - return await core.logger.debug(args[0]); + ipcMain.handle('core:api:start', async (_event, args) => { + return await core.api.start(args[0]); + }); + ipcMain.handle('core:api:isRunning', async () => { + return await core.api.isRunning(); + }); + ipcMain.handle('core:api:stop', async () => { + return await core.api.stop(); + }); + ipcMain.handle('core:logger:debug', (_event, args) => { + return core.logger.debug(args[0]); }); - ipcMain.handle('core:logger:info', async (_event, args) => { - return await core.logger.info(args[0]); + ipcMain.handle('core:logger:info', (_event, args) => { + return core.logger.info(args[0]); }); - ipcMain.handle('core:logger:warn', async (_event, args) => { - return await core.logger.warn(args[0]); + ipcMain.handle('core:logger:warn', (_event, args) => { + return core.logger.warn(args[0]); }); - ipcMain.handle('core:logger:error', async (_event, args) => { - return await core.logger.error(args[0]); + ipcMain.handle('core:logger:error', (_event, args) => { + return core.logger.error(args[0]); }); ipcMain.handle('core:logger:read', async (_event, args) => { return await core.logger.read(args[0]); diff --git a/src/preload/index.ts b/src/preload/index.ts index 27efaaa6..3ed8b9e0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -13,6 +13,11 @@ export interface ContextBridgeApi { }; }; core: { + api: { + start: ElekIoCore['api']['start']; + isRunning: ElekIoCore['api']['isRunning']; + stop: ElekIoCore['api']['stop']; + }; logger: { debug: ElekIoCore['logger']['debug']; info: ElekIoCore['logger']['info']; @@ -73,6 +78,11 @@ const ipc: ContextBridgeApi = { }, }, core: { + api: { + start: (...args) => ipcRenderer.invoke('core:api:start', args), + isRunning: (...args) => ipcRenderer.invoke('core:api:isRunning', args), + stop: (...args) => ipcRenderer.invoke('core:api:stop', args), + }, logger: { debug: (...args) => ipcRenderer.invoke('core:logger:debug', args), info: (...args) => ipcRenderer.invoke('core:logger:info', args), diff --git a/src/renderer/src/components/ui/commit-history.tsx b/src/renderer/src/components/ui/commit-history.tsx index 30a40481..d15f4fa5 100644 --- a/src/renderer/src/components/ui/commit-history.tsx +++ b/src/renderer/src/components/ui/commit-history.tsx @@ -9,6 +9,7 @@ export interface CommitHistoryProps extends HTMLAttributes { commits: GitCommit[]; language: SupportedLanguage; projectId: string; + disabled?: boolean; } export function CommitHistory({ @@ -16,6 +17,7 @@ export function CommitHistory({ commits, language, projectId, + disabled, ...props }: CommitHistoryProps): JSX.Element { return ( @@ -27,6 +29,7 @@ export function CommitHistory({ key={commit.hash} language={language} commit={commit} + disabled={disabled || false} to="/projects/$projectId/history/$commitHash" params={{ projectId, commitHash: commit.hash }} /> diff --git a/src/renderer/src/components/ui/page-section.tsx b/src/renderer/src/components/ui/page-section.tsx index 4d8d0c9b..826ed0d3 100644 --- a/src/renderer/src/components/ui/page-section.tsx +++ b/src/renderer/src/components/ui/page-section.tsx @@ -9,7 +9,7 @@ export interface PageSectionProps VariantProps { children?: ReactNode; title?: string; - description?: string; + description?: string | ReactElement; actions?: ReactElement; } diff --git a/src/renderer/src/components/ui/project-header.tsx b/src/renderer/src/components/ui/user-header.tsx similarity index 97% rename from src/renderer/src/components/ui/project-header.tsx rename to src/renderer/src/components/ui/user-header.tsx index a6ced012..84416588 100644 --- a/src/renderer/src/components/ui/project-header.tsx +++ b/src/renderer/src/components/ui/user-header.tsx @@ -38,11 +38,11 @@ import { DropdownMenuTrigger, } from './dropdown-menu'; -export interface ProjectHeaderProps extends HTMLAttributes { +export interface UserHeaderProps extends HTMLAttributes { user: User; } -const ProjectHeader = forwardRef( +const UserHeader = forwardRef( ({ user }, ref) => { const router = useRouter(); const routerState = useRouterState(); @@ -268,6 +268,6 @@ const ProjectHeader = forwardRef( ); } ); -ProjectHeader.displayName = 'ProjectHeader'; +UserHeader.displayName = 'ProjectHeader'; -export { ProjectHeader }; +export { UserHeader }; diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts index 07b2b82a..a638198a 100644 --- a/src/renderer/src/routeTree.gen.ts +++ b/src/renderer/src/routeTree.gen.ts @@ -15,7 +15,6 @@ import { Route as ProjectsImport } from './routes/projects' import { Route as IndexImport } from './routes/index' import { Route as UserIndexImport } from './routes/user/index' import { Route as ProjectsIndexImport } from './routes/projects/index' -import { Route as UserSetupImport } from './routes/user/setup' import { Route as UserProfileImport } from './routes/user/profile' import { Route as ProjectsCreateImport } from './routes/projects/create' import { Route as ProjectsProjectIdImport } from './routes/projects/$projectId' @@ -61,11 +60,6 @@ const ProjectsIndexRoute = ProjectsIndexImport.update({ getParentRoute: () => ProjectsRoute, } as any) -const UserSetupRoute = UserSetupImport.update({ - path: '/user/setup', - getParentRoute: () => rootRoute, -} as any) - const UserProfileRoute = UserProfileImport.update({ path: '/user/profile', getParentRoute: () => rootRoute, @@ -232,13 +226,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UserProfileImport parentRoute: typeof rootRoute } - '/user/setup': { - id: '/user/setup' - path: '/user/setup' - fullPath: '/user/setup' - preLoaderRoute: typeof UserSetupImport - parentRoute: typeof rootRoute - } '/projects/': { id: '/projects/' path: '/' @@ -430,7 +417,6 @@ export const routeTree = rootRoute.addChildren({ ProjectsIndexRoute, }), UserProfileRoute, - UserSetupRoute, UserIndexRoute, }) @@ -445,7 +431,6 @@ export const routeTree = rootRoute.addChildren({ "/", "/projects", "/user/profile", - "/user/setup", "/user/" ] }, @@ -479,9 +464,6 @@ export const routeTree = rootRoute.addChildren({ "/user/profile": { "filePath": "user/profile.tsx" }, - "/user/setup": { - "filePath": "user/setup.tsx" - }, "/projects/": { "filePath": "projects/index.tsx", "parent": "/projects" diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index 97003e37..6c06987b 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ - beforeLoad: () => { + beforeLoad: async () => { throw redirect({ to: '/projects', }); diff --git a/src/renderer/src/routes/projects.tsx b/src/renderer/src/routes/projects.tsx index 6ae013c4..e9cb582e 100644 --- a/src/renderer/src/routes/projects.tsx +++ b/src/renderer/src/routes/projects.tsx @@ -1,4 +1,4 @@ -import { ProjectHeader } from '@renderer/components/ui/project-header'; +import { UserHeader } from '@renderer/components/ui/user-header'; import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/projects')({ @@ -6,7 +6,7 @@ export const Route = createFileRoute('/projects')({ const user = await context.core.user.get(); if (!user) { throw redirect({ - to: '/user/setup', + to: '/user/profile', }); } @@ -20,7 +20,7 @@ function ProjectsLayout(): JSX.Element { return ( <> - + ); diff --git a/src/renderer/src/routes/user/index.tsx b/src/renderer/src/routes/user/index.tsx index ad9fd071..a0ffe0ee 100644 --- a/src/renderer/src/routes/user/index.tsx +++ b/src/renderer/src/routes/user/index.tsx @@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/user/')({ beforeLoad: async () => { throw redirect({ - to: '/user/setup', + to: '/user/profile', }); }, }); diff --git a/src/renderer/src/routes/user/profile.tsx b/src/renderer/src/routes/user/profile.tsx index c4a9efdc..3ca7d787 100644 --- a/src/renderer/src/routes/user/profile.tsx +++ b/src/renderer/src/routes/user/profile.tsx @@ -1,14 +1,16 @@ import { + GitCommit, SetUserProps, setUserSchema, supportedLanguageSchema, } from '@elek-io/core'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@renderer/components/ui/button'; -import { Commit } from '@renderer/components/ui/commit'; +import { CommitHistory } from '@renderer/components/ui/commit-history'; import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -24,6 +26,8 @@ import { SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; +import { Switch } from '@renderer/components/ui/switch'; +import { UserHeader } from '@renderer/components/ui/user-header'; import { NotificationIntent, useStore } from '@renderer/store'; import { createFileRoute, useRouter } from '@tanstack/react-router'; import { Check } from 'lucide-react'; @@ -31,6 +35,11 @@ import { ReactElement, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; export const Route = createFileRoute('/user/profile')({ + beforeLoad: async ({ context }) => { + const user = await context.core.user.get(); + + return { user }; + }, component: UserProfilePage, }); @@ -45,23 +54,55 @@ function UserProfilePage(): JSX.Element { }, defaultValues: { userType: 'local', - name: 'John Doe', - email: 'john.doe@example.com', - language: 'en', - window: null, + name: context.user?.name || '', + email: context.user?.email || '', + language: context.user?.language || 'en', + localApi: { + port: context.user?.localApi.port || 31310, + isEnabled: context.user?.localApi.isEnabled || false, + }, }, }); + const exampleCommit: GitCommit = { + hash: '1234567890', + author: { + name: setUserForm.watch('name'), + email: setUserForm.watch('email'), + }, + datetime: new Date().toISOString(), + tag: null, + message: { + method: 'create', + reference: { + objectType: 'project', + id: '1', + }, + }, + }; + const localApiUrl = `http://localhost:${setUserForm.watch('localApi.port')}/v1/ui`; function Description(): ReactElement { + if (context.user === null) { + return ( + <> + Before we start you need to set up a local User first. Don't worry, + you are not creating an account and this information is only saved + locally on this device! Read more about{' '} + + local Users in our documentation + {' '} + if you have questions about this step. + + ); + } + return ( <> - Before we start you need to set up a local User first. Don't worry, you - are not creating an account and this information is only saved locally - on this device! Read more about{' '} + By editing your Users details, ... Read more about{' '} local Users in our documentation - {' '} - if you have questions about this step. + + . ); } @@ -80,10 +121,35 @@ function UserProfilePage(): JSX.Element { ); } + function LocalApiDescription(): ReactElement { + return ( + <> + Use the local API to read data from your local Projects. The local API + is only accessible on this device. You can access it at{' '} + + {localApiUrl} + {' '} + once it is enabled. + + ); + } + const onSetUser: SubmitHandler = async (props) => { setIsSettingUser(true); try { await context.core.user.set(props); + const isLocalApiRunning = await context.core.api.isRunning(); + + console.log('Local API is running:', isLocalApiRunning); + + if (props.localApi.isEnabled === true && isLocalApiRunning === false) { + context.core.api.start(props.localApi.port); + } + + if (props.localApi.isEnabled === false && isLocalApiRunning === true) { + context.core.api.stop(); + } + setIsSettingUser(false); await router.navigate({ to: '/projects' }); addNotification({ @@ -103,107 +169,175 @@ function UserProfilePage(): JSX.Element { }; return ( - } - actions={} - layout="bare" - > -
-
-
- + <> + {context.user && } + } + actions={} + layout="bare" + > +
+
+
- - ( - - - Preferred language - - - - - - - )} - /> - ( - - Full name - - - - - - )} - /> - ( - - Email - - - - - - )} - /> + + + ( + + + Preferred language + + + + + + Changing your preferred language will attempt to + translate everything you see in elek.io Client. You + can help translate elek.io Client by contributing to + our{' '} + + GitHub repository + + . + + + + )} + /> + ( + + Full name + + + + + Your name will be used by others to identify changes + made by you. + + + + )} + /> + ( + + Email + + + + + Your email allows other members of Projects to + contact you. + + + + )} + /> + + } + className="space-y-6" + > + ( + + Port + + + + + The port used to access the local API. Make sure it + is not in use by another application. + + + + )} + /> + ( + +
+ Enabled + + Enabling the local API allows you to read local + Project data. + +
+ + + + +
+ )} + /> +
- -
-
- -
-
+
+
+ +
+
+ ); } diff --git a/src/renderer/src/routes/user/setup.tsx b/src/renderer/src/routes/user/setup.tsx deleted file mode 100644 index f352463a..00000000 --- a/src/renderer/src/routes/user/setup.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { - SetUserProps, - setUserSchema, - supportedLanguageSchema, -} from '@elek-io/core'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Button } from '@renderer/components/ui/button'; -import { Commit } from '@renderer/components/ui/commit'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@renderer/components/ui/form'; -import { FormInput } from '@renderer/components/ui/form-input'; -import { Page } from '@renderer/components/ui/page'; -import { PageSection } from '@renderer/components/ui/page-section'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@renderer/components/ui/select'; -import { NotificationIntent, useStore } from '@renderer/store'; -import { createFileRoute, useRouter } from '@tanstack/react-router'; -import { Check } from 'lucide-react'; -import { ReactElement, useState } from 'react'; -import { SubmitHandler, useForm } from 'react-hook-form'; - -export const Route = createFileRoute('/user/setup')({ - component: SetupUserPage, -}); - -function SetupUserPage(): JSX.Element { - const router = useRouter(); - const context = Route.useRouteContext(); - const addNotification = useStore((state) => state.addNotification); - const [isSettingUser, setIsSettingUser] = useState(false); - const setUserForm = useForm({ - resolver: async (data, context, options) => { - return zodResolver(setUserSchema)(data, context, options); - }, - defaultValues: { - userType: 'local', - name: 'John Doe', - email: 'john.doe@example.com', - language: 'en', - window: null, - }, - }); - - function Description(): ReactElement { - return ( - <> - Before we start you need to set up a local User first. Don't worry, you - are not creating an account and this information is only saved locally - on this device! Read more about{' '} - - local Users in our documentation - {' '} - if you have questions about this step. - - ); - } - - function Actions(): ReactElement { - return ( - <> - - - ); - } - - const onSetUser: SubmitHandler = async (props) => { - setIsSettingUser(true); - try { - await context.core.user.set(props); - setIsSettingUser(false); - await router.navigate({ to: '/projects' }); - addNotification({ - intent: NotificationIntent.SUCCESS, - title: 'Successfully setup User', - description: 'The User was successfully setup.', - }); - } catch (error) { - setIsSettingUser(false); - console.error(error); - addNotification({ - intent: NotificationIntent.DANGER, - title: 'Failed to setup User', - description: 'There was an error setting up the User.', - }); - } - }; - - return ( - } - actions={} - layout="bare" - > -
-
-
- -
- - ( - - - Preferred language - - - - - - - )} - /> - ( - - Full name - - - - - - )} - /> - ( - - Email - - - - - - )} - /> - - -
-
-
- -
-
- ); -}