dsh-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +29 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.en.md +106 -0
- package/README.md +89 -0
- package/lib/client.js +15820 -0
- package/lib/index.js +894 -0
- package/lib/probe.js +53 -0
- package/lib/transport.js +25 -0
- package/package.json +47 -0
- package/scripts/build.mjs +144 -0
- package/src/client/McpSettingsSection.module.css +236 -0
- package/src/client/McpSettingsSection.tsx +351 -0
- package/src/client/ServerForm.module.css +196 -0
- package/src/client/ServerForm.tsx +301 -0
- package/src/client/ToolControlSection.module.css +100 -0
- package/src/client/ToolControlSection.tsx +137 -0
- package/src/client/index.ts +160 -0
- package/src/client/locales.ts +152 -0
- package/src/client/mcp-store.ts +249 -0
- package/src/client/remote-contribution.js +339 -0
- package/src/client/types.ts +124 -0
- package/src/css-modules.d.ts +6 -0
- package/static/snapshot.webp +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
McpManagerFailure, McpServerId, McpServerView,
|
|
4
|
+
McpToolInjectionMode, McpToolsState, McpToolView,
|
|
5
|
+
} from './types.ts'
|
|
6
|
+
import type { InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
|
|
7
|
+
import { McpServerForm, type McpServerFormRemote } from './ServerForm.tsx'
|
|
8
|
+
import type { McpToolControlRemote } from './ToolControlSection.tsx'
|
|
9
|
+
import type { McpSettingsLocaleKey } from './locales.ts'
|
|
10
|
+
import { createMcpManagerStore, type McpDraft, type McpTestOutcome } from './mcp-store.ts'
|
|
11
|
+
import css from './McpSettingsSection.module.css'
|
|
12
|
+
|
|
13
|
+
/** Registration-side Remote face used by the section. */
|
|
14
|
+
export interface McpManagerInjected extends McpServerFormRemote, McpToolControlRemote {
|
|
15
|
+
/** Read the current server list. */
|
|
16
|
+
list: () => Promise<readonly McpServerView[]>
|
|
17
|
+
/** Persist one draft; null means success, a failure is otherwise returned. */
|
|
18
|
+
save: (draft: McpDraft) => Promise<McpManagerFailure | null>
|
|
19
|
+
/** Delete one server; null means success, a failure is otherwise returned. */
|
|
20
|
+
remove: (id: McpServerId) => Promise<McpManagerFailure | null>
|
|
21
|
+
/** Probe one draft; `draft.id` lets stored secret values resolve. */
|
|
22
|
+
test: (draft: McpDraft) => Promise<McpTestOutcome>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Full component props assembled by the Settings slot renderer. */
|
|
26
|
+
export type McpSettingsSectionProps =
|
|
27
|
+
PropsRuntime<'settings.section'>
|
|
28
|
+
& PropsLocale<'settings.mcp'>
|
|
29
|
+
& PropsStore<ReturnType<typeof createMcpManagerStore>>
|
|
30
|
+
& InjectFace<McpManagerInjected>
|
|
31
|
+
|
|
32
|
+
/** Phase label key for a status badge. */
|
|
33
|
+
function phaseKey(phase: McpServerView['status']['phase']): McpSettingsLocaleKey {
|
|
34
|
+
switch (phase) {
|
|
35
|
+
case 'mounting': return 'statusMounting'
|
|
36
|
+
case 'live': return 'statusLive'
|
|
37
|
+
case 'failed': return 'statusFailed'
|
|
38
|
+
case 'stopped': return 'statusStopped'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Raw `mcp__<server>__<tool>` tail, for the tool list. */
|
|
43
|
+
function rawOf(name: string): string {
|
|
44
|
+
const rest = name.slice(5)
|
|
45
|
+
const i = rest.indexOf('__')
|
|
46
|
+
return i < 0 ? rest : rest.slice(i + 2)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rebuild an editable draft from a stored server view (quick toggle path). */
|
|
50
|
+
function viewToDraft(server: McpServerView): McpDraft {
|
|
51
|
+
return {
|
|
52
|
+
id: server.id,
|
|
53
|
+
serverName: server.serverName,
|
|
54
|
+
transport: server.transport,
|
|
55
|
+
enabled: server.enabled,
|
|
56
|
+
command: server.command,
|
|
57
|
+
argsText: server.args.join('\n'),
|
|
58
|
+
cwd: server.cwd,
|
|
59
|
+
url: server.url,
|
|
60
|
+
headersText: server.headers.map(header => `${header.name}: ${header.value}`).join('\n'),
|
|
61
|
+
env: server.env.map((entry, index) => ({
|
|
62
|
+
key: `view-${index}`,
|
|
63
|
+
name: entry.name,
|
|
64
|
+
secret: entry.secret,
|
|
65
|
+
value: '',
|
|
66
|
+
configured: entry.configured,
|
|
67
|
+
})),
|
|
68
|
+
toolCallTimeoutMs: String(server.toolCallTimeoutMs),
|
|
69
|
+
failOnStartupError: server.failOnStartupError,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Render the MCP management page: the injection-mode selector (default
|
|
75
|
+
* on-demand search), the server list with per-server refresh and an expandable
|
|
76
|
+
* per-server tool-binding list, or the editor when a draft is open.
|
|
77
|
+
*/
|
|
78
|
+
export function McpSettingsSection(props: McpSettingsSectionProps): ReactNode {
|
|
79
|
+
const { list, save, remove, test } = props
|
|
80
|
+
const state = props.useStore(snapshot => snapshot)
|
|
81
|
+
const { setLoadState, setServers, beginCreate, beginEdit, cancelEdit, updateDraft, setBusy, setTestRunning, setTest } = props.actions
|
|
82
|
+
const t = props.t
|
|
83
|
+
|
|
84
|
+
const [loadErrorDetail, setLoadErrorDetail] = useState<string | null>(null)
|
|
85
|
+
const [tools, setTools] = useState<McpToolsState | null>(null)
|
|
86
|
+
const [toolsError, setToolsError] = useState<string | null>(null)
|
|
87
|
+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(new Set())
|
|
88
|
+
const [refreshing, setRefreshing] = useState<ReadonlySet<string>>(new Set())
|
|
89
|
+
const timersRef = useRef<number[]>([])
|
|
90
|
+
|
|
91
|
+
useEffect(() => () => {
|
|
92
|
+
for (const id of timersRef.current) window.clearTimeout(id)
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
const refreshTools = (): void => {
|
|
96
|
+
void props.toolsList().then(
|
|
97
|
+
(next) => { setTools(next); setToolsError(null) },
|
|
98
|
+
(error) => {
|
|
99
|
+
console.error('[dsh-mcp] toolsList failed:', error)
|
|
100
|
+
setToolsError(String((error instanceof Error ? error.message : error) ?? error))
|
|
101
|
+
setTools(null)
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fail = (error: unknown): void => {
|
|
107
|
+
// The settings shell hides Remote failures behind a generic copy; surface
|
|
108
|
+
// the real message here so a broken list() is diagnosable from the page.
|
|
109
|
+
console.error('[dsh-mcp] list failed:', error)
|
|
110
|
+
setLoadErrorDetail(String((error instanceof Error ? error.message : error) ?? error))
|
|
111
|
+
setLoadState('error')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const load = (): void => {
|
|
115
|
+
void list().then(
|
|
116
|
+
(servers) => {
|
|
117
|
+
setServers(servers)
|
|
118
|
+
setLoadState('ready')
|
|
119
|
+
setLoadErrorDetail(null)
|
|
120
|
+
},
|
|
121
|
+
(error) => fail(error),
|
|
122
|
+
)
|
|
123
|
+
refreshTools()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
let current = true
|
|
128
|
+
void list().then(
|
|
129
|
+
(servers) => {
|
|
130
|
+
if (!current) return
|
|
131
|
+
setServers(servers)
|
|
132
|
+
setLoadState('ready')
|
|
133
|
+
setLoadErrorDetail(null)
|
|
134
|
+
},
|
|
135
|
+
(error) => { if (current) fail(error) },
|
|
136
|
+
)
|
|
137
|
+
void props.toolsList().then(
|
|
138
|
+
(next) => { if (current) { setTools(next); setToolsError(null) } },
|
|
139
|
+
(error) => {
|
|
140
|
+
if (!current) return
|
|
141
|
+
console.error('[dsh-mcp] toolsList failed:', error)
|
|
142
|
+
setToolsError(String((error instanceof Error ? error.message : error) ?? error))
|
|
143
|
+
setTools(null)
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
return () => { current = false }
|
|
147
|
+
}, [list, setLoadState, setServers])
|
|
148
|
+
|
|
149
|
+
const setMode = (mode: McpToolInjectionMode): void => {
|
|
150
|
+
void props.toolsMode({ mode }).then(refreshTools, (error) => {
|
|
151
|
+
console.error('[dsh-mcp] toolsMode failed:', error)
|
|
152
|
+
refreshTools()
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const toggleTool = (tool: McpToolView): void => {
|
|
157
|
+
const next = !tool.enabled
|
|
158
|
+
// Optimistic local toggle; the Host is the source of truth.
|
|
159
|
+
setTools(prev => prev === null
|
|
160
|
+
? prev
|
|
161
|
+
: { ...prev, tools: prev.tools.map(item => item.name === tool.name ? { ...item, enabled: next } : item) })
|
|
162
|
+
void props.toolsSet({ name: tool.name, enabled: next }).catch((error) => {
|
|
163
|
+
console.error('[dsh-mcp] toolsSet failed:', error)
|
|
164
|
+
refreshTools()
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const refreshServer = (serverName: string): void => {
|
|
169
|
+
setRefreshing(prev => new Set(prev).add(serverName))
|
|
170
|
+
void Promise.all([
|
|
171
|
+
list().then(
|
|
172
|
+
(servers) => { setServers(servers); setLoadState('ready') },
|
|
173
|
+
() => {},
|
|
174
|
+
),
|
|
175
|
+
props.toolsList().then(setTools, () => {}),
|
|
176
|
+
]).finally(() => {
|
|
177
|
+
setRefreshing(prev => {
|
|
178
|
+
const next = new Set(prev)
|
|
179
|
+
next.delete(serverName)
|
|
180
|
+
return next
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const toggleExpand = (serverName: string): void => {
|
|
186
|
+
setExpanded(prev => {
|
|
187
|
+
const next = new Set(prev)
|
|
188
|
+
if (next.has(serverName)) next.delete(serverName)
|
|
189
|
+
else next.add(serverName)
|
|
190
|
+
return next
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const toggleEnabled = async (server: McpServerView): Promise<void> => {
|
|
195
|
+
const target = !server.enabled
|
|
196
|
+
const draft = viewToDraft(server)
|
|
197
|
+
draft.enabled = target
|
|
198
|
+
try {
|
|
199
|
+
const failure = await save(draft)
|
|
200
|
+
if (failure !== null) {
|
|
201
|
+
console.error('[dsh-mcp] setEnabled failed:', failure.message)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
await refreshServer(server.serverName)
|
|
205
|
+
if (target) {
|
|
206
|
+
// Mounting is asynchronous: refresh again after the connection settles
|
|
207
|
+
// so the badge and tool count reflect the live state without a manual
|
|
208
|
+
// refresh.
|
|
209
|
+
timersRef.current.push(window.setTimeout(() => refreshServer(server.serverName), 2000))
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('[dsh-mcp] setEnabled failed:', error)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const serverTools = (serverName: string): McpToolView[] =>
|
|
217
|
+
(tools?.tools ?? []).filter(tool => tool.server === serverName)
|
|
218
|
+
|
|
219
|
+
const mode = tools?.mode ?? 'search'
|
|
220
|
+
|
|
221
|
+
if (state.loadState === 'loading') {
|
|
222
|
+
return <p className={css.status} aria-busy="true">{t('loading')}</p>
|
|
223
|
+
}
|
|
224
|
+
if (state.loadState === 'error') {
|
|
225
|
+
return (
|
|
226
|
+
<div className={css.failure}>
|
|
227
|
+
<p role="alert">{t('loadError')}</p>
|
|
228
|
+
{loadErrorDetail !== null ? <pre className={css.muted}>{loadErrorDetail}</pre> : null}
|
|
229
|
+
<button type="button" onClick={() => { setLoadState('loading'); load() }}>{t('retry')}</button>
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
if (state.draft !== null) {
|
|
234
|
+
const actions = {
|
|
235
|
+
updateDraft,
|
|
236
|
+
cancelEdit,
|
|
237
|
+
setBusy,
|
|
238
|
+
setTestRunning,
|
|
239
|
+
setTest,
|
|
240
|
+
setServers,
|
|
241
|
+
}
|
|
242
|
+
return (
|
|
243
|
+
<McpServerForm
|
|
244
|
+
draft={state.draft}
|
|
245
|
+
busy={state.busy}
|
|
246
|
+
testRunning={state.testRunning}
|
|
247
|
+
test={state.test}
|
|
248
|
+
t={t}
|
|
249
|
+
actions={actions}
|
|
250
|
+
injected={{ save, remove, test, list }}
|
|
251
|
+
/>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div className={css.section}>
|
|
257
|
+
<div className={css.header}>
|
|
258
|
+
<button type="button" className={css.primary} onClick={beginCreate}>{t('addServer')}</button>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className={css.modeRow}>
|
|
262
|
+
<span className={css.modeLabel}>{t('toolsModeLabel')}</span>
|
|
263
|
+
<label className={css.modeOption}>
|
|
264
|
+
<input
|
|
265
|
+
type="radio"
|
|
266
|
+
name="mcp-tool-mode"
|
|
267
|
+
checked={mode === 'search'}
|
|
268
|
+
onChange={() => setMode('search')}
|
|
269
|
+
/>
|
|
270
|
+
{t('toolsModeSearch')}
|
|
271
|
+
</label>
|
|
272
|
+
<label className={css.modeOption}>
|
|
273
|
+
<input
|
|
274
|
+
type="radio"
|
|
275
|
+
name="mcp-tool-mode"
|
|
276
|
+
checked={mode === 'full'}
|
|
277
|
+
onChange={() => setMode('full')}
|
|
278
|
+
/>
|
|
279
|
+
{t('toolsModeFull')}
|
|
280
|
+
</label>
|
|
281
|
+
</div>
|
|
282
|
+
<p className={css.modeHint}>
|
|
283
|
+
{toolsError !== null ? toolsError : tools === null ? t('toolsLoading') : mode === 'search' ? t('toolsHintSearch') : t('toolsHintFull')}
|
|
284
|
+
</p>
|
|
285
|
+
|
|
286
|
+
{state.servers.length === 0 ? (
|
|
287
|
+
<p className={css.status}>{t('empty')}</p>
|
|
288
|
+
) : (
|
|
289
|
+
<ul className={css.list}>
|
|
290
|
+
{state.servers.map(server => {
|
|
291
|
+
const isExpanded = expanded.has(server.serverName)
|
|
292
|
+
const isRefreshing = refreshing.has(server.serverName)
|
|
293
|
+
const serverToolList = serverTools(server.serverName)
|
|
294
|
+
return (
|
|
295
|
+
<li key={server.id} className={css.card}>
|
|
296
|
+
<div className={css.row}>
|
|
297
|
+
<div className={css.rowMain}>
|
|
298
|
+
<div className={css.rowTitle}>
|
|
299
|
+
<span className={css.serverName}>{server.serverName}</span>
|
|
300
|
+
<span className={`${css.badge} ${css[server.status.phase]}`}>{t(phaseKey(server.status.phase))}</span>
|
|
301
|
+
{!server.enabled ? <span className={css.muted}>{t('statusStopped')}</span> : null}
|
|
302
|
+
</div>
|
|
303
|
+
<div className={css.rowMeta}>
|
|
304
|
+
<span>{server.transport}</span>
|
|
305
|
+
<span>{t('toolCount')}: {server.status.tools.length}</span>
|
|
306
|
+
<span>{t('envVars')}: {server.env.length}</span>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div className={css.rowActions}>
|
|
310
|
+
<button type="button" disabled={isRefreshing} onClick={() => void toggleEnabled(server)}>
|
|
311
|
+
{server.enabled ? t('disable') : t('enabled')}
|
|
312
|
+
</button>
|
|
313
|
+
<button type="button" disabled={isRefreshing} onClick={() => refreshServer(server.serverName)}>
|
|
314
|
+
{isRefreshing ? t('refreshing') : t('refresh')}
|
|
315
|
+
</button>
|
|
316
|
+
<button type="button" onClick={() => toggleExpand(server.serverName)}>
|
|
317
|
+
{isExpanded ? t('toolsCollapse') : t('toolsExpand')}
|
|
318
|
+
</button>
|
|
319
|
+
<button type="button" onClick={() => beginEdit(server)}>{t('edit')}</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
{isExpanded ? (
|
|
323
|
+
<div className={css.toolPanel}>
|
|
324
|
+
{tools === null ? (
|
|
325
|
+
<p className={css.muted}>{toolsError ?? t('toolsLoading')}</p>
|
|
326
|
+
) : serverToolList.length === 0 ? (
|
|
327
|
+
<p className={css.muted}>{t('toolsEmpty')}</p>
|
|
328
|
+
) : (
|
|
329
|
+
<div className={css.toolGrid}>
|
|
330
|
+
{serverToolList.map(tool => (
|
|
331
|
+
<label key={tool.name} className={css.toolRow} title={tool.name}>
|
|
332
|
+
<input
|
|
333
|
+
type="checkbox"
|
|
334
|
+
checked={tool.enabled}
|
|
335
|
+
onChange={() => toggleTool(tool)}
|
|
336
|
+
/>
|
|
337
|
+
<span className={css.toolName}>{rawOf(tool.name)}</span>
|
|
338
|
+
</label>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
) : null}
|
|
344
|
+
</li>
|
|
345
|
+
)
|
|
346
|
+
})}
|
|
347
|
+
</ul>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
.form {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 14px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
max-width: 760px;
|
|
7
|
+
color: var(--dsw-alias-label-primary);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.titleRow {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
gap: 12px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.title {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font-size: 15px;
|
|
20
|
+
line-height: 22px;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.titleActions {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
button {
|
|
30
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
31
|
+
border-radius: 6px;
|
|
32
|
+
padding: 5px 12px;
|
|
33
|
+
background: transparent;
|
|
34
|
+
color: var(--dsw-alias-label-primary);
|
|
35
|
+
font: inherit;
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
line-height: 20px;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button:hover {
|
|
42
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button:focus-visible {
|
|
46
|
+
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
47
|
+
outline-offset: 1px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button:disabled {
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
opacity: 0.55;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.primary {
|
|
56
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
57
|
+
background: var(--dsw-alias-state-business-primary);
|
|
58
|
+
color: var(--dsw-alias-label-on-accent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.primary:hover {
|
|
62
|
+
background: var(--dsw-alias-state-business-primary-hover);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.danger {
|
|
66
|
+
color: var(--dsw-alias-state-error-primary);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.error {
|
|
70
|
+
margin: 0;
|
|
71
|
+
color: var(--dsw-alias-state-error-primary);
|
|
72
|
+
font-size: 13px;
|
|
73
|
+
line-height: 20px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.field {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 5px;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
line-height: 20px;
|
|
82
|
+
color: var(--dsw-alias-label-secondary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.field input,
|
|
86
|
+
.field select,
|
|
87
|
+
.field textarea {
|
|
88
|
+
width: 100%;
|
|
89
|
+
box-sizing: border-box;
|
|
90
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
padding: 6px 10px;
|
|
93
|
+
outline: none;
|
|
94
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
95
|
+
color: var(--dsw-alias-label-primary);
|
|
96
|
+
font: inherit;
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
resize: vertical;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.field input::placeholder,
|
|
102
|
+
.field textarea::placeholder {
|
|
103
|
+
color: var(--dsw-alias-label-tertiary);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.field input:focus-visible,
|
|
107
|
+
.field select:focus-visible,
|
|
108
|
+
.field textarea:focus-visible {
|
|
109
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
110
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.envBlock {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
118
|
+
border-radius: 10px;
|
|
119
|
+
padding: 10px 12px 12px;
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.envBlock legend {
|
|
124
|
+
padding: 0 4px;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
line-height: 20px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.hint,
|
|
131
|
+
.muted {
|
|
132
|
+
margin: 0;
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
line-height: 18px;
|
|
135
|
+
color: var(--dsw-alias-label-tertiary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.envRow {
|
|
139
|
+
display: grid;
|
|
140
|
+
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.6fr) auto auto;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 8px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.envRow input {
|
|
146
|
+
box-sizing: border-box;
|
|
147
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
padding: 5px 8px;
|
|
150
|
+
outline: none;
|
|
151
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
152
|
+
color: var(--dsw-alias-label-primary);
|
|
153
|
+
font: inherit;
|
|
154
|
+
font-size: 13px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.secretToggle {
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 4px;
|
|
161
|
+
color: var(--dsw-alias-label-secondary);
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
line-height: 18px;
|
|
164
|
+
white-space: nowrap;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.checkRow {
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 8px;
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
line-height: 20px;
|
|
174
|
+
color: var(--dsw-alias-label-secondary);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.testResult {
|
|
179
|
+
margin: 0;
|
|
180
|
+
font-size: 13px;
|
|
181
|
+
line-height: 20px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.testOk {
|
|
185
|
+
color: var(--dsw-alias-state-success-primary);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.testFail {
|
|
189
|
+
color: var(--dsw-alias-state-error-primary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.actions {
|
|
193
|
+
display: flex;
|
|
194
|
+
justify-content: flex-end;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
}
|