|
| 1 | +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" |
| 2 | +import { Tag } from "@opencode-ai/ui/v2/badge-v2" |
| 3 | +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" |
| 4 | +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" |
| 5 | +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" |
| 6 | +import { useDialog } from "@opencode-ai/ui/context/dialog" |
| 7 | +import fuzzysort from "fuzzysort" |
| 8 | +import { type Component, For, Show, createMemo } from "solid-js" |
| 9 | +import { createStore } from "solid-js/store" |
| 10 | +import { ServerRowMenu } from "@/components/server/server-row-menu" |
| 11 | +import { ServerHealthIndicator } from "@/components/server/server-row" |
| 12 | +import { useLanguage } from "@/context/language" |
| 13 | +import { ServerConnection, serverName } from "@/context/server" |
| 14 | +import { useServerManagementController } from "../dialog-select-server" |
| 15 | +import { DialogServerV2 } from "./dialog-server-v2" |
| 16 | +import { SettingsListV2 } from "./parts/list" |
| 17 | +import "./settings-v2.css" |
| 18 | + |
| 19 | +export const SettingsServersV2: Component = () => { |
| 20 | + const dialog = useDialog() |
| 21 | + const language = useLanguage() |
| 22 | + const controller = useServerManagementController() |
| 23 | + const [store, setStore] = createStore({ filter: "" }) |
| 24 | + |
| 25 | + const showSearch = createMemo(() => controller.sortedItems().length > 1) |
| 26 | + |
| 27 | + const filtered = createMemo(() => { |
| 28 | + const items = controller.sortedItems() |
| 29 | + const query = store.filter.trim() |
| 30 | + if (!query) return items |
| 31 | + return fuzzysort |
| 32 | + .go(query, items, { |
| 33 | + keys: [(item) => serverName(item), (item) => item.http.url], |
| 34 | + }) |
| 35 | + .map((result) => result.obj) |
| 36 | + }) |
| 37 | + |
| 38 | + const openAdd = () => { |
| 39 | + dialog.push(() => <DialogServerV2 mode="add" />) |
| 40 | + } |
| 41 | + |
| 42 | + const openEdit = (server: ServerConnection.Http) => { |
| 43 | + dialog.push(() => <DialogServerV2 mode="edit" server={server} />) |
| 44 | + } |
| 45 | + |
| 46 | + return ( |
| 47 | + <> |
| 48 | + <div |
| 49 | + class="settings-v2-tab-header settings-v2-servers-header" |
| 50 | + classList={{ "settings-v2-tab-header--stacked": showSearch() }} |
| 51 | + > |
| 52 | + <div class="settings-v2-tab-header-row"> |
| 53 | + <h2 class="settings-v2-tab-title">{language.t("status.popover.tab.servers")}</h2> |
| 54 | + <ButtonV2 variant="ghost-muted" icon="plus" onClick={openAdd}> |
| 55 | + {language.t("dialog.server.add.button")} |
| 56 | + </ButtonV2> |
| 57 | + </div> |
| 58 | + <Show when={showSearch()}> |
| 59 | + <div class="settings-v2-tab-search"> |
| 60 | + <TextInputV2 |
| 61 | + type="search" |
| 62 | + appearance="base" |
| 63 | + value={store.filter} |
| 64 | + onInput={(event) => setStore("filter", event.currentTarget.value)} |
| 65 | + placeholder={language.t("dialog.server.search.placeholder")} |
| 66 | + spellcheck={false} |
| 67 | + autocorrect="off" |
| 68 | + autocomplete="off" |
| 69 | + autocapitalize="off" |
| 70 | + aria-label={language.t("dialog.server.search.placeholder")} |
| 71 | + /> |
| 72 | + <Show when={store.filter}> |
| 73 | + <IconButtonV2 |
| 74 | + type="button" |
| 75 | + variant="ghost-muted" |
| 76 | + size="small" |
| 77 | + class="settings-v2-tab-search-clear" |
| 78 | + icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />} |
| 79 | + onClick={() => setStore("filter", "")} |
| 80 | + /> |
| 81 | + </Show> |
| 82 | + </div> |
| 83 | + </Show> |
| 84 | + </div> |
| 85 | + |
| 86 | + <div class="settings-v2-tab-body settings-v2-servers"> |
| 87 | + <Show |
| 88 | + when={filtered().length > 0} |
| 89 | + fallback={ |
| 90 | + <div class="settings-v2-servers-status"> |
| 91 | + <span>{store.filter ? language.t("palette.empty") : language.t("dialog.server.empty")}</span> |
| 92 | + <Show when={store.filter}> |
| 93 | + <span class="settings-v2-servers-status-filter">"{store.filter}"</span> |
| 94 | + </Show> |
| 95 | + </div> |
| 96 | + } |
| 97 | + > |
| 98 | + <SettingsListV2> |
| 99 | + <For each={filtered()}> |
| 100 | + {(item) => { |
| 101 | + const key = ServerConnection.key(item) |
| 102 | + const health = () => controller.status()[key] |
| 103 | + const isDefault = () => controller.defaultKey() === key |
| 104 | + return ( |
| 105 | + <div class="settings-v2-servers-row"> |
| 106 | + <div class="settings-v2-servers-lead"> |
| 107 | + <ServerHealthIndicator health={health()} /> |
| 108 | + <div class="settings-v2-servers-copy"> |
| 109 | + <span class="settings-v2-servers-name">{serverName(item)}</span> |
| 110 | + <span class="settings-v2-servers-meta"> |
| 111 | + <Show when={health()?.version}>v{health()?.version}</Show> |
| 112 | + <Show when={health()?.version && item.type === "http"}> • </Show> |
| 113 | + <Show |
| 114 | + when={item.type === "http" && item.http.username} |
| 115 | + fallback={ |
| 116 | + <Show when={item.type === "http"}>{language.t("server.row.noUsername")}</Show> |
| 117 | + } |
| 118 | + > |
| 119 | + {item.http.username} |
| 120 | + </Show> |
| 121 | + </span> |
| 122 | + </div> |
| 123 | + </div> |
| 124 | + <div class="settings-v2-servers-actions"> |
| 125 | + <Show when={controller.canDefault() && isDefault()}> |
| 126 | + <Tag>{language.t("dialog.server.status.default")}</Tag> |
| 127 | + </Show> |
| 128 | + <ServerRowMenu server={item} controller={controller} onEdit={openEdit} /> |
| 129 | + </div> |
| 130 | + </div> |
| 131 | + ) |
| 132 | + }} |
| 133 | + </For> |
| 134 | + </SettingsListV2> |
| 135 | + </Show> |
| 136 | + </div> |
| 137 | + </> |
| 138 | + ) |
| 139 | +} |
0 commit comments