mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-29 19:34:32 +00:00
feat: add local server management dialog shell
This commit is contained in:
parent
df635562e9
commit
8d8e8fe8f4
2 changed files with 437 additions and 114 deletions
276
packages/app/src/components/dialog-local-server.tsx
Normal file
276
packages/app/src/components/dialog-local-server.tsx
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { LocalServerState } from "@/context/platform"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
export function DialogLocalServer() {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const [store, setStore] = createStore({
|
||||
state: undefined as LocalServerState | undefined,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const localServer = platform.localServer
|
||||
if (!localServer) return
|
||||
let mounted = true
|
||||
void localServer
|
||||
.getState()
|
||||
.then((state) => {
|
||||
if (!mounted) return
|
||||
setStore({ state, loading: false })
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!mounted) return
|
||||
requestError(language, err)
|
||||
setStore("loading", false)
|
||||
})
|
||||
const off = localServer.subscribe((event) => {
|
||||
setStore("state", reconcile(event.state))
|
||||
setStore("loading", false)
|
||||
})
|
||||
onCleanup(() => {
|
||||
mounted = false
|
||||
off()
|
||||
})
|
||||
})
|
||||
|
||||
const current = () => store.state
|
||||
const localServer = () => platform.localServer
|
||||
const busy = createMemo(() => !!current()?.job)
|
||||
const mode = createMemo(() => current()?.config.mode ?? "windows")
|
||||
const selected = createMemo(() => current()?.checks.distro?.selected)
|
||||
|
||||
const run = async (action: () => Promise<void>) => {
|
||||
try {
|
||||
await action()
|
||||
} catch (err) {
|
||||
requestError(language, err)
|
||||
}
|
||||
}
|
||||
|
||||
const setMode = async (next: "windows" | "wsl") => {
|
||||
const state = current()
|
||||
if (!state || !localServer()) return
|
||||
await run(() =>
|
||||
localServer()!.setConfig({
|
||||
...state.config,
|
||||
mode: next,
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
complete: next === "windows",
|
||||
pendingRestart: next === "windows" ? false : state.config.onboarding.pendingRestart,
|
||||
step: next === "windows" ? null : state.config.onboarding.step,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const selectDistro = async (name: string) => {
|
||||
const state = current()
|
||||
if (!state || !localServer()) return
|
||||
await run(() =>
|
||||
localServer()!.setConfig({
|
||||
...state.config,
|
||||
mode: "wsl",
|
||||
distro: name,
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
complete: false,
|
||||
step: "distro",
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="px-5 pb-5 flex flex-col gap-4">
|
||||
<Show
|
||||
when={!store.loading}
|
||||
fallback={<div class="px-1 py-6 text-14-regular text-text-weak">Loading local server...</div>}
|
||||
>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="text-14-medium text-text-strong">Runtime</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={mode() === "windows" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("windows")}
|
||||
>
|
||||
Run on Windows
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode() === "wsl" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("wsl")}
|
||||
>
|
||||
Run in WSL
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Current runtime:{" "}
|
||||
{current()?.runtime.mode === "wsl"
|
||||
? `wsl${current()?.runtime.distro ? `:${current()?.runtime.distro}` : ""}`
|
||||
: "windows"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={mode() === "wsl"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">WSL</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.wsl?.error ??
|
||||
current()?.checks.wsl?.status ??
|
||||
current()?.checks.wsl?.version ??
|
||||
"Not checked yet"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("wsl"))}
|
||||
>
|
||||
Check WSL
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installWsl())}
|
||||
>
|
||||
Install WSL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={current()?.config.onboarding.pendingRestart}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
Windows restart required to finish WSL installation.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Distro</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.distro?.error ??
|
||||
selected()?.name ??
|
||||
current()?.config.distro ??
|
||||
"No distro selected"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("distro"))}
|
||||
>
|
||||
Check distros
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Debian"))}
|
||||
>
|
||||
Install Debian
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Ubuntu-24.04"))}
|
||||
>
|
||||
Install Ubuntu 24
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Installed distros</div>
|
||||
<Show
|
||||
when={(current()?.checks.distro?.installed?.length ?? 0) > 0}
|
||||
fallback={<div class="text-12-regular text-text-weak">No distros detected yet.</div>}
|
||||
>
|
||||
<For each={current()?.checks.distro?.installed ?? []}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border-weak-base px-3 py-2 text-left transition-colors"
|
||||
classList={{ "bg-surface-raised-base": current()?.config.distro === item.name }}
|
||||
onClick={() => void selectDistro(item.name)}
|
||||
>
|
||||
<div class="text-13-medium text-text-strong">{item.name}</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
{[item.isDefault ? "default" : null, item.state, item.version ? `WSL ${item.version}` : null]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={selected()}>
|
||||
{(probe) => (
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-medium text-text-strong">Selected distro checks</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
User: {probe().username ?? "unknown"}
|
||||
{probe().isRoot ? " · root" : ""}
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "}
|
||||
{probe().canExecute ? "yes" : "no"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy() || !current()?.config.distro}
|
||||
onClick={() => void run(() => localServer()!.openTerminal())}
|
||||
>
|
||||
Open terminal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={(current()?.transcript.length ?? 0) > 0}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-2">
|
||||
<div class="text-14-medium text-text-strong">Diagnostics</div>
|
||||
<div class="max-h-56 overflow-y-auto rounded-md border border-border-weak-base bg-background-base px-3 py-2 font-mono text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
<For each={current()?.transcript ?? []}>{(line) => <div>{line.text}</div>}</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function requestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { showToast } from "@opencode-ai/ui/toast"
|
|||
import { useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { DialogLocalServer } from "@/components/dialog-local-server"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
|
@ -191,6 +192,9 @@ export function DialogSelectServer() {
|
|||
showForm: false,
|
||||
status: undefined as boolean | undefined,
|
||||
},
|
||||
localServer: {
|
||||
showPage: false,
|
||||
},
|
||||
editServer: {
|
||||
id: undefined as string | undefined,
|
||||
value: "",
|
||||
|
|
@ -419,7 +423,8 @@ export function DialogSelectServer() {
|
|||
)
|
||||
}
|
||||
|
||||
const mode = createMemo<"list" | "add" | "edit">(() => {
|
||||
const mode = createMemo<"list" | "local" | "add" | "edit">(() => {
|
||||
if (store.localServer.showPage) return "local"
|
||||
if (store.editServer.id) return "edit"
|
||||
if (store.addServer.showForm) return "add"
|
||||
return "list"
|
||||
|
|
@ -433,9 +438,11 @@ export function DialogSelectServer() {
|
|||
const resetForm = () => {
|
||||
resetAdd()
|
||||
resetEdit()
|
||||
setStore("localServer", "showPage", false)
|
||||
}
|
||||
|
||||
const startAdd = () => {
|
||||
setStore("localServer", "showPage", false)
|
||||
resetEdit()
|
||||
setStore("addServer", {
|
||||
showForm: true,
|
||||
|
|
@ -449,6 +456,7 @@ export function DialogSelectServer() {
|
|||
}
|
||||
|
||||
const startEdit = (conn: ServerConnection.Http) => {
|
||||
setStore("localServer", "showPage", false)
|
||||
resetAdd()
|
||||
setStore("editServer", {
|
||||
id: conn.http.url,
|
||||
|
|
@ -461,6 +469,12 @@ export function DialogSelectServer() {
|
|||
})
|
||||
}
|
||||
|
||||
const startLocal = () => {
|
||||
resetAdd()
|
||||
resetEdit()
|
||||
setStore("localServer", "showPage", true)
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (mode() === "add") {
|
||||
if (addMutation.isPending) return
|
||||
|
|
@ -477,6 +491,7 @@ export function DialogSelectServer() {
|
|||
|
||||
const isFormMode = createMemo(() => mode() !== "list")
|
||||
const isAddMode = createMemo(() => mode() === "add")
|
||||
const isLocalMode = createMemo(() => mode() === "local")
|
||||
const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
|
||||
|
||||
const formTitle = createMemo(() => {
|
||||
|
|
@ -484,7 +499,13 @@ export function DialogSelectServer() {
|
|||
return (
|
||||
<div class="flex items-center gap-2 -ml-2">
|
||||
<IconButton icon="arrow-left" variant="ghost" onClick={resetForm} aria-label={language.t("common.goBack")} />
|
||||
<span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span>
|
||||
<span>
|
||||
{isLocalMode()
|
||||
? "Local Server"
|
||||
: isAddMode()
|
||||
? language.t("dialog.server.add.title")
|
||||
: language.t("dialog.server.edit.title")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -508,130 +529,156 @@ export function DialogSelectServer() {
|
|||
<Show
|
||||
when={!isFormMode()}
|
||||
fallback={
|
||||
<ServerForm
|
||||
value={isAddMode() ? store.addServer.url : store.editServer.value}
|
||||
name={isAddMode() ? store.addServer.name : store.editServer.name}
|
||||
username={isAddMode() ? store.addServer.username : store.editServer.username}
|
||||
password={isAddMode() ? store.addServer.password : store.editServer.password}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={formBusy()}
|
||||
error={isAddMode() ? store.addServer.error : store.editServer.error}
|
||||
status={isAddMode() ? store.addServer.status : store.editServer.status}
|
||||
onChange={isAddMode() ? handleAddChange : handleEditChange}
|
||||
onNameChange={isAddMode() ? handleAddNameChange : handleEditNameChange}
|
||||
onUsernameChange={isAddMode() ? handleAddUsernameChange : handleEditUsernameChange}
|
||||
onPasswordChange={isAddMode() ? handleAddPasswordChange : handleEditPasswordChange}
|
||||
onSubmit={submitForm}
|
||||
onBack={resetForm}
|
||||
/>
|
||||
<Show
|
||||
when={isLocalMode()}
|
||||
fallback={
|
||||
<ServerForm
|
||||
value={isAddMode() ? store.addServer.url : store.editServer.value}
|
||||
name={isAddMode() ? store.addServer.name : store.editServer.name}
|
||||
username={isAddMode() ? store.addServer.username : store.editServer.username}
|
||||
password={isAddMode() ? store.addServer.password : store.editServer.password}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={formBusy()}
|
||||
error={isAddMode() ? store.addServer.error : store.editServer.error}
|
||||
status={isAddMode() ? store.addServer.status : store.editServer.status}
|
||||
onChange={isAddMode() ? handleAddChange : handleEditChange}
|
||||
onNameChange={isAddMode() ? handleAddNameChange : handleEditNameChange}
|
||||
onUsernameChange={isAddMode() ? handleAddUsernameChange : handleEditUsernameChange}
|
||||
onPasswordChange={isAddMode() ? handleAddPasswordChange : handleEditPasswordChange}
|
||||
onSubmit={submitForm}
|
||||
onBack={resetForm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DialogLocalServer />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Show when={platform.localServer}>
|
||||
<div class="px-5">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-md bg-surface-base px-4 py-3 text-left transition-colors hover:bg-surface-base-hover"
|
||||
onClick={startLocal}
|
||||
>
|
||||
<div class="text-14-medium text-text-strong">Local Server</div>
|
||||
<div class="text-12-regular text-text-weak">Configure Windows or WSL local runtime</div>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(key)}>
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
{language.t("dialog.server.menu.delete")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<Show
|
||||
when={isFormMode()}
|
||||
when={!isLocalMode() && isFormMode()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={startAdd}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
>
|
||||
{language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
<Show when={!isLocalMode()}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={startAdd}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
>
|
||||
{language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Button variant="primary" size="large" onClick={submitForm} disabled={formBusy()} class="px-3 py-1.5">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue