mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +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)',
|
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(),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">"{domain}"</span>?
|
<span className="font-bold">"{server.domain}"</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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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();
|
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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user