mirror of
https://github.com/colanode/colanode.git
synced 2026-08-29 01:58:54 +02:00
Improve server loading and syncing (#253)
This commit is contained in:
@@ -186,6 +186,10 @@ const initApp = async (): Promise<AppInitOutput> => {
|
||||
await app.metadata.delete('app', 'theme.mode');
|
||||
}
|
||||
|
||||
// add default Colanode servers
|
||||
await app.createServer(new URL('https://eu.colanode.com/config'));
|
||||
await app.createServer(new URL('https://us.colanode.com/config'));
|
||||
|
||||
return 'success';
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
import { QueryInput, QueryMap } from '@colanode/client/queries';
|
||||
import { AppMeta, AppService } from '@colanode/client/services';
|
||||
import { AppInitOutput } from '@colanode/client/types';
|
||||
import { build, extractFileSubtype, generateId, IdType } from '@colanode/core';
|
||||
import {
|
||||
build,
|
||||
extractFileSubtype,
|
||||
generateId,
|
||||
IdType,
|
||||
isColanodeDomain,
|
||||
} from '@colanode/core';
|
||||
import {
|
||||
BroadcastInitMessage,
|
||||
BroadcastMessage,
|
||||
@@ -72,6 +78,12 @@ navigator.locks.request('colanode', async () => {
|
||||
await app.metadata.set('app', 'version', build.version);
|
||||
await app.metadata.set('app', 'platform', appMeta.platform);
|
||||
|
||||
const domain = self.location.hostname;
|
||||
if (isColanodeDomain(domain)) {
|
||||
await app.createServer(new URL('https://eu.colanode.com/config'));
|
||||
await app.createServer(new URL('https://us.colanode.com/config'));
|
||||
}
|
||||
|
||||
appInitOutput = 'success';
|
||||
|
||||
broadcastMessage({
|
||||
|
||||
@@ -62,6 +62,7 @@ import { RecordFieldValueSetMutationHandler } from './records/record-field-value
|
||||
import { RecordNameUpdateMutationHandler } from './records/record-name-update';
|
||||
import { ServerCreateMutationHandler } from './servers/server-create';
|
||||
import { ServerDeleteMutationHandler } from './servers/server-delete';
|
||||
import { ServerSyncMutationHandler } from './servers/server-sync';
|
||||
import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder';
|
||||
import { SpaceCreateMutationHandler } from './spaces/space-create';
|
||||
import { SpaceDeleteMutationHandler } from './spaces/space-delete';
|
||||
@@ -121,6 +122,7 @@ export const buildMutationHandlerMap = (
|
||||
'select.option.update': new SelectOptionUpdateMutationHandler(app),
|
||||
'server.create': new ServerCreateMutationHandler(app),
|
||||
'server.delete': new ServerDeleteMutationHandler(app),
|
||||
'server.sync': new ServerSyncMutationHandler(app),
|
||||
'space.create': new SpaceCreateMutationHandler(app),
|
||||
'space.delete': new SpaceDeleteMutationHandler(app),
|
||||
'user.role.update': new UserRoleUpdateMutationHandler(app),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ServerDeleteMutationOutput,
|
||||
} from '@colanode/client/mutations/servers/server-delete';
|
||||
import { AppService } from '@colanode/client/services/app-service';
|
||||
import { isColanodeServer } from '@colanode/core';
|
||||
import { isColanodeDomain } from '@colanode/core';
|
||||
|
||||
export class ServerDeleteMutationHandler
|
||||
implements MutationHandler<ServerDeleteMutationInput>
|
||||
@@ -19,7 +19,7 @@ export class ServerDeleteMutationHandler
|
||||
async handleMutation(
|
||||
input: ServerDeleteMutationInput
|
||||
): Promise<ServerDeleteMutationOutput> {
|
||||
if (isColanodeServer(input.domain)) {
|
||||
if (isColanodeDomain(input.domain)) {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.ServerDeleteForbidden,
|
||||
'Cannot delete Colanode server'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import ms from 'ms';
|
||||
|
||||
import { MutationHandler } from '@colanode/client/lib/types';
|
||||
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
|
||||
import {
|
||||
ServerSyncMutationInput,
|
||||
ServerSyncMutationOutput,
|
||||
} from '@colanode/client/mutations/servers/server-sync';
|
||||
import { AppService } from '@colanode/client/services/app-service';
|
||||
|
||||
export class ServerSyncMutationHandler
|
||||
implements MutationHandler<ServerSyncMutationInput>
|
||||
{
|
||||
private readonly app: AppService;
|
||||
|
||||
constructor(app: AppService) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async handleMutation(
|
||||
input: ServerSyncMutationInput
|
||||
): Promise<ServerSyncMutationOutput> {
|
||||
const server = this.app.getServer(input.domain);
|
||||
if (!server) {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.ServerNotFound,
|
||||
`Server ${input.domain} was not found! Try using a different server.`
|
||||
);
|
||||
}
|
||||
|
||||
// no need to sync if the server has been synced in the last minute
|
||||
const lastSyncedAt = server.server.syncedAt;
|
||||
if (lastSyncedAt && lastSyncedAt.getTime() > Date.now() - ms('1 minute')) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
const success = await server.sync();
|
||||
return {
|
||||
success,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import ms from 'ms';
|
||||
|
||||
import {
|
||||
JobHandler,
|
||||
JobOutput,
|
||||
@@ -38,6 +40,20 @@ export class ServerSyncJobHandler implements JobHandler<ServerSyncInput> {
|
||||
};
|
||||
}
|
||||
|
||||
const accounts = this.app
|
||||
.getAccounts()
|
||||
.filter((account) => account.server.domain === server.domain);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
// don't sync if the server has no active accounts and has been synced in the last day
|
||||
const lastSyncedAt = server.server.syncedAt;
|
||||
if (lastSyncedAt && lastSyncedAt.getTime() > Date.now() - ms('1 day')) {
|
||||
return {
|
||||
type: 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await server.sync();
|
||||
return {
|
||||
type: 'success',
|
||||
|
||||
@@ -67,6 +67,7 @@ export * from './users/users-create';
|
||||
export * from './files/temp-file-create';
|
||||
export * from './apps/tab-create';
|
||||
export * from './apps/tab-update';
|
||||
export * from './servers/server-sync';
|
||||
export * from './apps/tab-delete';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
|
||||
17
packages/client/src/mutations/servers/server-sync.ts
Normal file
17
packages/client/src/mutations/servers/server-sync.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ServerSyncMutationInput = {
|
||||
type: 'server.sync';
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type ServerSyncMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@colanode/client/mutations' {
|
||||
interface MutationMap {
|
||||
'server.sync': {
|
||||
input: ServerSyncMutationInput;
|
||||
output: ServerSyncMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export class ServerService {
|
||||
await this.app.jobs.triggerJobSchedule(scheduleId);
|
||||
}
|
||||
|
||||
public async sync() {
|
||||
public async sync(): Promise<boolean> {
|
||||
const config = await ServerService.fetchServerConfig(this.configUrl);
|
||||
if (config) {
|
||||
const attributes: ServerAttributes = {
|
||||
@@ -158,6 +158,8 @@ export class ServerService {
|
||||
type: 'server.updated',
|
||||
server: this.server,
|
||||
});
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
public static async fetchServerConfig(configUrl: URL | string) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const isColanodeServer = (domain: string) => {
|
||||
export const isColanodeDomain = (domain: string) => {
|
||||
return domain.endsWith('.colanode.com');
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useLiveQuery } from '@tanstack/react-db';
|
||||
import { PlusIcon, SettingsIcon } from 'lucide-react';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { collections } from '@colanode/ui/collections';
|
||||
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
|
||||
import { ServerCard } from '@colanode/ui/components/servers/server-card';
|
||||
import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog';
|
||||
import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog';
|
||||
import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog';
|
||||
|
||||
interface AuthServerProps {
|
||||
onSelect: (server: Server) => void;
|
||||
@@ -15,16 +13,11 @@ interface AuthServerProps {
|
||||
|
||||
export const AuthServer = ({ onSelect }: AuthServerProps) => {
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [settingsDomain, setSettingsDomain] = useState<string | null>(null);
|
||||
const [deleteDomain, setDeleteDomain] = useState<string | null>(null);
|
||||
|
||||
const serversQuery = useLiveQuery((q) =>
|
||||
q.from({ servers: collections.servers })
|
||||
);
|
||||
const servers = serversQuery.data ?? [];
|
||||
const settingsServer = servers.find(
|
||||
(server) => server.domain === settingsDomain
|
||||
);
|
||||
const deleteServer = servers.find((server) => server.domain === deleteDomain);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
@@ -40,31 +33,7 @@ export const AuthServer = ({ onSelect }: AuthServerProps) => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{servers.map((server) => (
|
||||
<button
|
||||
key={server.domain}
|
||||
onClick={() => onSelect(server)}
|
||||
className="group/server relative flex w-full flex-row items-center gap-3 rounded-lg border border-border/60 bg-background p-2 text-left transition-all hover:cursor-pointer hover:border-border hover:bg-accent hover:shadow-md"
|
||||
>
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="grow">
|
||||
<p className="grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{server.domain}</p>
|
||||
</div>
|
||||
<button
|
||||
className="text-muted-foreground opacity-0 group-hover/server:opacity-100 hover:bg-input size-8 flex items-center justify-center rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setSettingsDomain(server.domain);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="size-4" />
|
||||
</button>
|
||||
</button>
|
||||
<ServerCard key={server.domain} server={server} onSelect={onSelect} />
|
||||
))}
|
||||
<button
|
||||
onClick={() => setOpenCreate(true)}
|
||||
@@ -83,32 +52,6 @@ export const AuthServer = ({ onSelect }: AuthServerProps) => {
|
||||
{openCreate && (
|
||||
<ServerCreateDialog onCancel={() => setOpenCreate(false)} />
|
||||
)}
|
||||
{deleteServer && (
|
||||
<ServerDeleteDialog
|
||||
server={deleteServer}
|
||||
open={!!deleteServer}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleteDomain(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{settingsServer && (
|
||||
<ServerSettingsDialog
|
||||
server={settingsServer}
|
||||
open={!!settingsServer}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setSettingsDomain(null);
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
setSettingsDomain(null);
|
||||
setDeleteDomain(settingsServer.domain);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
87
packages/ui/src/components/servers/server-card.tsx
Normal file
87
packages/ui/src/components/servers/server-card.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
|
||||
import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog';
|
||||
import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
interface ServerCardProps {
|
||||
server: Server;
|
||||
onSelect: (server: Server) => void;
|
||||
}
|
||||
|
||||
export const ServerCard = ({ server, onSelect }: ServerCardProps) => {
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const { mutate: syncServer, isPending: isSyncing } = useMutation();
|
||||
|
||||
const handleServerClick = () => {
|
||||
if (isSyncing) return;
|
||||
|
||||
syncServer({
|
||||
input: {
|
||||
type: 'server.sync',
|
||||
domain: server.domain,
|
||||
},
|
||||
onSuccess() {
|
||||
onSelect(server);
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleServerClick}
|
||||
disabled={isSyncing}
|
||||
className="group/server relative flex w-full flex-row items-center gap-3 rounded-lg border border-border/60 bg-background p-2 text-left transition-all hover:cursor-pointer hover:border-border hover:bg-accent hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="grow">
|
||||
<p className="grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{server.domain}</p>
|
||||
</div>
|
||||
<button
|
||||
className="text-muted-foreground opacity-0 group-hover/server:opacity-100 hover:bg-input size-8 flex items-center justify-center rounded-md cursor-pointer disabled:opacity-100"
|
||||
disabled={isSyncing}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<SettingsIcon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
</button>
|
||||
<ServerSettingsDialog
|
||||
server={server}
|
||||
open={openSettings}
|
||||
onOpenChange={setOpenSettings}
|
||||
onDelete={() => {
|
||||
setOpenSettings(false);
|
||||
setOpenDelete(true);
|
||||
}}
|
||||
/>
|
||||
<ServerDeleteDialog
|
||||
server={server}
|
||||
open={openDelete}
|
||||
onOpenChange={setOpenDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { TrashIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { formatDate, isColanodeServer, timeAgo } from '@colanode/core';
|
||||
import { formatDate, isColanodeDomain, timeAgo } from '@colanode/core';
|
||||
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
|
||||
import { Badge } from '@colanode/ui/components/ui/badge';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
@@ -45,7 +45,7 @@ export const ServerSettingsDialog = ({
|
||||
return () => clearInterval(interval);
|
||||
}, [open]);
|
||||
|
||||
const canDelete = !isColanodeServer(server.domain);
|
||||
const canDelete = !isColanodeDomain(server.domain);
|
||||
const isAvailable = server.state?.isAvailable ?? false;
|
||||
const isOutdated = server.isOutdated;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cloud, ExternalLink } from 'lucide-react';
|
||||
|
||||
import { isColanodeServer } from '@colanode/core';
|
||||
import { isColanodeDomain } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import { useServer } from '@colanode/ui/contexts/server';
|
||||
|
||||
@@ -8,7 +8,7 @@ const CLOUD_URL = 'https://cloud.colanode.com';
|
||||
|
||||
export const WorkspaceStorageCloud = () => {
|
||||
const server = useServer();
|
||||
if (!isColanodeServer(server.domain)) {
|
||||
if (!isColanodeDomain(server.domain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user