mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Server settings dialog and other minor improvements (#58)
This commit is contained in:
@@ -21,7 +21,7 @@ export const createServersTable: Migration = {
|
||||
name: 'Colanode Cloud (EU)',
|
||||
avatar: 'https://colanode.com/assets/flags/eu.svg',
|
||||
attributes: '{}',
|
||||
version: '0.1.0',
|
||||
version: '0.2.0',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
@@ -29,7 +29,7 @@ export const createServersTable: Migration = {
|
||||
name: 'Colanode Cloud (US)',
|
||||
avatar: 'https://colanode.com/assets/flags/us.svg',
|
||||
attributes: '{}',
|
||||
version: '0.1.0',
|
||||
version: '0.2.0',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { SelectServer } from '@colanode/client/databases/app';
|
||||
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib';
|
||||
import { ServerListQueryInput } from '@colanode/client/queries/servers/server-list';
|
||||
import { AppService } from '@colanode/client/services/app-service';
|
||||
import { Event } from '@colanode/client/types/events';
|
||||
import { Server } from '@colanode/client/types/servers';
|
||||
import { ServerDetails } from '@colanode/client/types/servers';
|
||||
|
||||
export class ServerListQueryHandler
|
||||
implements QueryHandler<ServerListQueryInput>
|
||||
@@ -14,37 +13,34 @@ export class ServerListQueryHandler
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async handleQuery(_: ServerListQueryInput): Promise<Server[]> {
|
||||
const rows = await this.fetchServers();
|
||||
return this.mapServers(rows);
|
||||
async handleQuery(_: ServerListQueryInput): Promise<ServerDetails[]> {
|
||||
return this.getServers();
|
||||
}
|
||||
|
||||
async checkForChanges(
|
||||
event: Event,
|
||||
_: ServerListQueryInput,
|
||||
output: Server[]
|
||||
__: ServerDetails[]
|
||||
): Promise<ChangeCheckResult<ServerListQueryInput>> {
|
||||
if (event.type === 'server.created') {
|
||||
const newServers = [...output, event.server];
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newServers,
|
||||
result: this.getServers(),
|
||||
};
|
||||
} else if (event.type === 'server.updated') {
|
||||
const newServers = output.map((server) =>
|
||||
server.domain === event.server.domain ? event.server : server
|
||||
);
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newServers,
|
||||
result: this.getServers(),
|
||||
};
|
||||
} else if (event.type === 'server.deleted') {
|
||||
const newServers = output.filter(
|
||||
(server) => server.domain !== event.server.domain
|
||||
);
|
||||
return {
|
||||
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[]> {
|
||||
return this.app.database.selectFrom('servers').selectAll().execute();
|
||||
}
|
||||
private getServers(): ServerDetails[] {
|
||||
const serverServices = this.app.getServers();
|
||||
const result: ServerDetails[] = [];
|
||||
|
||||
private mapServers = (rows: SelectServer[]): Server[] => {
|
||||
return rows.map((row) => {
|
||||
return {
|
||||
domain: row.domain,
|
||||
name: row.name,
|
||||
avatar: row.avatar,
|
||||
attributes: JSON.parse(row.attributes),
|
||||
version: row.version,
|
||||
createdAt: new Date(row.created_at),
|
||||
syncedAt: row.synced_at ? new Date(row.synced_at) : null,
|
||||
for (const serverService of serverServices) {
|
||||
const serverDetails: ServerDetails = {
|
||||
...serverService.server,
|
||||
state: serverService.state,
|
||||
isOutdated: serverService.isOutdated,
|
||||
configUrl: serverService.configUrl,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
result.push(serverDetails);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Server } from '@colanode/client/types/servers';
|
||||
import { ServerDetails } from '@colanode/client/types/servers';
|
||||
|
||||
export type ServerListQueryInput = {
|
||||
type: 'server.list';
|
||||
@@ -8,7 +8,7 @@ declare module '@colanode/client/queries' {
|
||||
interface QueryMap {
|
||||
'server.list': {
|
||||
input: ServerListQueryInput;
|
||||
output: Server[];
|
||||
output: ServerDetails[];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,10 @@ export class AppService {
|
||||
return Array.from(this.accounts.values());
|
||||
}
|
||||
|
||||
public getServers(): ServerService[] {
|
||||
return Array.from(this.servers.values());
|
||||
}
|
||||
|
||||
public getServer(domain: string): ServerService | null {
|
||||
return this.servers.get(domain) ?? null;
|
||||
}
|
||||
@@ -191,6 +195,7 @@ export class AppService {
|
||||
}
|
||||
|
||||
const attributes: ServerAttributes = {
|
||||
sha: config.sha,
|
||||
pathPrefix: config.pathPrefix,
|
||||
insecure: url.protocol === 'http:',
|
||||
account: config.account?.google.enabled
|
||||
|
||||
@@ -6,24 +6,21 @@ import { EventLoop } from '@colanode/client/lib/event-loop';
|
||||
import { mapServer } from '@colanode/client/lib/mappers';
|
||||
import { isServerOutdated } from '@colanode/client/lib/servers';
|
||||
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';
|
||||
|
||||
type ServerState = {
|
||||
isAvailable: boolean;
|
||||
lastCheckedAt: Date;
|
||||
lastCheckedSuccessfullyAt: Date | null;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const debug = createDebugger('desktop:service:server');
|
||||
|
||||
export class ServerService {
|
||||
private readonly app: AppService;
|
||||
private readonly eventLoop: EventLoop;
|
||||
|
||||
private state: ServerState | null = null;
|
||||
private eventLoop: EventLoop;
|
||||
private isOutdated: boolean;
|
||||
public state: ServerState | null = null;
|
||||
public isOutdated: boolean;
|
||||
|
||||
public readonly server: Server;
|
||||
public readonly configUrl: string;
|
||||
@@ -88,6 +85,7 @@ export class ServerService {
|
||||
if (config) {
|
||||
const attributes: ServerAttributes = {
|
||||
...this.server.attributes,
|
||||
sha: config.sha,
|
||||
account: config.account?.google.enabled
|
||||
? {
|
||||
google: {
|
||||
|
||||
@@ -9,6 +9,14 @@ export type ServerAttributes = {
|
||||
pathPrefix?: string | null;
|
||||
insecure?: boolean;
|
||||
account?: ServerAccountAttributes;
|
||||
sha?: string | null;
|
||||
};
|
||||
|
||||
export type ServerState = {
|
||||
isAvailable: boolean;
|
||||
lastCheckedAt: Date;
|
||||
lastCheckedSuccessfullyAt: Date | null;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type Server = {
|
||||
@@ -20,3 +28,9 @@ export type Server = {
|
||||
createdAt: Date;
|
||||
syncedAt: Date | null;
|
||||
};
|
||||
|
||||
export type ServerDetails = Server & {
|
||||
state: ServerState | null;
|
||||
isOutdated: boolean;
|
||||
configUrl: string;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import { Resizable } from 're-resizable';
|
||||
import { ContainerBlank } from '@colanode/ui/components/layouts/containers/container-blank';
|
||||
import { ContainerTabs } from '@colanode/ui/components/layouts/containers/container-tabs';
|
||||
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 { useServer } from '@colanode/ui/contexts/server';
|
||||
import { useLayoutState } from '@colanode/ui/hooks/use-layout-state';
|
||||
import { useWindowSize } from '@colanode/ui/hooks/use-window-size';
|
||||
import { percentToNumber } from '@colanode/ui/lib/utils';
|
||||
|
||||
export const Layout = () => {
|
||||
const server = useServer();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const {
|
||||
@@ -35,10 +38,11 @@ export const Layout = () => {
|
||||
handleMoveRight,
|
||||
} = useLayoutState();
|
||||
|
||||
const shouldDisplayLeft = leftContainerMetadata.tabs.length > 0;
|
||||
const shouldDisplayLeft =
|
||||
!server.isOutdated && leftContainerMetadata.tabs.length > 0;
|
||||
|
||||
const shouldDisplayRight =
|
||||
shouldDisplayLeft && rightContainerMetadata.tabs.length > 0;
|
||||
!server.isOutdated && rightContainerMetadata.tabs.length > 0;
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider
|
||||
@@ -150,7 +154,10 @@ export const Layout = () => {
|
||||
/>
|
||||
</Resizable>
|
||||
)}
|
||||
{!shouldDisplayLeft && !shouldDisplayRight && <ContainerBlank />}
|
||||
{!server.isOutdated && !shouldDisplayLeft && !shouldDisplayRight && (
|
||||
<ContainerBlank />
|
||||
)}
|
||||
{server.isOutdated && <ServerUpgradeRequired />}
|
||||
</div>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { ServerDetails } from '@colanode/client/types';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
@@ -16,11 +17,11 @@ import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
interface ServerDeleteDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
domain: string;
|
||||
server: ServerDetails;
|
||||
}
|
||||
|
||||
export const ServerDeleteDialog = ({
|
||||
domain,
|
||||
server,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ServerDeleteDialogProps) => {
|
||||
@@ -32,7 +33,7 @@ export const ServerDeleteDialog = ({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want delete the server{' '}
|
||||
<span className="font-bold">"{domain}"</span>?
|
||||
<span className="font-bold">"{server.domain}"</span>?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Deleting the server will remove all accounts connected to it. You
|
||||
@@ -48,7 +49,7 @@ export const ServerDeleteDialog = ({
|
||||
mutate({
|
||||
input: {
|
||||
type: 'server.delete',
|
||||
domain,
|
||||
domain: server.domain,
|
||||
},
|
||||
onSuccess() {
|
||||
onOpenChange(false);
|
||||
|
||||
@@ -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 { Server } from '@colanode/client/types';
|
||||
import { isColanodeServer } from '@colanode/core';
|
||||
import { Server, ServerDetails } from '@colanode/client/types';
|
||||
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
|
||||
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';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -17,7 +22,7 @@ import {
|
||||
interface ServerDropdownProps {
|
||||
value: Server | null;
|
||||
onChange: (server: Server) => void;
|
||||
servers: Server[];
|
||||
servers: ServerDetails[];
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,8 +34,14 @@ export const ServerDropdown = ({
|
||||
}: ServerDropdownProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [settingsDomain, setSettingsDomain] = 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 (
|
||||
<Fragment>
|
||||
<DropdownMenu
|
||||
@@ -70,46 +81,41 @@ export const ServerDropdown = ({
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-96">
|
||||
{servers.map((server) => {
|
||||
const canDelete = !isColanodeServer(server.domain);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={server.domain}
|
||||
onSelect={() => {
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-grow items-center gap-3">
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.domain}
|
||||
</p>
|
||||
</div>
|
||||
{servers.map((server) => (
|
||||
<DropdownMenuItem
|
||||
key={server.domain}
|
||||
onSelect={() => {
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-grow items-center gap-3">
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.domain}
|
||||
</p>
|
||||
</div>
|
||||
{canDelete && (
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDeleteDomain(server.domain);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setSettingsDomain(server.domain);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="size-4" />
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
@@ -130,10 +136,10 @@ export const ServerDropdown = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteDomain && (
|
||||
{deleteServer && (
|
||||
<ServerDeleteDialog
|
||||
domain={deleteDomain}
|
||||
open={!!deleteDomain}
|
||||
server={deleteServer}
|
||||
open={!!deleteServer}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { isServerOutdated } from '@colanode/client/lib';
|
||||
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 { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { isFeatureSupported } from '@colanode/ui/lib/features';
|
||||
@@ -27,8 +25,6 @@ export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
|
||||
return <ServerNotFound domain={domain} />;
|
||||
}
|
||||
|
||||
const isOutdated = isServerOutdated(server.version);
|
||||
|
||||
return (
|
||||
<ServerContext.Provider
|
||||
value={{
|
||||
@@ -38,7 +34,7 @@ export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isOutdated ? <ServerUpgradeRequired /> : children}
|
||||
{children}
|
||||
</ServerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
191
packages/ui/src/components/servers/server-settings-dialog.tsx
Normal file
191
packages/ui/src/components/servers/server-settings-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ export const ServerUpgradeRequired = () => {
|
||||
const server = useServer();
|
||||
|
||||
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">
|
||||
<CircleFadingArrowUp className="h-10 w-10 text-gray-800" />
|
||||
<h2 className="text-4xl text-gray-800">Server upgrade required</h2>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { ServerDetails } from '@colanode/client/types';
|
||||
import { FeatureKey } from '@colanode/ui/lib/features';
|
||||
|
||||
interface ServerContext extends Server {
|
||||
interface ServerContext extends ServerDetails {
|
||||
supports(feature: FeatureKey): boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user