Server settings dialog and other minor improvements (#58)

This commit is contained in:
Hakan Shehu
2025-06-12 23:56:23 +02:00
committed by GitHub
parent 756af021d1
commit 10ac2d3019
13 changed files with 336 additions and 107 deletions

View File

@@ -21,7 +21,7 @@ export const createServersTable: Migration = {
name: 'Colanode Cloud (EU)', name: 'Colanode Cloud (EU)',
avatar: 'https://colanode.com/assets/flags/eu.svg', avatar: 'https://colanode.com/assets/flags/eu.svg',
attributes: '{}', attributes: '{}',
version: '0.1.0', version: '0.2.0',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}, },
{ {
@@ -29,7 +29,7 @@ export const createServersTable: Migration = {
name: 'Colanode Cloud (US)', name: 'Colanode Cloud (US)',
avatar: 'https://colanode.com/assets/flags/us.svg', avatar: 'https://colanode.com/assets/flags/us.svg',
attributes: '{}', attributes: '{}',
version: '0.1.0', version: '0.2.0',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}, },
]) ])

View File

@@ -1,9 +1,8 @@
import { SelectServer } from '@colanode/client/databases/app';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib'; import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib';
import { ServerListQueryInput } from '@colanode/client/queries/servers/server-list'; import { ServerListQueryInput } from '@colanode/client/queries/servers/server-list';
import { AppService } from '@colanode/client/services/app-service'; import { AppService } from '@colanode/client/services/app-service';
import { Event } from '@colanode/client/types/events'; import { Event } from '@colanode/client/types/events';
import { Server } from '@colanode/client/types/servers'; import { ServerDetails } from '@colanode/client/types/servers';
export class ServerListQueryHandler export class ServerListQueryHandler
implements QueryHandler<ServerListQueryInput> implements QueryHandler<ServerListQueryInput>
@@ -14,37 +13,34 @@ export class ServerListQueryHandler
this.app = app; this.app = app;
} }
async handleQuery(_: ServerListQueryInput): Promise<Server[]> { async handleQuery(_: ServerListQueryInput): Promise<ServerDetails[]> {
const rows = await this.fetchServers(); return this.getServers();
return this.mapServers(rows);
} }
async checkForChanges( async checkForChanges(
event: Event, event: Event,
_: ServerListQueryInput, _: ServerListQueryInput,
output: Server[] __: ServerDetails[]
): Promise<ChangeCheckResult<ServerListQueryInput>> { ): Promise<ChangeCheckResult<ServerListQueryInput>> {
if (event.type === 'server.created') { if (event.type === 'server.created') {
const newServers = [...output, event.server];
return { return {
hasChanges: true, hasChanges: true,
result: newServers, result: this.getServers(),
}; };
} else if (event.type === 'server.updated') { } else if (event.type === 'server.updated') {
const newServers = output.map((server) =>
server.domain === event.server.domain ? event.server : server
);
return { return {
hasChanges: true, hasChanges: true,
result: newServers, result: this.getServers(),
}; };
} else if (event.type === 'server.deleted') { } else if (event.type === 'server.deleted') {
const newServers = output.filter(
(server) => server.domain !== event.server.domain
);
return { return {
hasChanges: true, hasChanges: true,
result: newServers, result: this.getServers(),
};
} else if (event.type === 'server.availability.changed') {
return {
hasChanges: true,
result: this.getServers(),
}; };
} }
@@ -53,21 +49,21 @@ export class ServerListQueryHandler
}; };
} }
private fetchServers(): Promise<SelectServer[]> { private getServers(): ServerDetails[] {
return this.app.database.selectFrom('servers').selectAll().execute(); const serverServices = this.app.getServers();
} const result: ServerDetails[] = [];
private mapServers = (rows: SelectServer[]): Server[] => { for (const serverService of serverServices) {
return rows.map((row) => { const serverDetails: ServerDetails = {
return { ...serverService.server,
domain: row.domain, state: serverService.state,
name: row.name, isOutdated: serverService.isOutdated,
avatar: row.avatar, configUrl: serverService.configUrl,
attributes: JSON.parse(row.attributes),
version: row.version,
createdAt: new Date(row.created_at),
syncedAt: row.synced_at ? new Date(row.synced_at) : null,
}; };
});
}; result.push(serverDetails);
}
return result;
}
} }

View File

