feat(app): update manage servers dialog styling and behavior
This commit is contained in:
@@ -12,6 +12,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
|
||||||
type ServerStatus = { healthy: boolean; version?: string }
|
type ServerStatus = { healthy: boolean; version?: string }
|
||||||
@@ -52,16 +53,16 @@ async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>
|
|||||||
|
|
||||||
function AddRow(props: AddRowProps) {
|
function AddRow(props: AddRowProps) {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-3 px-4 min-w-0 flex-1">
|
<div class="flex items-center px-3 h-14 min-w-0 flex-1">
|
||||||
<div
|
<div class="relative flex-1 min-w-0">
|
||||||
classList={{
|
<div
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
classList={{
|
||||||
"bg-icon-success-base": props.status === true,
|
"size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2": true,
|
||||||
"bg-icon-critical-base": props.status === false,
|
"bg-icon-success-base": props.status === true,
|
||||||
"bg-border-weak-base": props.status === undefined,
|
"bg-icon-critical-base": props.status === false,
|
||||||
}}
|
"bg-border-weak-base": props.status === undefined,
|
||||||
/>
|
}}
|
||||||
<div class="flex-1 min-w-0">
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
hideLabel
|
hideLabel
|
||||||
@@ -74,6 +75,7 @@ function AddRow(props: AddRowProps) {
|
|||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
onKeyDown={props.onKeyDown}
|
onKeyDown={props.onKeyDown}
|
||||||
onBlur={props.onBlur}
|
onBlur={props.onBlur}
|
||||||
|
class="pl-7"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -344,9 +346,10 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={language.t("dialog.server.title")}>
|
<Dialog title={language.t("dialog.server.title")}>
|
||||||
<div class="flex flex-col gap-2 pb-4">
|
<div class="flex flex-col gap-2 pb-5">
|
||||||
<List
|
<List
|
||||||
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
|
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
||||||
|
noInitialSelection
|
||||||
emptyMessage={language.t("dialog.server.empty")}
|
emptyMessage={language.t("dialog.server.empty")}
|
||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
key={(x) => x}
|
key={(x) => x}
|
||||||
@@ -354,7 +357,7 @@ export function DialogSelectServer() {
|
|||||||
if (x) select(x)
|
if (x) select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
class="[&_[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]]:py-3"
|
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"
|
||||||
add={
|
add={
|
||||||
store.addServer.showForm
|
store.addServer.showForm
|
||||||
? {
|
? {
|
||||||
@@ -376,6 +379,35 @@ export function DialogSelectServer() {
|
|||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
||||||
|
const [truncated, setTruncated] = createSignal(false)
|
||||||
|
let nameRef: HTMLSpanElement | undefined
|
||||||
|
let versionRef: HTMLSpanElement | undefined
|
||||||
|
|
||||||
|
const check = () => {
|
||||||
|
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
||||||
|
const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
|
||||||
|
setTruncated(nameTruncated || versionTruncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
check()
|
||||||
|
window.addEventListener("resize", check)
|
||||||
|
onCleanup(() => window.removeEventListener("resize", check))
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipValue = () => {
|
||||||
|
const name = serverDisplayName(i)
|
||||||
|
const version = store.status[i]?.version
|
||||||
|
return (
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<span>{name}</span>
|
||||||
|
<Show when={version}>
|
||||||
|
<span class="text-text-invert-base">{version}</span>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||||
<Show
|
<Show
|
||||||
@@ -393,28 +425,34 @@ export function DialogSelectServer() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
|
||||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
|
||||||
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
||||||
"bg-icon-success-base": store.status[i]?.healthy === true,
|
>
|
||||||
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
<div
|
||||||
"bg-border-weak-base": store.status[i] === undefined,
|
classList={{
|
||||||
}}
|
"size-1.5 rounded-full shrink-0": true,
|
||||||
/>
|
"bg-icon-success-base": store.status[i]?.healthy === true,
|
||||||
<span class="truncate">{serverDisplayName(i)}</span>
|
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
||||||
<Show when={store.status[i]?.version}>
|
"bg-border-weak-base": store.status[i] === undefined,
|
||||||
<span class="text-text-weak text-14-regular">{store.status[i]?.version}</span>
|
}}
|
||||||
</Show>
|
/>
|
||||||
<Show when={defaultUrl() === i}>
|
<span ref={nameRef} class="truncate">
|
||||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
{serverDisplayName(i)}
|
||||||
{language.t("dialog.server.status.default")}
|
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
<Show when={store.status[i]?.version}>
|
||||||
</div>
|
<span ref={versionRef} class="text-text-weak text-14-regular truncate">
|
||||||
|
{store.status[i]?.version}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={store.editServer.id !== i}>
|
<Show when={store.editServer.id !== i}>
|
||||||
<div class="flex items-center justify-center gap-5 px-4">
|
<div class="flex items-center justify-center gap-5 px-4">
|
||||||
@@ -508,7 +546,7 @@ export function DialogSelectServer() {
|
|||||||
}}
|
}}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<div class="px-6">
|
<div class="px-5">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
[data-slot="dialog-header"] {
|
[data-slot="dialog-header"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px 16px 16px 24px;
|
padding: 20px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface FilteredListProps<T> {
|
|||||||
sortBy?: (a: T, b: T) => number
|
sortBy?: (a: T, b: T) => number
|
||||||
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
|
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
|
||||||
onSelect?: (value: T | undefined, index: number) => void
|
onSelect?: (value: T | undefined, index: number) => void
|
||||||
|
noInitialSelection?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFilteredList<T>(props: FilteredListProps<T>) {
|
export function useFilteredList<T>(props: FilteredListProps<T>) {
|
||||||
@@ -57,6 +58,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function initialActive() {
|
function initialActive() {
|
||||||
|
if (props.noInitialSelection) return ""
|
||||||
if (props.current) return props.key(props.current)
|
if (props.current) return props.key(props.current)
|
||||||
|
|
||||||
const items = flat()
|
const items = flat()
|
||||||
@@ -71,6 +73,10 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
if (props.noInitialSelection) {
|
||||||
|
list.setActive("")
|
||||||
|
return
|
||||||
|
}
|
||||||
const all = flat()
|
const all = flat()
|
||||||
if (all.length === 0) return
|
if (all.length === 0) return
|
||||||
list.setActive(props.key(all[0]))
|
list.setActive(props.key(all[0]))
|
||||||
|
|||||||
Reference in New Issue
Block a user