From 1fb0975b09ccf7d0da6a34228cb3aedffe6cbdfd Mon Sep 17 00:00:00 2001 From: Hakan Shehu Date: Thu, 26 Sep 2024 18:38:19 +0200 Subject: [PATCH] Init server features --- .../components/accounts/email-register.tsx | 17 ++- .../src/components/accounts/login-form.tsx | 121 ++++++++++++++++++ desktop/src/components/accounts/login.tsx | 72 +---------- desktop/src/components/app.tsx | 1 + desktop/src/data/app-manager.ts | 29 +++++ desktop/src/data/migrations/app.ts | 65 +++++++++- desktop/src/data/schemas/app.ts | 16 +++ desktop/src/data/server-manager.ts | 9 ++ desktop/src/queries/use-servers-query.tsx | 41 ++++++ desktop/src/types/accounts.ts | 1 + desktop/src/types/servers.ts | 13 ++ 11 files changed, 310 insertions(+), 75 deletions(-) create mode 100644 desktop/src/components/accounts/login-form.tsx create mode 100644 desktop/src/data/server-manager.ts create mode 100644 desktop/src/queries/use-servers-query.tsx create mode 100644 desktop/src/types/servers.ts diff --git a/desktop/src/components/accounts/email-register.tsx b/desktop/src/components/accounts/email-register.tsx index 0a4d4c33..67220f32 100644 --- a/desktop/src/components/accounts/email-register.tsx +++ b/desktop/src/components/accounts/email-register.tsx @@ -14,9 +14,9 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoginOutput } from '@/types/accounts'; import { toast } from '@/components/ui/use-toast'; -import {parseApiError} from "@/lib/axios"; -import {Icon} from "@/components/ui/icon"; -import axios from "axios"; +import { parseApiError } from '@/lib/axios'; +import { Icon } from '@/components/ui/icon'; +import axios from 'axios'; const formSchema = z.object({ name: z.string().min(2), @@ -29,7 +29,10 @@ interface EmailRegisterProps { onRegister: (output: LoginOutput) => void; } -export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) => { +export const EmailRegister = ({ + serverUrl, + onRegister, +}: EmailRegisterProps) => { const [isPending, setIsPending] = React.useState(false); const form = useForm>({ @@ -46,7 +49,7 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) => try { const { data } = await axios.post( `${serverUrl}/v1/accounts/register/email`, - values + values, ); onRegister(data); @@ -60,7 +63,7 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) => } finally { setIsPending(false); } - } + }; return (
@@ -117,4 +120,4 @@ export const EmailRegister = ({ serverUrl, onRegister }: EmailRegisterProps) =>
); -} +}; diff --git a/desktop/src/components/accounts/login-form.tsx b/desktop/src/components/accounts/login-form.tsx new file mode 100644 index 00000000..622b6e26 --- /dev/null +++ b/desktop/src/components/accounts/login-form.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { useAppDatabase } from '@/contexts/app-database'; +import { LoginOutput } from '@/types/accounts'; +import { Server } from '@/types/servers'; +import { EmailRegister } from '@/components/accounts/email-register'; +import { EmailLogin } from '@/components/accounts/email-login'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Icon } from '@/components/ui/icon'; +import { Avatar } from '@/components/ui/avatar'; + +interface LoginFormProps { + servers: Server[]; +} + +export const LoginForm = ({ servers }: LoginFormProps) => { + const appDatabase = useAppDatabase(); + const [showRegister, setShowRegister] = React.useState(false); + const [server, setServer] = React.useState(servers[0]); + const serverUrl = `https://${server.domain}`; + + const handleLogin = async (output: LoginOutput) => { + const insertAccountQuery = appDatabase.database + .insertInto('accounts') + .values({ + id: output.account.id, + name: output.account.name, + avatar: output.account.avatar, + device_id: output.account.deviceId, + email: output.account.email, + token: output.account.token, + }) + .compile(); + + await appDatabase.mutate(insertAccountQuery); + + if (output.workspaces.length > 0) { + const insertWorkspacesQuery = appDatabase.database + .insertInto('workspaces') + .values( + output.workspaces.map((workspace) => ({ + id: workspace.id, + name: workspace.name, + account_id: output.account.id, + avatar: workspace.avatar, + role: workspace.role, + description: workspace.description, + synced: 0, + user_id: workspace.userId, + version_id: workspace.versionId, + })), + ) + .compile(); + + await appDatabase.mutate(insertWorkspacesQuery); + } + + window.location.href = '/'; + }; + + return ( +
+
+ + +
+ +
+

{server.name}

+

{server.domain}

+
+ + +
+
+ + {servers.map((server) => ( + { + setServer(server); + }} + className="flex w-full flex-grow flex-row items-center gap-2 rounded-md border-b border-input p-2 hover:cursor-pointer hover:bg-gray-100" + > + +
+

{server.name}

+

+ {server.domain} +

+
+
+ ))} +
+
+
+ {showRegister ? ( + + ) : ( + + )} +

{ + setShowRegister(!showRegister); + }} + > + {showRegister + ? 'Already have an account? Login' + : 'No account yet? Register'} +