@@ -1,4 +1,4 @@
import { Server } from '@colanode/client/types/servers'; import { ServerDetails } from '@colanode/client/types/servers';
export type ServerListQueryInput = { export type ServerListQueryInput = {
type: 'server.list'; type: 'server.list';
@@ -8,7 +8,7 @@ declare module '@colanode/client/queries' {
interface QueryMap { interface QueryMap {
'server.list': { 'server.list': {
input: ServerListQueryInput; input: ServerListQueryInput;
output: Server[]; output: ServerDetails[];
}; };
} }
} }

View File

@@ -117,6 +117,10 @@ export class AppService {
return Array.from(this.accounts.values()); return Array.from(this.accounts.values());
} }
public getServers(): ServerService[] {
return Array.from(this.servers.values());
}
public getServer(domain: string): ServerService | null { public getServer(domain: string): ServerService | null {
return this.servers.get(domain) ?? null; return this.servers.get(domain) ?? null;
} }
@@ -191,6 +195,7 @@ export class AppService {
} }
const attributes: ServerAttributes = { const attributes: ServerAttributes = {
sha: config.sha,
pathPrefix: config.pathPrefix, pathPrefix: config.pathPrefix,
insecure: url.protocol === 'http:', insecure: url.protocol === 'http:',
account: config.account?.google.enabled account: config.account?.google.enabled

View File

@@ -6,24 +6,21 @@ import { EventLoop } from '@colanode/client/lib/event-loop';
import { mapServer } from '@colanode/client/lib/mappers'; import { mapServer } from '@colanode/client/lib/mappers';
import { isServerOutdated } from '@colanode/client/lib/servers'; import { isServerOutdated } from '@colanode/client/lib/servers';
import { AppService } from '@colanode/client/services/app-service'; import { AppService } from '@colanode/client/services/app-service';
import { Server, ServerAttributes } from '@colanode/client/types/servers'; import {
Server,
ServerAttributes,
ServerState,
} from '@colanode/client/types/servers';
import { createDebugger, ServerConfig } from '@colanode/core'; import { createDebugger, ServerConfig } from '@colanode/core';
type ServerState = {
isAvailable: boolean;
lastCheckedAt: Date;
lastCheckedSuccessfullyAt: Date | null;
count: number;
};
const debug = createDebugger('desktop:service:server'); const debug = createDebugger('desktop:service:server');
export class ServerService { export class ServerService {
private readonly app: AppService; private readonly app: AppService;
private readonly eventLoop: EventLoop;
private state: ServerState | null = null; public state: ServerState | null = null;
private eventLoop: EventLoop; public isOutdated: boolean;
private isOutdated: boolean;
public readonly server: Server; public readonly server: Server;
public readonly configUrl: string; public readonly configUrl: string;
@@ -88,6 +85,7 @@ export class ServerService {
if (config) { if (config) {
const attributes: ServerAttributes = { const attributes: ServerAttributes = {
...this.server.attributes, ...this.server.attributes,
sha: config.sha,
account: config.account?.google.enabled account: config.account?.google.enabled
? { ? {
google: { google: {

View File

@@ -9,6 +9,14 @@ export type ServerAttributes = {
pathPrefix?: string | null; pathPrefix?: string | null;
insecure?: boolean; insecure?: boolean;
account?: ServerAccountAttributes; account?: ServerAccountAttributes;
sha?: string | null;
};
export type ServerState = {
isAvailable: boolean;
lastCheckedAt: Date;
lastCheckedSuccessfullyAt: Date | null;
count: number;
}; };
export type Server = { export type Server = {
@@ -20,3 +28,9 @@ export type Server = {
createdAt: Date; createdAt: Date;
syncedAt: Date | null; syncedAt: Date | null;
}; };
export type ServerDetails = Server & {
state: ServerState | null;
isOutdated: boolean;
configUrl: string;
};

View File

@@ -3,12 +3,15 @@ import { Resizable } from 're-resizable';
import { ContainerBlank } from '@colanode/ui/components/layouts/containers/container-blank'; import { ContainerBlank } from '@colanode/ui/components/layouts/containers/container-blank';
import { ContainerTabs } from '@colanode/ui/components/layouts/containers/container-tabs'; import { ContainerTabs } from '@colanode/ui/components/layouts/containers/container-tabs';
import { Sidebar } from '@colanode/ui/components/layouts/sidebars/sidebar'; import { Sidebar } from '@colanode/ui/components/layouts/sidebars/sidebar';
import { ServerUpgradeRequired } from '@colanode/ui/components/servers/server-upgrade-required';
import { LayoutContext } from '@colanode/ui/contexts/layout'; import { LayoutContext } from '@colanode/ui/contexts/layout';
import { useServer } from '@colanode/ui/contexts/server';
import { useLayoutState } from '@colanode/ui/hooks/use-layout-state'; import { useLayoutState } from '@colanode/ui/hooks/use-layout-state';
import { useWindowSize } from '@colanode/ui/hooks/use-window-size'; import { useWindowSize } from '@colanode/ui/hooks/use-window-size';
import { percentToNumber } from '@colanode/ui/lib/utils'; import { percentToNumber } from '@colanode/ui/lib/utils';
export const Layout = () => { export const Layout = () => {
const server = useServer();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { const {
@@ -35,10 +38,11 @@ export const Layout = () => {
handleMoveRight, handleMoveRight,
} = useLayoutState(); } = useLayoutState();
const shouldDisplayLeft = leftContainerMetadata.tabs.length > 0; const shouldDisplayLeft =
!server.isOutdated && leftContainerMetadata.tabs.length > 0;
const shouldDisplayRight = const shouldDisplayRight =
shouldDisplayLeft && rightContainerMetadata.tabs.length > 0; !server.isOutdated && rightContainerMetadata.tabs.length > 0;
return ( return (
<LayoutContext.Provider <LayoutContext.Provider
@@ -150,7 +154,10 @@ export const Layout = () => {
/> />
</Resizable> </Resizable>
)} )}
{!shouldDisplayLeft && !shouldDisplayRight && <ContainerBlank />} {!server.isOutdated && !shouldDisplayLeft && !shouldDisplayRight && (
<ContainerBlank />
)}
{server.isOutdated && <ServerUpgradeRequired />}
</div> </div>
</LayoutContext.Provider> </LayoutContext.Provider>
); );

View File

@@ -1,5 +1,6 @@
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ServerDetails } from '@colanode/client/types';
import { import {
AlertDialog, AlertDialog,
AlertDialogCancel, AlertDialogCancel,
@@ -16,11 +17,11 @@ import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface ServerDeleteDialogProps { interface ServerDeleteDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
domain: string; server: ServerDetails;
} }
export const ServerDeleteDialog = ({ export const ServerDeleteDialog = ({
domain, server,
open, open,
onOpenChange, onOpenChange,
}: ServerDeleteDialogProps) => { }: ServerDeleteDialogProps) => {
@@ -32,7 +33,7 @@ export const ServerDeleteDialog = ({
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
Are you sure you want delete the server{' '} Are you sure you want delete the server{' '}
<span className="font-bold">&quot;{domain}&quot;</span>? <span className="font-bold">&quot;{server.domain}&quot;</span>?
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Deleting the server will remove all accounts connected to it. You Deleting the server will remove all accounts connected to it. You
@@ -48,7 +49,7 @@ export const ServerDeleteDialog = ({
mutate({ mutate({
input: { input: {
type: 'server.delete', type: 'server.delete',
domain, domain: server.domain,
}, },
onSuccess() { onSuccess() {
onOpenChange(false); onOpenChange(false);

View File

@@ -1,11 +1,16 @@
import { ChevronDown, PlusIcon, ServerOffIcon, TrashIcon } from 'lucide-react'; import {
ChevronDown,
PlusIcon,
ServerOffIcon,
SettingsIcon,
} from 'lucide-react';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { Server } from '@colanode/client/types'; import { Server, ServerDetails } from '@colanode/client/types';
import { isColanodeServer } from '@colanode/core';
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar'; import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog'; import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog';
import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog'; import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog';
import { ServerSettingsDialog } from '@colanode/ui/components/servers/server-settings-dialog';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -17,7 +22,7 @@ import {
interface ServerDropdownProps { interface ServerDropdownProps {
value: Server | null; value: Server | null;
onChange: (server: Server) => void; onChange: (server: Server) => void;
servers: Server[]; servers: ServerDetails[];
readonly?: boolean; readonly?: boolean;
} }
@@ -29,8 +34,14 @@ export const ServerDropdown = ({
}: ServerDropdownProps) => { }: ServerDropdownProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [openCreate, setOpenCreate] = useState(false); const [openCreate, setOpenCreate] = useState(false);
const [settingsDomain, setSettingsDomain] = useState<string | null>(null);
const [deleteDomain, setDeleteDomain] = useState<string | null>(null); const [deleteDomain, setDeleteDomain] = useState<string | null>(null);
const settingsServer = servers.find(
(server) => server.domain === settingsDomain
);
const deleteServer = servers.find((server) => server.domain === deleteDomain);
return ( return (
<Fragment> <Fragment>
<DropdownMenu <DropdownMenu
@@ -70,46 +81,41 @@ export const ServerDropdown = ({
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-96"> <DropdownMenuContent className="w-96">
{servers.map((server) => { {servers.map((server) => (
const canDelete = !isColanodeServer(server.domain); <DropdownMenuItem
return ( key={server.domain}
<DropdownMenuItem onSelect={() => {
key={server.domain} if (value?.domain !== server.domain) {
onSelect={() => { onChange(server);
if (value?.domain !== server.domain) { }
onChange(server); }}
} className="group/server flex w-full flex-grow flex-row items-center gap-3 rounded-md border-b border-input p-2 cursor-pointer hover:bg-gray-100"
}} >
className="group/server flex w-full flex-grow flex-row items-center gap-3 rounded-md border-b border-input p-2 cursor-pointer hover:bg-gray-100" <div className="flex flex-grow items-center gap-3">
> <ServerAvatar
<div className="flex flex-grow items-center gap-3"> url={server.avatar}
<ServerAvatar name={server.name}
url={server.avatar} className="size-8 rounded-md"
name={server.name} />
className="size-8 rounded-md" <div className="flex-grow">
/> <p className="flex-grow font-semibold">{server.name}</p>
<div className="flex-grow"> <p className="text-xs text-muted-foreground">
<p className="flex-grow font-semibold">{server.name}</p> {server.domain}
<p className="text-xs text-muted-foreground"> </p>
{server.domain}
</p>
</div>
</div> </div>
{canDelete && ( </div>
<button <button
className="text-muted-foreground opacity-0 group-hover/server:opacity-100 hover:bg-gray-200 size-8 flex items-center justify-center rounded-md cursor-pointer" className="text-muted-foreground opacity-0 group-hover/server:opacity-100 hover:bg-gray-200 size-8 flex items-center justify-center rounded-md cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
setDeleteDomain(server.domain); setSettingsDomain(server.domain);
}} }}
> >
<TrashIcon className="size-4" /> <SettingsIcon className="size-4" />
</button> </button>
)} </DropdownMenuItem>
</DropdownMenuItem> ))}
);
})}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onSelect={() => { onSelect={() => {
@@ -130,10 +136,10 @@ export const ServerDropdown = ({
}} }}
/> />
)} )}
{deleteDomain && ( {deleteServer && (
<ServerDeleteDialog <ServerDeleteDialog
domain={deleteDomain} server={deleteServer}
open={!!deleteDomain} open={!!deleteServer}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setDeleteDomain(null); setDeleteDomain(null);
@@ -141,6 +147,21 @@ export const ServerDropdown = ({
}} }}
/> />
)} )}
{settingsServer && (
<ServerSettingsDialog
server={settingsServer}
open={!!settingsServer}
onOpenChange={(open) => {
if (!open) {
setSettingsDomain(null);
}
}}
onDelete={() => {
setSettingsDomain(null);
setDeleteDomain(settingsServer.domain);
}}
/>
)}
</Fragment> </Fragment>
); );
}; };

View File

@@ -1,6 +1,4 @@
import { isServerOutdated } from '@colanode/client/lib';
import { ServerNotFound } from '@colanode/ui/components/servers/server-not-found'; import { ServerNotFound } from '@colanode/ui/components/servers/server-not-found';
import { ServerUpgradeRequired } from '@colanode/ui/components/servers/server-upgrade-required';
import { ServerContext } from '@colanode/ui/contexts/server'; import { ServerContext } from '@colanode/ui/contexts/server';
import { useQuery } from '@colanode/ui/hooks/use-query'; import { useQuery } from '@colanode/ui/hooks/use-query';
import { isFeatureSupported } from '@colanode/ui/lib/features'; import { isFeatureSupported } from '@colanode/ui/lib/features';
@@ -27,8 +25,6 @@ export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
return <ServerNotFound domain={domain} />; return <ServerNotFound domain={domain} />;
} }
const isOutdated = isServerOutdated(server.version);
return ( return (
<ServerContext.Provider <ServerContext.Provider
value={{ value={{
@@ -38,7 +34,7 @@ export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
}, },
}} }}
> >
{isOutdated ? <ServerUpgradeRequired /> : children} {children}
</ServerContext.Provider> </ServerContext.Provider>
); );
}; };

View File

@@ -0,0 +1,191 @@
import { TrashIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { ServerDetails } from '@colanode/client/types';
import { formatDate, isColanodeServer, 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';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@colanode/ui/components/ui/dialog';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@colanode/ui/components/ui/tooltip';
import { cn } from '@colanode/ui/lib/utils';
interface ServerSettingsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
server: ServerDetails;
onDelete: () => void;
}
export const ServerSettingsDialog = ({
open,
onOpenChange,
server,
onDelete,
}: ServerSettingsDialogProps) => {
const [, setTick] = useState(0);
useEffect(() => {
if (!open) return;
const interval = setInterval(() => {
setTick((prev) => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [open]);
const canDelete = !isColanodeServer(server.domain);
const isAvailable = server.state?.isAvailable ?? false;
const isOutdated = server.isOutdated;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl w-2xl min-w-xl overflow-y-auto">
<DialogHeader>
<div className="flex items-center gap-3">
<ServerAvatar
url={server.avatar}
name={server.name}
className="size-10"
/>
<div>
<DialogTitle className="text-left">{server.name}</DialogTitle>
<DialogDescription className="text-left">
{server.domain}
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="space-y-6">
<div className="border rounded-lg p-4">
<h3 className="text-sm font-semibold mb-3">Server Status</h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Status</span>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Badge
variant="default"
className={cn(
'cursor-pointer select-none',
isAvailable ? 'bg-emerald-500' : 'bg-destructive'
)}
>
{server.state?.isAvailable ? 'Available' : 'Unavailable'}
</Badge>
</TooltipTrigger>
<TooltipContent side="top">
{isAvailable
? 'Server is available to use in this device'
: 'Server is unavailable to use. This means the server is not reachable, is outdated or is not allowed to be used from this app.'}
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Version</span>
<div className="flex items-center gap-2">
<span className="text-sm">{server.version}</span>
{isOutdated && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Badge
variant="destructive"
className="text-xs cursor-pointer select-none"
>
Outdated
</Badge>
</TooltipTrigger>
<TooltipContent side="top">
Server is outdated. Please update the server to the
latest version.
</TooltipContent>
</Tooltip>
)}
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">SHA</span>
<span className="text-sm">{server.attributes.sha}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last ping</span>
<span className="text-sm">
{server.state?.lastCheckedAt
? timeAgo(server.state?.lastCheckedAt)
: 'Never'}
</span>
</div>
{server.state?.lastCheckedSuccessfullyAt &&
server.state?.lastCheckedAt &&
server.state.lastCheckedSuccessfullyAt.getTime() !==
server.state.lastCheckedAt.getTime() && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Last successful ping
</span>
<span className="text-sm">
{timeAgo(server.state.lastCheckedSuccessfullyAt)}
</span>
</div>
)}
</div>
</div>
<div className="border rounded-lg p-4">
<h3 className="text-sm font-semibold mb-3">Server Details</h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Domain</span>
<span className="text-sm font-mono">{server.domain}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Created</span>
<span className="text-sm">{formatDate(server.createdAt)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Config URL
</span>
<a
href={server.configUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline font-mono break-all"
>
{server.configUrl}
</a>
</div>
</div>
</div>
</div>
{canDelete && (
<div className="border rounded-lg p-4">
<h3 className="text-sm mb-3">Delete server from this device</h3>
<Button
variant="destructive"
onClick={() => {
onDelete();
}}
>
<TrashIcon className="size-4 mr-1" />
Delete
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
};

View File

@@ -6,7 +6,7 @@ export const ServerUpgradeRequired = () => {
const server = useServer(); const server = useServer();
return ( return (
<div className="min-w-screen flex h-full min-h-screen w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center gap-8 text-center w-128"> <div className="flex flex-col items-center gap-8 text-center w-128">
<CircleFadingArrowUp className="h-10 w-10 text-gray-800" /> <CircleFadingArrowUp className="h-10 w-10 text-gray-800" />
<h2 className="text-4xl text-gray-800">Server upgrade required</h2> <h2 className="text-4xl text-gray-800">Server upgrade required</h2>

View File

@@ -1,9 +1,9 @@
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { Server } from '@colanode/client/types'; import { ServerDetails } from '@colanode/client/types';
import { FeatureKey } from '@colanode/ui/lib/features'; import { FeatureKey } from '@colanode/ui/lib/features';
interface ServerContext extends Server { interface ServerContext extends ServerDetails {
supports(feature: FeatureKey): boolean; supports(feature: FeatureKey): boolean;
} }