|
|
@@ -1,41 +1,36 @@
|
|
|
-import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
|
|
|
-import { createStore, reconcile } from "solid-js/store"
|
|
|
+import { Button } from "@opencode-ai/ui/button"
|
|
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
|
-import { List } from "@opencode-ai/ui/list"
|
|
|
-import { Button } from "@opencode-ai/ui/button"
|
|
|
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
+import { Icon } from "@opencode-ai/ui/icon"
|
|
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
|
+import { List } from "@opencode-ai/ui/list"
|
|
|
import { TextField } from "@opencode-ai/ui/text-field"
|
|
|
-import { normalizeServerUrl, useServer } from "@/context/server"
|
|
|
-import { usePlatform } from "@/context/platform"
|
|
|
+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 { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
-import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
-import { useGlobalSDK } from "@/context/global-sdk"
|
|
|
-import { showToast } from "@opencode-ai/ui/toast"
|
|
|
-import { ServerRow } from "@/components/server/server-row"
|
|
|
+import { usePlatform } from "@/context/platform"
|
|
|
+import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
|
|
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
|
|
|
|
|
|
-interface AddRowProps {
|
|
|
- value: string
|
|
|
- placeholder: string
|
|
|
- adding: boolean
|
|
|
- error: string
|
|
|
- status: boolean | undefined
|
|
|
- onChange: (value: string) => void
|
|
|
- onKeyDown: (event: KeyboardEvent) => void
|
|
|
- onBlur: () => void
|
|
|
-}
|
|
|
-
|
|
|
-interface EditRowProps {
|
|
|
+interface ServerFormProps {
|
|
|
value: string
|
|
|
+ name: string
|
|
|
+ username: string
|
|
|
+ password: string
|
|
|
placeholder: string
|
|
|
busy: boolean
|
|
|
error: string
|
|
|
status: boolean | undefined
|
|
|
onChange: (value: string) => void
|
|
|
- onKeyDown: (event: KeyboardEvent) => void
|
|
|
- onBlur: () => void
|
|
|
+ onNameChange: (value: string) => void
|
|
|
+ onUsernameChange: (value: string) => void
|
|
|
+ onPasswordChange: (value: string) => void
|
|
|
+ onSubmit: () => void
|
|
|
+ onBack: () => void
|
|
|
}
|
|
|
|
|
|
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
|
|
@@ -84,83 +79,86 @@ function useServerPreview(fetcher: typeof fetch) {
|
|
|
return host.includes(".") || host.includes(":")
|
|
|
}
|
|
|
|
|
|
- const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => {
|
|
|
+ const previewStatus = async (
|
|
|
+ value: string,
|
|
|
+ username: string,
|
|
|
+ password: string,
|
|
|
+ setStatus: (value: boolean | undefined) => void,
|
|
|
+ ) => {
|
|
|
setStatus(undefined)
|
|
|
if (!looksComplete(value)) return
|
|
|
const normalized = normalizeServerUrl(value)
|
|
|
if (!normalized) return
|
|
|
- const result = await checkServerHealth(normalized, fetcher)
|
|
|
+ const http: ServerConnection.HttpBase = { url: normalized }
|
|
|
+ if (username) http.username = username
|
|
|
+ if (password) http.password = password
|
|
|
+ const result = await checkServerHealth(http, fetcher)
|
|
|
setStatus(result.healthy)
|
|
|
}
|
|
|
|
|
|
return { previewStatus }
|
|
|
}
|
|
|
|
|
|
-function AddRow(props: AddRowProps) {
|
|
|
- return (
|
|
|
- <div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
|
|
- <div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
|
|
- <div
|
|
|
- classList={{
|
|
|
- "size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none": true,
|
|
|
- "bg-icon-success-base": props.status === true,
|
|
|
- "bg-icon-critical-base": props.status === false,
|
|
|
- "bg-border-weak-base": props.status === undefined,
|
|
|
- }}
|
|
|
- ref={(el) => {
|
|
|
- // Position relative to input-wrapper
|
|
|
- requestAnimationFrame(() => {
|
|
|
- const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
|
|
- if (wrapper instanceof HTMLElement) {
|
|
|
- wrapper.appendChild(el)
|
|
|
- }
|
|
|
- })
|
|
|
- }}
|
|
|
- />
|
|
|
- <TextField
|
|
|
- type="text"
|
|
|
- hideLabel
|
|
|
- placeholder={props.placeholder}
|
|
|
- value={props.value}
|
|
|
- autofocus
|
|
|
- validationState={props.error ? "invalid" : "valid"}
|
|
|
- error={props.error}
|
|
|
- disabled={props.adding}
|
|
|
- onChange={props.onChange}
|
|
|
- onKeyDown={props.onKeyDown}
|
|
|
- onBlur={props.onBlur}
|
|
|
- class="pl-7"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )
|
|
|
-}
|
|
|
+function ServerForm(props: ServerFormProps) {
|
|
|
+ const language = useLanguage()
|
|
|
+ const keyDown = (event: KeyboardEvent) => {
|
|
|
+ event.stopPropagation()
|
|
|
+ if (event.key === "Escape") {
|
|
|
+ event.preventDefault()
|
|
|
+ props.onBack()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (event.key !== "Enter" || event.isComposing) return
|
|
|
+ event.preventDefault()
|
|
|
+ props.onSubmit()
|
|
|
+ }
|
|
|
|
|
|
-function EditRow(props: EditRowProps) {
|
|
|
return (
|
|
|
- <div class="flex items-center gap-3 px-4 min-w-0 flex-1" onClick={(event) => event.stopPropagation()}>
|
|
|
- <div
|
|
|
- classList={{
|
|
|
- "size-1.5 rounded-full shrink-0": true,
|
|
|
- "bg-icon-success-base": props.status === true,
|
|
|
- "bg-icon-critical-base": props.status === false,
|
|
|
- "bg-border-weak-base": props.status === undefined,
|
|
|
- }}
|
|
|
- />
|
|
|
- <div class="flex-1 min-w-0">
|
|
|
+ <div class="px-5">
|
|
|
+ <div class="bg-surface-raised-base rounded-md p-5 flex flex-col gap-3">
|
|
|
+ <div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
|
|
+ <TextField
|
|
|
+ type="text"
|
|
|
+ label={language.t("dialog.server.add.url")}
|
|
|
+ placeholder={props.placeholder}
|
|
|
+ value={props.value}
|
|
|
+ autofocus
|
|
|
+ validationState={props.error ? "invalid" : "valid"}
|
|
|
+ error={props.error}
|
|
|
+ disabled={props.busy}
|
|
|
+ onChange={props.onChange}
|
|
|
+ onKeyDown={keyDown}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
<TextField
|
|
|
type="text"
|
|
|
- hideLabel
|
|
|
- placeholder={props.placeholder}
|
|
|
- value={props.value}
|
|
|
- autofocus
|
|
|
- validationState={props.error ? "invalid" : "valid"}
|
|
|
- error={props.error}
|
|
|
+ label={language.t("dialog.server.add.name")}
|
|
|
+ placeholder={language.t("dialog.server.add.namePlaceholder")}
|
|
|
+ value={props.name}
|
|
|
disabled={props.busy}
|
|
|
- onChange={props.onChange}
|
|
|
- onKeyDown={props.onKeyDown}
|
|
|
- onBlur={props.onBlur}
|
|
|
+ onChange={props.onNameChange}
|
|
|
+ onKeyDown={keyDown}
|
|
|
/>
|
|
|
+ <div class="grid grid-cols-2 gap-2 min-w-0">
|
|
|
+ <TextField
|
|
|
+ type="text"
|
|
|
+ label={language.t("dialog.server.add.username")}
|
|
|
+ placeholder="username"
|
|
|
+ value={props.username}
|
|
|
+ disabled={props.busy}
|
|
|
+ onChange={props.onUsernameChange}
|
|
|
+ onKeyDown={keyDown}
|
|
|
+ />
|
|
|
+ <TextField
|
|
|
+ type="password"
|
|
|
+ label={language.t("dialog.server.add.password")}
|
|
|
+ placeholder="password"
|
|
|
+ value={props.password}
|
|
|
+ disabled={props.busy}
|
|
|
+ onChange={props.onPasswordChange}
|
|
|
+ onKeyDown={keyDown}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)
|
|
|
@@ -171,16 +169,17 @@ export function DialogSelectServer() {
|
|
|
const dialog = useDialog()
|
|
|
const server = useServer()
|
|
|
const platform = usePlatform()
|
|
|
- const globalSDK = useGlobalSDK()
|
|
|
const language = useLanguage()
|
|
|
const fetcher = platform.fetch ?? globalThis.fetch
|
|
|
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
|
|
|
const { previewStatus } = useServerPreview(fetcher)
|
|
|
- let listRoot: HTMLDivElement | undefined
|
|
|
const [store, setStore] = createStore({
|
|
|
- status: {} as Record<string, ServerHealth | undefined>,
|
|
|
+ status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
|
|
|
addServer: {
|
|
|
url: "",
|
|
|
+ name: "",
|
|
|
+ username: "",
|
|
|
+ password: "",
|
|
|
adding: false,
|
|
|
error: "",
|
|
|
showForm: false,
|
|
|
@@ -189,6 +188,9 @@ export function DialogSelectServer() {
|
|
|
editServer: {
|
|
|
id: undefined as string | undefined,
|
|
|
value: "",
|
|
|
+ name: "",
|
|
|
+ username: "",
|
|
|
+ password: "",
|
|
|
error: "",
|
|
|
busy: false,
|
|
|
status: undefined as boolean | undefined,
|
|
|
@@ -198,40 +200,46 @@ export function DialogSelectServer() {
|
|
|
const resetAdd = () => {
|
|
|
setStore("addServer", {
|
|
|
url: "",
|
|
|
+ name: "",
|
|
|
+ username: "",
|
|
|
+ password: "",
|
|
|
+ adding: false,
|
|
|
error: "",
|
|
|
showForm: false,
|
|
|
status: undefined,
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
const resetEdit = () => {
|
|
|
setStore("editServer", {
|
|
|
id: undefined,
|
|
|
value: "",
|
|
|
+ name: "",
|
|
|
+ username: "",
|
|
|
+ password: "",
|
|
|
error: "",
|
|
|
status: undefined,
|
|
|
busy: false,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- const replaceServer = (original: string, next: string) => {
|
|
|
- const active = server.url
|
|
|
- const nextActive = active === original ? next : active
|
|
|
-
|
|
|
- server.add(next)
|
|
|
+ const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
|
|
+ const active = server.key
|
|
|
+ const newConn = server.add(next)
|
|
|
+ if (!newConn) return
|
|
|
+ const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
|
|
|
if (nextActive) server.setActive(nextActive)
|
|
|
- server.remove(original)
|
|
|
+ server.remove(ServerConnection.key(original))
|
|
|
}
|
|
|
|
|
|
const items = createMemo(() => {
|
|
|
- const current = server.url
|
|
|
+ const current = server.current
|
|
|
const list = server.list
|
|
|
if (!current) return list
|
|
|
if (!list.includes(current)) return [current, ...list]
|
|
|
return [current, ...list.filter((x) => x !== current)]
|
|
|
})
|
|
|
|
|
|
- const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
|
|
|
+ const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0])
|
|
|
|
|
|
const sortedItems = createMemo(() => {
|
|
|
const list = items()
|
|
|
@@ -246,17 +254,17 @@ export function DialogSelectServer() {
|
|
|
return list.slice().sort((a, b) => {
|
|
|
if (a === active) return -1
|
|
|
if (b === active) return 1
|
|
|
- const diff = rank(store.status[a]) - rank(store.status[b])
|
|
|
+ const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)])
|
|
|
if (diff !== 0) return diff
|
|
|
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
|
|
})
|
|
|
})
|
|
|
|
|
|
async function refreshHealth() {
|
|
|
- const results: Record<string, ServerHealth> = {}
|
|
|
+ const results: Record<ServerConnection.Key, ServerHealth> = {}
|
|
|
await Promise.all(
|
|
|
- items().map(async (url) => {
|
|
|
- results[url] = await checkServerHealth(url, fetcher)
|
|
|
+ items().map(async (conn) => {
|
|
|
+ results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
|
|
|
}),
|
|
|
)
|
|
|
setStore("status", reconcile(results))
|
|
|
@@ -269,36 +277,74 @@ export function DialogSelectServer() {
|
|
|
onCleanup(() => clearInterval(interval))
|
|
|
})
|
|
|
|
|
|
- async function select(value: string, persist?: boolean) {
|
|
|
- if (!persist && store.status[value]?.healthy === false) return
|
|
|
+ async function select(conn: ServerConnection.Any, persist?: boolean) {
|
|
|
+ if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
|
|
|
dialog.close()
|
|
|
- if (persist) {
|
|
|
- server.add(value)
|
|
|
+ if (persist && conn.type === "http") {
|
|
|
+ server.add(conn)
|
|
|
navigate("/")
|
|
|
return
|
|
|
}
|
|
|
- server.setActive(value)
|
|
|
+ server.setActive(ServerConnection.key(conn))
|
|
|
navigate("/")
|
|
|
}
|
|
|
|
|
|
const handleAddChange = (value: string) => {
|
|
|
if (store.addServer.adding) return
|
|
|
setStore("addServer", { url: value, error: "" })
|
|
|
- void previewStatus(value, (next) => setStore("addServer", { status: next }))
|
|
|
+ void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
|
|
+ setStore("addServer", { status: next }),
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- const scrollListToBottom = () => {
|
|
|
- const scroll = listRoot?.querySelector<HTMLDivElement>('[data-slot="list-scroll"]')
|
|
|
- if (!scroll) return
|
|
|
- requestAnimationFrame(() => {
|
|
|
- scroll.scrollTop = scroll.scrollHeight
|
|
|
- })
|
|
|
+ const handleAddNameChange = (value: string) => {
|
|
|
+ if (store.addServer.adding) return
|
|
|
+ setStore("addServer", { name: value, error: "" })
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleAddUsernameChange = (value: string) => {
|
|
|
+ if (store.addServer.adding) return
|
|
|
+ setStore("addServer", { username: value, error: "" })
|
|
|
+ void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
|
|
+ setStore("addServer", { status: next }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleAddPasswordChange = (value: string) => {
|
|
|
+ if (store.addServer.adding) return
|
|
|
+ setStore("addServer", { password: value, error: "" })
|
|
|
+ void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
|
|
+ setStore("addServer", { status: next }),
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
const handleEditChange = (value: string) => {
|
|
|
if (store.editServer.busy) return
|
|
|
setStore("editServer", { value, error: "" })
|
|
|
- void previewStatus(value, (next) => setStore("editServer", { status: next }))
|
|
|
+ void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
|
|
+ setStore("editServer", { status: next }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleEditNameChange = (value: string) => {
|
|
|
+ if (store.editServer.busy) return
|
|
|
+ setStore("editServer", { name: value, error: "" })
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleEditUsernameChange = (value: string) => {
|
|
|
+ if (store.editServer.busy) return
|
|
|
+ setStore("editServer", { username: value, error: "" })
|
|
|
+ void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
|
|
+ setStore("editServer", { status: next }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleEditPasswordChange = (value: string) => {
|
|
|
+ if (store.editServer.busy) return
|
|
|
+ setStore("editServer", { password: value, error: "" })
|
|
|
+ void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
|
|
+ setStore("editServer", { status: next }),
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
async function handleAdd(value: string) {
|
|
|
@@ -311,74 +357,142 @@ export function DialogSelectServer() {
|
|
|
|
|
|
setStore("addServer", { adding: true, error: "" })
|
|
|
|
|
|
- const result = await checkServerHealth(normalized, fetcher)
|
|
|
+ const conn: ServerConnection.Http = {
|
|
|
+ type: "http",
|
|
|
+ http: { url: normalized },
|
|
|
+ }
|
|
|
+ if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
|
|
+ if (store.addServer.username) conn.http.username = store.addServer.username
|
|
|
+ if (store.addServer.password) conn.http.password = store.addServer.password
|
|
|
+ const result = await checkServerHealth(conn.http, fetcher)
|
|
|
setStore("addServer", { adding: false })
|
|
|
-
|
|
|
if (!result.healthy) {
|
|
|
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
resetAdd()
|
|
|
- await select(normalized, true)
|
|
|
+ await select(conn, true)
|
|
|
}
|
|
|
|
|
|
- async function handleEdit(original: string, value: string) {
|
|
|
- if (store.editServer.busy) return
|
|
|
+ async function handleEdit(original: ServerConnection.Any, value: string) {
|
|
|
+ if (store.editServer.busy || original.type !== "http") return
|
|
|
const normalized = normalizeServerUrl(value)
|
|
|
if (!normalized) {
|
|
|
resetEdit()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (normalized === original) {
|
|
|
+ const name = store.editServer.name.trim() || undefined
|
|
|
+ const username = store.editServer.username || undefined
|
|
|
+ const password = store.editServer.password || undefined
|
|
|
+ const existingName = original.displayName
|
|
|
+ if (
|
|
|
+ normalized === original.http.url &&
|
|
|
+ name === existingName &&
|
|
|
+ username === original.http.username &&
|
|
|
+ password === original.http.password
|
|
|
+ ) {
|
|
|
resetEdit()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
setStore("editServer", { busy: true, error: "" })
|
|
|
|
|
|
- const result = await checkServerHealth(normalized, fetcher)
|
|
|
+ const conn: ServerConnection.Http = {
|
|
|
+ type: "http",
|
|
|
+ displayName: name,
|
|
|
+ http: { url: normalized, username, password },
|
|
|
+ }
|
|
|
+ const result = await checkServerHealth(conn.http, fetcher)
|
|
|
setStore("editServer", { busy: false })
|
|
|
-
|
|
|
if (!result.healthy) {
|
|
|
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
|
|
return
|
|
|
}
|
|
|
+ if (normalized === original.http.url) {
|
|
|
+ server.add(conn)
|
|
|
+ } else {
|
|
|
+ replaceServer(original, conn)
|
|
|
+ }
|
|
|
+
|
|
|
+ resetEdit()
|
|
|
+ }
|
|
|
+
|
|
|
+ const mode = createMemo<"list" | "add" | "edit">(() => {
|
|
|
+ if (store.editServer.id) return "edit"
|
|
|
+ if (store.addServer.showForm) return "add"
|
|
|
+ return "list"
|
|
|
+ })
|
|
|
|
|
|
- replaceServer(original, normalized)
|
|
|
+ const editing = createMemo(() => {
|
|
|
+ if (!store.editServer.id) return
|
|
|
+ return items().find((x) => x.type === "http" && x.http.url === store.editServer.id)
|
|
|
+ })
|
|
|
|
|
|
+ const resetForm = () => {
|
|
|
+ resetAdd()
|
|
|
resetEdit()
|
|
|
}
|
|
|
|
|
|
- const handleAddKey = (event: KeyboardEvent) => {
|
|
|
- event.stopPropagation()
|
|
|
- if (event.key !== "Enter" || event.isComposing) return
|
|
|
- event.preventDefault()
|
|
|
- handleAdd(store.addServer.url)
|
|
|
+ const startAdd = () => {
|
|
|
+ resetEdit()
|
|
|
+ setStore("addServer", {
|
|
|
+ showForm: true,
|
|
|
+ url: "",
|
|
|
+ name: "",
|
|
|
+ username: "",
|
|
|
+ password: "",
|
|
|
+ error: "",
|
|
|
+ status: undefined,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- const blurAdd = () => {
|
|
|
- if (!store.addServer.url.trim()) {
|
|
|
- resetAdd()
|
|
|
- return
|
|
|
- }
|
|
|
- handleAdd(store.addServer.url)
|
|
|
+ const startEdit = (conn: ServerConnection.Http) => {
|
|
|
+ resetAdd()
|
|
|
+ setStore("editServer", {
|
|
|
+ id: conn.http.url,
|
|
|
+ value: conn.http.url,
|
|
|
+ name: conn.displayName ?? "",
|
|
|
+ username: conn.http.username ?? "",
|
|
|
+ password: conn.http.password ?? "",
|
|
|
+ error: "",
|
|
|
+ status: store.status[ServerConnection.key(conn)]?.healthy,
|
|
|
+ busy: false,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- const handleEditKey = (event: KeyboardEvent, original: string) => {
|
|
|
- event.stopPropagation()
|
|
|
- if (event.key === "Escape") {
|
|
|
- event.preventDefault()
|
|
|
- resetEdit()
|
|
|
+ const submitForm = () => {
|
|
|
+ if (mode() === "add") {
|
|
|
+ void handleAdd(store.addServer.url)
|
|
|
return
|
|
|
}
|
|
|
- if (event.key !== "Enter" || event.isComposing) return
|
|
|
- event.preventDefault()
|
|
|
- handleEdit(original, store.editServer.value)
|
|
|
+ const original = editing()
|
|
|
+ if (!original) return
|
|
|
+ void handleEdit(original, store.editServer.value)
|
|
|
}
|
|
|
|
|
|
- async function handleRemove(url: string) {
|
|
|
+ const isFormMode = createMemo(() => mode() !== "list")
|
|
|
+ const isAddMode = createMemo(() => mode() === "add")
|
|
|
+ const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy))
|
|
|
+
|
|
|
+ const formTitle = createMemo(() => {
|
|
|
+ if (!isFormMode()) return language.t("dialog.server.title")
|
|
|
+ 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>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ createEffect(() => {
|
|
|
+ if (!store.editServer.id) return
|
|
|
+ if (editing()) return
|
|
|
+ resetEdit()
|
|
|
+ })
|
|
|
+
|
|
|
+ async function handleRemove(url: ServerConnection.Key) {
|
|
|
server.remove(url)
|
|
|
if ((await platform.getDefaultServerUrl?.()) === url) {
|
|
|
platform.setDefaultServerUrl?.(null)
|
|
|
@@ -386,82 +500,71 @@ export function DialogSelectServer() {
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <Dialog title={language.t("dialog.server.title")}>
|
|
|
+ <Dialog title={formTitle()}>
|
|
|
<div class="flex flex-col gap-2">
|
|
|
- <div ref={(el) => (listRoot = el)}>
|
|
|
+ <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}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ >
|
|
|
<List
|
|
|
- search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
|
|
+ search={{
|
|
|
+ placeholder: language.t("dialog.server.search.placeholder"),
|
|
|
+ autofocus: false,
|
|
|
+ }}
|
|
|
noInitialSelection
|
|
|
emptyMessage={language.t("dialog.server.empty")}
|
|
|
items={sortedItems}
|
|
|
- key={(x) => x}
|
|
|
+ key={(x) => x.http.url}
|
|
|
onSelect={(x) => {
|
|
|
if (x) select(x)
|
|
|
}}
|
|
|
- onFilter={(value) => {
|
|
|
- if (value && store.addServer.showForm && !store.addServer.adding) {
|
|
|
- resetAdd()
|
|
|
- }
|
|
|
- }}
|
|
|
divider={true}
|
|
|
- class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
|
|
|
- add={
|
|
|
- store.addServer.showForm
|
|
|
- ? {
|
|
|
- render: () => (
|
|
|
- <AddRow
|
|
|
- value={store.addServer.url}
|
|
|
- placeholder={language.t("dialog.server.add.placeholder")}
|
|
|
- adding={store.addServer.adding}
|
|
|
- error={store.addServer.error}
|
|
|
- status={store.addServer.status}
|
|
|
- onChange={handleAddChange}
|
|
|
- onKeyDown={handleAddKey}
|
|
|
- onBlur={blurAdd}
|
|
|
- />
|
|
|
- ),
|
|
|
- }
|
|
|
- : undefined
|
|
|
- }
|
|
|
+ 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-raised-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 group/item">
|
|
|
- <Show
|
|
|
- when={store.editServer.id !== i}
|
|
|
- fallback={
|
|
|
- <EditRow
|
|
|
- value={store.editServer.value}
|
|
|
- placeholder={language.t("dialog.server.add.placeholder")}
|
|
|
- busy={store.editServer.busy}
|
|
|
- error={store.editServer.error}
|
|
|
- status={store.editServer.status}
|
|
|
- onChange={handleEditChange}
|
|
|
- onKeyDown={(event) => handleEditKey(event, i)}
|
|
|
- onBlur={() => handleEdit(i, store.editServer.value)}
|
|
|
- />
|
|
|
- }
|
|
|
- >
|
|
|
- <ServerRow
|
|
|
- url={i}
|
|
|
- status={store.status[i]}
|
|
|
- dimmed={store.status[i]?.healthy === false}
|
|
|
- class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
|
|
- badge={
|
|
|
- <Show when={defaultUrl() === i}>
|
|
|
- <span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
|
|
- {language.t("dialog.server.status.default")}
|
|
|
- </span>
|
|
|
- </Show>
|
|
|
- }
|
|
|
- />
|
|
|
- </Show>
|
|
|
- <Show when={store.editServer.id !== i}>
|
|
|
- <div class="flex items-center justify-center gap-5 pl-4">
|
|
|
- <Show when={current() === i}>
|
|
|
- <p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
|
|
+ <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={defaultUrl() === i.http.url}>
|
|
|
+ <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}
|
|
|
@@ -475,24 +578,20 @@ export function DialogSelectServer() {
|
|
|
<DropdownMenu.Content class="mt-1">
|
|
|
<DropdownMenu.Item
|
|
|
onSelect={() => {
|
|
|
- setStore("editServer", {
|
|
|
- id: i,
|
|
|
- value: i,
|
|
|
- error: "",
|
|
|
- status: store.status[i]?.healthy,
|
|
|
- })
|
|
|
+ if (i.type !== "http") return
|
|
|
+ startEdit(i)
|
|
|
}}
|
|
|
>
|
|
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
|
|
</DropdownMenu.Item>
|
|
|
- <Show when={canDefault() && defaultUrl() !== i}>
|
|
|
- <DropdownMenu.Item onSelect={() => setDefault(i)}>
|
|
|
+ <Show when={canDefault() && defaultUrl() !== i.http.url}>
|
|
|
+ <DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
|
|
|
<DropdownMenu.ItemLabel>
|
|
|
{language.t("dialog.server.menu.default")}
|
|
|
</DropdownMenu.ItemLabel>
|
|
|
</DropdownMenu.Item>
|
|
|
</Show>
|
|
|
- <Show when={canDefault() && defaultUrl() === i}>
|
|
|
+ <Show when={canDefault() && defaultUrl() === i.http.url}>
|
|
|
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
|
|
<DropdownMenu.ItemLabel>
|
|
|
{language.t("dialog.server.menu.defaultRemove")}
|
|
|
@@ -501,7 +600,7 @@ export function DialogSelectServer() {
|
|
|
</Show>
|
|
|
<DropdownMenu.Separator />
|
|
|
<DropdownMenu.Item
|
|
|
- onSelect={() => handleRemove(i)}
|
|
|
+ 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>
|
|
|
@@ -509,27 +608,37 @@ export function DialogSelectServer() {
|
|
|
</DropdownMenu.Content>
|
|
|
</DropdownMenu.Portal>
|
|
|
</DropdownMenu>
|
|
|
- </div>
|
|
|
- </Show>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
)
|
|
|
}}
|
|
|
</List>
|
|
|
- </div>
|
|
|
+ </Show>
|
|
|
|
|
|
<div class="px-5 pb-5">
|
|
|
- <Button
|
|
|
- variant="secondary"
|
|
|
- icon="plus-small"
|
|
|
- size="large"
|
|
|
- onClick={() => {
|
|
|
- setStore("addServer", { showForm: true, url: "", error: "" })
|
|
|
- scrollListToBottom()
|
|
|
- }}
|
|
|
- class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
|
|
+ <Show
|
|
|
+ when={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>
|
|
|
+ }
|
|
|
>
|
|
|
- {store.addServer.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
|
|
|
- </Button>
|
|
|
+ <Button variant="primary" size="large" onClick={submitForm} disabled={formBusy()} class="px-3 py-1.5">
|
|
|
+ {formBusy()
|
|
|
+ ? language.t("dialog.server.add.checking")
|
|
|
+ : isAddMode()
|
|
|
+ ? language.t("dialog.server.add.button")
|
|
|
+ : language.t("common.save")}
|
|
|
+ </Button>
|
|
|
+ </Show>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Dialog>
|