+
+ ); +}; diff --git a/desktop/src/components/accounts/login.tsx b/desktop/src/components/accounts/login.tsx index 94131538..6f47112e 100644 --- a/desktop/src/components/accounts/login.tsx +++ b/desktop/src/components/accounts/login.tsx @@ -1,86 +1,26 @@ import React from 'react'; -import { EmailLogin } from '@/components/accounts/email-login'; -import { LoginOutput } from '@/types/accounts'; -import { EmailRegister } from '@/components/accounts/email-register'; -import { useAppDatabase } from '@/contexts/app-database'; - -const SERVER_URL = 'http://localhost:3000'; +import { LoginForm } from './login-form'; +import { useServersQuery } from '@/queries/use-servers-query'; export const Login = () => { - const appDatabase = useAppDatabase(); - const [showRegister, setShowRegister] = React.useState(false); - - const handleLogin = async (output: LoginOutput) => { - const insertAccountQuery = appDatabase.database - .insertInto('accounts') - .values({ - id: output.account.id, - name: output.account.name, - avatar: output.account.avatar, - device_id: output.account.deviceId, - email: output.account.email, - token: output.account.token, - }) - .compile(); - - await appDatabase.mutate(insertAccountQuery); - - if (output.workspaces.length > 0) { - const insertWorkspacesQuery = appDatabase.database - .insertInto('workspaces') - .values( - output.workspaces.map((workspace) => ({ - id: workspace.id, - name: workspace.name, - account_id: output.account.id, - avatar: workspace.avatar, - role: workspace.role, - description: workspace.description, - synced: 0, - user_id: workspace.userId, - version_id: workspace.versionId, - })), - ) - .compile(); - - await appDatabase.mutate(insertWorkspacesQuery); - } - - window.location.href = '/'; - }; + const { data, isPending } = useServersQuery(); return (
-

neuron

+

colabron

- Login to Neuron + Login to Colabron

Use one of the following methods to login

-
- {showRegister ? ( - - ) : ( - - )} -

{ - setShowRegister(!showRegister); - }} - > - {showRegister - ? 'Already have an account? Login' - : 'No account yet? Register'} -

-
+ {isPending ? null : }
diff --git a/desktop/src/components/app.tsx b/desktop/src/components/app.tsx index f8f99a9c..a4c881e7 100644 --- a/desktop/src/components/app.tsx +++ b/desktop/src/components/app.tsx @@ -116,6 +116,7 @@ export const App = () => { email: account.email, deviceId: account.device_id, status: account.status, + server: account.server, workspaces: workspacesQuery.data?.rows.map((row) => { return { id: row.id, diff --git a/desktop/src/data/app-manager.ts b/desktop/src/data/app-manager.ts index df4dba40..6f37fa29 100644 --- a/desktop/src/data/app-manager.ts +++ b/desktop/src/data/app-manager.ts @@ -8,6 +8,7 @@ import { SqliteDialect, } from 'kysely'; import { AccountManager } from '@/data/account-manager'; +import { ServerManager } from '@/data/server-manager'; import { AppDatabaseSchema } from '@/data/schemas/app'; import { appDatabaseMigrations } from '@/data/migrations/app'; import { Account } from '@/types/accounts'; @@ -20,10 +21,12 @@ import { Workspace } from '@/types/workspaces'; import { SubscribedQueryContext, SubscribedQueryResult } from '@/types/queries'; import { eventBus } from '@/lib/event-bus'; import { isEqual } from 'lodash'; +import { Server } from '@/types/servers'; const EVENT_LOOP_INTERVAL = 1000; class AppManager { + private readonly servers: Map; private readonly accounts: Map; private readonly appPath: string; private readonly database: Kysely; @@ -209,6 +212,12 @@ class AppManager { private async executeInit(): Promise { await this.migrate(); + const servers = await this.getServers(); + for (const server of servers) { + const serverManager = new ServerManager(server); + this.servers.set(server.domain, serverManager); + } + const accounts = await this.getAccounts(); const workspaces = await this.getWorkspaces(); for (const account of accounts) { @@ -265,6 +274,25 @@ class AppManager { await migrator.migrateToLatest(); } + private async getServers(): Promise { + const servers = await this.database + .selectFrom('servers') + .selectAll() + .execute(); + + return servers.map((server) => ({ + domain: server.domain, + name: server.name, + avatar: server.avatar, + attributes: JSON.parse(server.attributes), + version: server.version, + createdAt: new Date(server.created_at), + lastSyncedAt: server.last_synced_at + ? new Date(server.last_synced_at) + : null, + })); + } + private async getAccounts(): Promise { const accounts = await this.database .selectFrom('accounts') @@ -278,6 +306,7 @@ class AppManager { avatar: account.avatar, token: account.token, deviceId: account.device_id, + server: account.server, status: account.status, })); } diff --git a/desktop/src/data/migrations/app.ts b/desktop/src/data/migrations/app.ts index 4d19c20d..b68aad7b 100644 --- a/desktop/src/data/migrations/app.ts +++ b/desktop/src/data/migrations/app.ts @@ -1,11 +1,70 @@ import { Migration } from 'kysely'; +const createServersTable: Migration = { + up: async (db) => { + await db.schema + .createTable('servers') + .addColumn('domain', 'text', (col) => col.notNull().primaryKey()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('avatar', 'text', (col) => col.notNull()) + .addColumn('attributes', 'text', (col) => col.notNull()) + .addColumn('version', 'text', (col) => col.notNull()) + .addColumn('created_at', 'text', (col) => col.notNull()) + .addColumn('last_synced_at', 'text') + .execute(); + }, + down: async (db) => { + await db.schema.dropTable('servers').execute(); + }, +}; + +const addNeuronServers: Migration = { + up: async (db) => { + await db + .insertInto('servers') + .values([ + { + domain: 'localhost:3000', + name: 'Localhost', + avatar: '', + attributes: '{}', + version: '0.1.0', + created_at: new Date().toISOString(), + }, + { + domain: 'eu.neuronapp.io', + name: 'Neuron Cloud (EU)', + avatar: '', + attributes: '{}', + version: '0.1.0', + created_at: new Date().toISOString(), + }, + { + domain: 'us.neuronapp.io', + name: 'Neuron Cloud (US)', + avatar: '', + attributes: '{}', + version: '0.1.0', + created_at: new Date().toISOString(), + }, + ]) + .execute(); + }, + down: async (db) => { + await db + .deleteFrom('servers') + .where('id', 'in', ['localhost', 'eu.neuronapp.io', 'us.neuronapp.io']) + .execute(); + }, +}; + const createAccountsTable: Migration = { up: async (db) => { await db.schema .createTable('accounts') .addColumn('id', 'text', (col) => col.notNull().primaryKey()) .addColumn('device_id', 'text', (col) => col.notNull()) + .addColumn('server', 'text', (col) => col.notNull()) .addColumn('name', 'text', (col) => col.notNull()) .addColumn('email', 'text', (col) => col.notNull()) .addColumn('avatar', 'text') @@ -41,6 +100,8 @@ const createWorkspacesTable: Migration = { }; export const appDatabaseMigrations: Record = { - '00001_create_accounts_table': createAccountsTable, - '00002_create_workspaces_table': createWorkspacesTable, + '00001_create_servers_table': createServersTable, + '00002_add_neuron_servers': addNeuronServers, + '00003_create_accounts_table': createAccountsTable, + '00004_create_workspaces_table': createWorkspacesTable, }; diff --git a/desktop/src/data/schemas/app.ts b/desktop/src/data/schemas/app.ts index 0d2ddc6c..4daf4cfd 100644 --- a/desktop/src/data/schemas/app.ts +++ b/desktop/src/data/schemas/app.ts @@ -1,7 +1,22 @@ import { ColumnType, Insertable, Selectable, Updateable } from 'kysely'; +interface ServerTable { + domain: ColumnType; + name: ColumnType; + avatar: ColumnType; + attributes: ColumnType; + version: ColumnType; + created_at: ColumnType; + last_synced_at: ColumnType; +} + +export type SelectServer = Selectable; +export type CreateServer = Insertable; +export type UpdateServer = Updateable; + interface AccountTable { id: ColumnType; + server: ColumnType; name: ColumnType; email: ColumnType; avatar: ColumnType; @@ -31,6 +46,7 @@ export type CreateWorkspace = Insertable; export type UpdateWorkspace = Updateable; export interface AppDatabaseSchema { + servers: ServerTable; accounts: AccountTable; workspaces: WorkspaceTable; } diff --git a/desktop/src/data/server-manager.ts b/desktop/src/data/server-manager.ts new file mode 100644 index 00000000..aab363e2 --- /dev/null +++ b/desktop/src/data/server-manager.ts @@ -0,0 +1,9 @@ +import { Server } from '@/types/servers'; + +export class ServerManager { + public readonly server: Server; + + constructor(server: Server) { + this.server = server; + } +} diff --git a/desktop/src/queries/use-servers-query.tsx b/desktop/src/queries/use-servers-query.tsx new file mode 100644 index 00000000..62929b62 --- /dev/null +++ b/desktop/src/queries/use-servers-query.tsx @@ -0,0 +1,41 @@ +import { useAppDatabase } from '@/contexts/app-database'; +import { SelectServer } from '@/data/schemas/app'; +import { Server } from '@/types/servers'; +import { useQuery } from '@tanstack/react-query'; +import { QueryResult } from 'kysely'; + +export const useServersQuery = () => { + const appDatabase = useAppDatabase(); + return useQuery, Error, Server[], string[]>({ + queryKey: ['servers'], + queryFn: async ({ queryKey }) => { + const query = appDatabase.database + .selectFrom('servers') + .selectAll() + .compile(); + + const servers = await appDatabase.queryAndSubscribe({ + key: ['servers'], + query: query, + }); + + return servers; + }, + select: (data: QueryResult): Server[] => { + const rows = data?.rows ?? []; + return rows.map(mapServer); + }, + }); +}; + +const mapServer = (row: SelectServer): Server => { + return { + domain: row.domain, + name: row.name, + avatar: row.avatar, + attributes: JSON.parse(row.attributes), + version: row.version, + createdAt: new Date(row.created_at), + lastSyncedAt: row.last_synced_at ? new Date(row.last_synced_at) : null, + }; +}; diff --git a/desktop/src/types/accounts.ts b/desktop/src/types/accounts.ts index 5eb32e28..2996db64 100644 --- a/desktop/src/types/accounts.ts +++ b/desktop/src/types/accounts.ts @@ -13,4 +13,5 @@ export type Account = { token: string; deviceId: string; status: string; + server: string; }; diff --git a/desktop/src/types/servers.ts b/desktop/src/types/servers.ts new file mode 100644 index 00000000..d7fd05bb --- /dev/null +++ b/desktop/src/types/servers.ts @@ -0,0 +1,13 @@ +export type Server = { + domain: string; + name: string; + avatar: string; + attributes: ServerAttributes; + version: string; + createdAt: Date; + lastSyncedAt: Date | null; +}; + +export type ServerAttributes = { + name: string; +};