Files
dsh-abap-workbench/client.js
T

826 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.__ModuleLoader__.load({
id: 'dsh-abap-workbench',
factory(require) {
const React = require('react')
const h = React.createElement
const { useCallback, useEffect, useState } = React
const NS = 'abap-adt-ui'
const API = '/abap-adt'
const DICTS = {
zh: {
sectionLabel: 'ABAP ADT',
title: 'ABAP ADT 配置',
subtitle: '管理当前工作区的 SAP ADT 目的地(写入 .dsh-abap-adt/destinations.yaml,立即生效)',
readOnlyBadge: '只读预设',
workspace: '工作区',
workspaceHint: '目的地文件写到该工作区的 .dsh-abap-adt/ 目录',
refresh: '刷新',
destinations: '目的地',
empty: '还没有配置目的地。内置 demo 模拟系统始终可用。',
add: '新增目的地',
name: '标识(工具调用用)',
displayName: '显示名(中文)',
displayNameHint: '只用于界面展示;工具调用仍用「标识」',
url: 'URL',
client: '客户端',
language: '语言',
username: '用户名',
passwordEnv: '密码引用',
passwordHint: '只填引用名(如 ADT_DEV_PASSWORD),真值存 DSH 凭据库或环境变量;不要写明文',
strictSSL: '严格 TLS',
profile: '环境',
description: '说明',
defaultDestination: '默认目的地',
save: '保存',
saving: '保存中…',
saved: '已保存',
modeTitle: '运行模式(本工作区)',
modes: { 'read-only': '只读', debug: '调试', change: '更改' },
modeHints: {
'read-only': '只读:写/激活/删除/传输/执行/调试全部关闭,只做调查与补丁建议。',
debug: '调试:允许执行以触发断点,可用 adt_debug 设断点/单步/看变量;写与传输仍关闭。',
change: '更改:按白名单可写(含传输,每次写需给请求号),允许执行与调试器;写前先给用户看 diff。',
},
modeSwitched: '运行模式已切换',
whitelistTitle: '可写白名单(更改模式)',
allowedPackages: '允许编辑的包',
allowedPackagesHint: '逗号分隔的 glob,例如 Z*,$TMP,Y*;$TMP 为本地对象包',
allowedTransports: '允许的传输请求号',
allowedTransportsHint: '逗号分隔的 glob,例如 D01K96*;* 表示不限制(仍要求用户提供请求号)',
whitelistDisabled: '白名单只在「更改」模式生效;先切到更改模式再编辑',
applyWhitelist: '应用白名单',
whitelistSaved: '白名单已保存并写入策略',
policyTitle: '当前生效策略(来自 destinations.yaml 的 policy 段)',
policyNote: '运行模式由本工作区决定(上方切换),以 policy 段写入 destinations.yaml;预设本身只带安全基线(只读)。',
test: '测试连接',
testing: '测试中…',
remove: '删除',
loadError: '读取配置失败',
saveError: '保存失败',
chipPrefix: 'SAP',
chipDemo: 'demo',
chipDest: '个目的地',
chipReadOnly: '只读',
chipError: 'ADT 状态未知',
chipTitle: '切换该工作区的默认 ADT 目的地(作用于本工作区所有会话)',
chipSwitched: '已切换,下次 adt_* 调用生效',
chipSwitchFailed: '切换失败',
chipGateNote: '状态条只在对该工作区配置了 ABAP(存在 .dsh-abap-adt/destinations.yaml)时显示;可在插件配置 workspaces 白名单里进一步限定。',
fileLabel: '配置文件',
notExists: '(尚未创建,保存后生成)',
never: '—',
},
en: {
sectionLabel: 'ABAP ADT',
title: 'ABAP ADT Settings',
subtitle: 'Manage SAP ADT destinations for this workspace (written to .dsh-abap-adt/destinations.yaml, applied immediately)',
readOnlyBadge: 'Read-only preset',
workspace: 'Workspace',
workspaceHint: 'The destinations file is written under .dsh-abap-adt/ in this workspace',
refresh: 'Refresh',
destinations: 'Destinations',
empty: 'No destinations configured yet. The built-in demo mock system is always available.',
add: 'Add destination',
name: 'Identifier (for tool calls)',
displayName: 'Display name',
displayNameHint: 'Shown in the UI only; tool calls still use the identifier',
url: 'URL',
client: 'Client',
language: 'Language',
username: 'User',
passwordEnv: 'Password ref',
passwordHint: 'Use a reference name (e.g. ADT_DEV_PASSWORD); the value lives in the DSH credential store or environment. Never paste a password here.',
strictSSL: 'Strict TLS',
profile: 'Profile',
description: 'Description',
defaultDestination: 'Default destination',
save: 'Save',
saving: 'Saving…',
saved: 'Saved',
modeTitle: 'Run mode (this workspace)',
modes: { 'read-only': 'Read-only', debug: 'Debug', change: 'Change' },
modeHints: {
'read-only': 'Read-only: writes, activation, deletion, transports, execution and the debugger are off; investigate and propose patches only.',
debug: 'Debug: execution is allowed to trigger breakpoints; adt_debug is available; writes and transports stay off.',
change: 'Change: whitelisted packages are writable (transports need the request number on every write), execution and the debugger are allowed; show a diff before writing.',
},
modeSwitched: 'Run mode switched',
whitelistTitle: 'Write whitelists (change mode)',
allowedPackages: 'Editable packages',
allowedPackagesHint: 'Comma-separated globs, e.g. Z*,$TMP,Y*; $TMP is the local-objects package',
allowedTransports: 'Allowed transport numbers',
allowedTransportsHint: 'Comma-separated globs, e.g. D01K96*; * means unrestricted (the request number is still required from the user)',
whitelistDisabled: 'Whitelists only apply in Change mode; switch to Change first',
applyWhitelist: 'Apply whitelists',
whitelistSaved: 'Whitelists saved and written into the policy',
policyTitle: 'Effective policy (from the destinations.yaml policy block)',
policyNote: 'The run mode belongs to this workspace (switch above) and is written as a policy block into destinations.yaml; the preset itself only carries the read-only baseline.',
test: 'Test connection',
testing: 'Testing…',
remove: 'Remove',
policyTitle: 'Permission policy (locked by the preset)',
policyNote: 'The current session preset locks SAP writes (sentinel allowedPackages + transports/execution/debugger off). This page manages destinations only; switch to a writable preset to edit code.',
loadError: 'Failed to load configuration',
saveError: 'Save failed',
chipPrefix: 'SAP',
chipDemo: 'demo',
chipDest: 'dest(s)',
chipReadOnly: 'read-only',
chipError: 'ADT status unknown',
chipTitle: 'Switch this workspace default ADT destination (applies to every session in the workspace)',
chipSwitched: 'Switched — applies to the next adt_* call',
chipSwitchFailed: 'Switch failed',
chipGateNote: 'The status chip only appears where ABAP is configured for the workspace (.dsh-abap-adt/destinations.yaml exists); narrow it further with the plugin config workspaces allowlist.',
fileLabel: 'Config file',
notExists: ' (not created yet — save to create)',
never: '—',
},
}
const C = {
text: 'var(--dsw-alias-label-primary, #1f2328)',
muted: 'var(--dsw-alias-label-secondary, #6b7280)',
border: 'var(--dsw-alias-border-l1, rgba(128,128,128,.28))',
borderStrong: 'var(--dsw-alias-border-l2, rgba(128,128,128,.45))',
surface: 'var(--dsw-alias-bg-layer-1, #ffffff)',
surface2: 'var(--dsw-alias-bg-layer-2, #f5f6f7)',
brand: 'var(--dsw-alias-brand-primary, #0a6ed1)',
ok: 'var(--dsw-alias-state-success-primary, #1a7f37)',
err: 'var(--dsw-alias-state-error-primary, #cf222e)',
warn: 'var(--dsw-alias-state-warn-primary, #9a6700)',
}
const inputStyle = {
boxSizing: 'border-box',
width: '100%',
padding: '6px 8px',
fontSize: 13,
color: C.text,
background: C.surface,
border: `1px solid ${C.border}`,
borderRadius: 6,
outline: 'none',
}
const buttonStyle = (primary) => ({
padding: '6px 12px',
fontSize: 13,
cursor: 'pointer',
borderRadius: 6,
border: `1px solid ${primary ? C.brand : C.borderStrong}`,
background: primary ? C.brand : 'transparent',
color: primary ? '#ffffff' : C.text,
})
const smallButtonStyle = {
padding: '4px 8px',
fontSize: 12,
cursor: 'pointer',
borderRadius: 5,
border: `1px solid ${C.border}`,
background: 'transparent',
color: C.text,
}
async function api(path, init) {
const response = await fetch(`${API}${path}`, {
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
...init,
})
const payload = await response.json().catch(() => ({ ok: false, error: `HTTP ${response.status}` }))
if (!response.ok || payload.ok === false) throw new Error(payload.error || `HTTP ${response.status}`)
return payload
}
function blankDestination() {
return { name: '', label: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' }
}
function Field({ label, hint, children }) {
return h(
'label',
{ style: { display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 } },
h('span', { style: { fontSize: 12, color: C.muted } }, label),
children,
hint ? h('span', { style: { fontSize: 11, color: C.muted, lineHeight: 1.4 } }, hint) : null,
)
}
function Badge({ children, tone }) {
const toneColor =
tone === 'ok' ? C.ok : tone === 'warn' ? C.warn : tone === 'err' ? C.err : tone === 'brand' ? C.brand : C.muted
return h(
'span',
{
style: {
display: 'inline-flex',
alignItems: 'center',
gap: 4,
padding: '2px 8px',
fontSize: 11,
fontWeight: 600,
borderRadius: 999,
border: `1px solid ${toneColor}`,
color: toneColor,
background: 'transparent',
whiteSpace: 'nowrap',
},
},
children,
)
}
function SettingsPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [workspace, setWorkspace] = useState('')
const [workspaces, setWorkspaces] = useState([])
const [file, setFile] = useState(null)
const [defaultDestination, setDefaultDestination] = useState('')
const [destinations, setDestinations] = useState([])
const [policy, setPolicy] = useState({})
const [mode, setMode] = useState('read-only')
const [whitelist, setWhitelist] = useState(null)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState('')
const [messageTone, setMessageTone] = useState('ok')
const [tests, setTests] = useState({})
const [testing, setTesting] = useState({})
const load = useCallback(async (target) => {
setLoading(true)
setError('')
setMessage('')
try {
const query = new URLSearchParams()
if (target) query.set('workspace', target)
const payload = await api(`/state?${query.toString()}`)
setWorkspace(payload.workspace || '')
setWorkspaces(payload.workspaces || [])
setFile(payload.file || null)
setDefaultDestination(payload.defaultDestination || '')
setDestinations(payload.destinations || [])
setPolicy(payload.policy || {})
setMode(payload.mode || 'read-only')
setWhitelist(payload.whitelist || null)
setTests({})
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
load()
}, [load])
const updateDestination = (index, key, value) => {
const row = destinations[index]
// Renaming the identifier keeps the default destination in sync, so a
// rename cannot leave defaultDestination pointing at a gone name.
if (key === 'name' && row && defaultDestination === row.name) setDefaultDestination(value)
setDestinations((rows) => rows.map((item, itemIndex) => (itemIndex === index ? { ...item, [key]: value } : item)))
}
const addDestination = () => {
setDestinations((rows) => [...rows, blankDestination()])
setMessage('')
}
const removeDestination = (index) => {
const removed = destinations[index]
if (removed && removed.name && removed.name === defaultDestination) setDefaultDestination('')
setDestinations((rows) => rows.filter((_, rowIndex) => rowIndex !== index))
setMessage('')
}
const save = async () => {
setSaving(true)
setMessage('')
try {
// Round-trip only the fields this page edits: schema defaults such as
// timeoutMs must not be re-serialized from a display value.
const payloadDestinations = destinations.map((dest) => ({
name: dest.name,
url: dest.url,
description: dest.description,
client: dest.client,
language: dest.language,
username: dest.username,
passwordEnv: dest.passwordEnv,
strictSSL: dest.strictSSL,
profile: dest.profile,
}))
// Display labels are UI-only and keyed by the stable identifier.
const labels = {}
for (const dest of destinations) {
const name = String(dest.name || '').trim()
const label = String(dest.label || '').trim()
if (name && label) labels[name] = label
}
const payload = await api('/save', {
method: 'POST',
body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations, labels }),
})
setMessage(`${DICTS.zh.saved}: ${payload.path}`)
setMessageTone('ok')
await load(workspace)
} catch (err) {
setMessage(`${DICTS.zh.saveError}: ${err.message}`)
setMessageTone('err')
} finally {
setSaving(false)
}
}
const switchMode = async (next) => {
if (next === mode || !workspace) return
setSaving(true)
setMessage('')
try {
await api('/mode', { method: 'POST', body: JSON.stringify({ workspace, mode: next }) })
await load(workspace)
setMessage(`${DICTS.zh.modeSwitched}: ${DICTS.zh.modes[next]}`)
setMessageTone('ok')
} catch (err) {
setMessage(`${DICTS.zh.saveError}: ${err.message}`)
setMessageTone('err')
} finally {
setSaving(false)
}
}
const applyWhitelist = async () => {
if (!workspace || !whitelist) return
setSaving(true)
setMessage('')
try {
await api('/mode', {
method: 'POST',
body: JSON.stringify({
workspace,
mode,
allowedPackages: whitelist.allowedPackages,
allowedTransports: whitelist.allowedTransports,
}),
})
await load(workspace)
setMessage(DICTS.zh.whitelistSaved)
setMessageTone('ok')
} catch (err) {
setMessage(`${DICTS.zh.saveError}: ${err.message}`)
setMessageTone('err')
} finally {
setSaving(false)
}
}
const test = async (dest) => {
const key = dest.name || `#${destinations.indexOf(dest) + 1}`
setTesting((map) => ({ ...map, [key]: true }))
try {
const payload = await api('/ping', {
method: 'POST',
body: JSON.stringify({
url: dest.url,
client: dest.client,
language: dest.language,
username: dest.username,
passwordEnv: dest.passwordEnv,
strictSSL: dest.strictSSL,
}),
})
setTests((map) => ({ ...map, [key]: payload.result }))
} catch (err) {
setTests((map) => ({ ...map, [key]: { ok: false, error: err.message, status: 0, ms: 0 } }))
} finally {
setTesting((map) => ({ ...map, [key]: false }))
}
}
if (loading) {
return h('div', { style: { padding: 24, color: C.muted, fontSize: 13 } }, '…')
}
const header = h(
'div',
{ style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
h('div', { style: { fontSize: 18, fontWeight: 700, color: C.text } }, DICTS.zh.title),
h(Badge, { tone: mode === 'change' ? 'err' : mode === 'debug' ? 'brand' : 'warn' }, DICTS.zh.modes[mode] || mode),
h('div', { style: { flex: 1 } }),
h('button', { style: smallButtonStyle, onClick: () => load(workspace), type: 'button' }, DICTS.zh.refresh),
)
const errorBox = error
? h(
'div',
{
style: {
marginTop: 12,
padding: '10px 12px',
borderRadius: 8,
border: `1px solid ${C.err}`,
color: C.err,
fontSize: 13,
},
},
`${DICTS.zh.loadError}: ${error}`,
)
: null
const workspaceRow = h(
'div',
{ style: { display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12, marginTop: 16 } },
h(
Field,
{ label: DICTS.zh.workspace, hint: DICTS.zh.workspaceHint },
h('input', {
style: inputStyle,
value: workspace,
list: 'abap-adt-workspaces',
onChange: (event) => setWorkspace(event.target.value),
onBlur: () => {
if (workspace && workspace !== file?.path?.replace(/[\\/]\.dsh-abap-adt[\\/].*$/, '')) load(workspace)
},
placeholder: 'D:\\AIagent\\SAP',
}),
),
h(
Field,
{ label: DICTS.zh.fileLabel },
h(
'div',
{ style: { fontSize: 12, color: C.muted, wordBreak: 'break-all', paddingTop: 6 } },
file?.path ? `${file.path}${file.exists ? '' : DICTS.zh.notExists}` : DICTS.zh.never,
),
),
h(
'datalist',
{ id: 'abap-adt-workspaces' },
workspaces.map((item) => h('option', { key: item.id || item.path, value: item.path }, item.title)),
),
)
const destinationCards = destinations.length
? destinations.map((dest, index) => {
const key = dest.name || `#${index + 1}`
const result = tests[key]
return h(
'div',
{
key: index,
style: {
marginTop: 12,
padding: 12,
borderRadius: 10,
border: `1px solid ${C.border}`,
background: C.surface2,
display: 'flex',
flexDirection: 'column',
gap: 10,
},
},
h(
'div',
{ style: { display: 'grid', gridTemplateColumns: '1.2fr 1fr 1.6fr 0.55fr 0.55fr', gap: 10 } },
h(Field, { label: DICTS.zh.displayName, hint: DICTS.zh.displayNameHint }, h('input', { style: inputStyle, value: dest.label || '', onChange: (e) => updateDestination(index, 'label', e.target.value), placeholder: '广东开发' })),
h(Field, { label: DICTS.zh.name }, h('input', { style: inputStyle, value: dest.name || '', onChange: (e) => updateDestination(index, 'name', e.target.value), placeholder: 'dev' })),
h(Field, { label: DICTS.zh.url }, h('input', { style: inputStyle, value: dest.url || '', onChange: (e) => updateDestination(index, 'url', e.target.value), placeholder: 'https://sap.example.com:44300' })),
h(Field, { label: DICTS.zh.client }, h('input', { style: inputStyle, value: dest.client || '', onChange: (e) => updateDestination(index, 'client', e.target.value), placeholder: '100' })),
h(Field, { label: DICTS.zh.language }, h('input', { style: inputStyle, value: dest.language || '', onChange: (e) => updateDestination(index, 'language', e.target.value), placeholder: 'ZH' })),
),
h(
'div',
{ style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 } },
h(Field, { label: DICTS.zh.username }, h('input', { style: inputStyle, value: dest.username || '', onChange: (e) => updateDestination(index, 'username', e.target.value) })),
h(Field, { label: DICTS.zh.passwordEnv, hint: DICTS.zh.passwordHint }, h('input', { style: inputStyle, value: dest.passwordEnv || '', onChange: (e) => updateDestination(index, 'passwordEnv', e.target.value), placeholder: 'ADT_DEV_PASSWORD' })),
h(Field, { label: DICTS.zh.profile }, h('select', { style: inputStyle, value: dest.profile || 'dev', onChange: (e) => updateDestination(index, 'profile', e.target.value) }, ['dev', 'qa', 'prd'].map((value) => h('option', { key: value, value }, value)))),
h(Field, { label: DICTS.zh.description }, h('input', { style: inputStyle, value: dest.description || '', onChange: (e) => updateDestination(index, 'description', e.target.value) })),
),
h(
'div',
{ style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } },
h(
'label',
{ style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: C.muted } },
h('input', { type: 'checkbox', checked: dest.strictSSL !== false, onChange: (e) => updateDestination(index, 'strictSSL', e.target.checked) }),
DICTS.zh.strictSSL,
),
h('div', { style: { flex: 1 } }),
result
? h(
'span',
{ style: { fontSize: 12, color: result.ok ? C.ok : C.err } },
`${result.status || 'ERR'} · ${result.ms}ms${result.error ? ` · ${result.error}` : ''}`,
)
: null,
h('button', { type: 'button', style: smallButtonStyle, onClick: () => test(dest), disabled: testing[key] }, testing[key] ? DICTS.zh.testing : DICTS.zh.test),
h('button', { type: 'button', style: smallButtonStyle, onClick: () => removeDestination(index) }, DICTS.zh.remove),
),
)
})
: h('div', { style: { marginTop: 12, fontSize: 13, color: C.muted } }, DICTS.zh.empty)
const policyRows = Object.entries(policy).length
? Object.entries(policy).map(([key, value]) =>
h(
'div',
{ key, style: { display: 'flex', gap: 10, fontSize: 12, padding: '3px 0' } },
h('span', { style: { minWidth: 210, color: C.muted, fontFamily: 'ui-monospace, monospace' } }, key),
h('span', { style: { color: C.text, wordBreak: 'break-all' } }, Array.isArray(value) ? value.join(', ') || '(empty)' : String(value)),
),
)
: h('div', { style: { fontSize: 12, color: C.muted } }, '—')
return h(
'div',
{ style: { padding: '20px 24px 40px', color: C.text, maxWidth: 1080 } },
header,
h('div', { style: { marginTop: 6, fontSize: 12.5, color: C.muted } }, DICTS.zh.subtitle),
errorBox,
workspaceRow,
h('div', { style: { marginTop: 6, fontSize: 11.5, color: C.muted, lineHeight: 1.5 } }, DICTS.zh.chipGateNote),
h(
'div',
{
style: {
marginTop: 16,
padding: 12,
borderRadius: 10,
border: `1px solid ${C.border}`,
background: C.surface2,
},
},
h(
'div',
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
h('span', { style: { fontSize: 13, fontWeight: 650 } }, DICTS.zh.modeTitle),
['read-only', 'debug', 'change'].map((value) =>
h(
'button',
{
key: value,
type: 'button',
disabled: saving || !workspace,
onClick: () => switchMode(value),
style: { ...buttonStyle(value === mode), padding: '5px 14px' },
},
DICTS.zh.modes[value],
),
),
h('div', { style: { flex: 1 } }),
h(Badge, { tone: mode === 'change' ? 'err' : mode === 'debug' ? 'brand' : 'warn' }, DICTS.zh.modes[mode] || mode),
),
h('div', { style: { marginTop: 8, fontSize: 12, color: C.muted, lineHeight: 1.5 } }, DICTS.zh.modeHints[mode] || ''),
whitelist
? h(
'div',
{ style: { marginTop: 12, paddingTop: 12, borderTop: `1px solid ${C.border}` } },
h('div', { style: { fontSize: 12.5, fontWeight: 600, marginBottom: 8 } }, DICTS.zh.whitelistTitle),
h(
'div',
{ style: { display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 12, alignItems: 'end' } },
h(
Field,
{
label: DICTS.zh.allowedPackages,
hint: whitelist.editable ? DICTS.zh.allowedPackagesHint : DICTS.zh.whitelistDisabled,
},
h('input', {
style: inputStyle,
value: whitelist.allowedPackages || '',
disabled: !whitelist.editable || saving,
onChange: (event) => setWhitelist((value) => ({ ...value, allowedPackages: event.target.value })),
}),
),
h(
Field,
{
label: DICTS.zh.allowedTransports,
hint: whitelist.editable ? DICTS.zh.allowedTransportsHint : DICTS.zh.whitelistDisabled,
},
h('input', {
style: inputStyle,
value: whitelist.allowedTransports || '',
disabled: !whitelist.editable || saving,
onChange: (event) => setWhitelist((value) => ({ ...value, allowedTransports: event.target.value })),
}),
),
h(
'button',
{
type: 'button',
style: buttonStyle(true),
disabled: !whitelist.editable || saving,
onClick: applyWhitelist,
},
DICTS.zh.applyWhitelist,
),
),
)
: null,
),
h(
'div',
{ style: { marginTop: 24, display: 'flex', alignItems: 'center', gap: 10 } },
h('div', { style: { fontSize: 15, fontWeight: 650 } }, DICTS.zh.destinations),
h('div', { style: { flex: 1 } }),
h('button', { type: 'button', style: smallButtonStyle, onClick: addDestination }, `+ ${DICTS.zh.add}`),
),
destinationCards,
h(
'div',
{ style: { marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
h(
Field,
{ label: DICTS.zh.defaultDestination },
h(
'select',
{ style: inputStyle, value: defaultDestination, onChange: (e) => setDefaultDestination(e.target.value) },
h('option', { value: '' }, 'demo'),
destinations
.filter((dest) => dest.name)
.map((dest) =>
h('option', { key: dest.name, value: dest.name }, dest.label ? `${dest.label}(${dest.name})` : dest.name),
),
),
),
),
h(
'div',
{ style: { marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 } },
h('button', { type: 'button', style: buttonStyle(true), onClick: save, disabled: saving || !workspace }, saving ? DICTS.zh.saving : DICTS.zh.save),
message ? h('span', { style: { fontSize: 12, color: messageTone === 'err' ? C.err : C.ok, wordBreak: 'break-all' } }, message) : null,
),
h(
'div',
{ style: { marginTop: 28, padding: 14, borderRadius: 10, border: `1px solid ${C.border}`, background: C.surface2 } },
h('div', { style: { fontSize: 14, fontWeight: 650 } }, DICTS.zh.policyTitle),
h('div', { style: { marginTop: 6, fontSize: 12.5, color: C.muted, lineHeight: 1.5 } }, DICTS.zh.policyNote),
h('div', { style: { marginTop: 10 } }, policyRows),
),
)
}
function StatusChip(props) {
const sessionId = props?.sessionId
const [state, setState] = useState(null)
const [failed, setFailed] = useState(false)
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
useEffect(() => {
let alive = true
const query = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : ''
api(`/state${query}`)
.then((payload) => {
if (alive) setState(payload)
})
.catch(() => {
if (alive) setFailed(true)
})
return () => {
alive = false
}
}, [sessionId])
if (failed) return null
if (!state) return null
// Workspace gate: only workspaces that actually have an ABAP
// destinations file (or are explicitly allowlisted on the Host) get the
// chip — unrelated workspaces and modes stay clean.
const enabled = state.chip ? state.chip.enabled : Boolean(state.file?.exists)
if (!enabled) return null
const destinations = state.destinations || []
const names = destinations.map((dest) => dest.name).filter(Boolean)
const selected = state.defaultDestination || 'demo'
const options = [
{ name: 'demo', label: DICTS.zh.chipDemo },
...destinations
.filter((dest) => dest.name && dest.name !== 'demo')
.map((dest) => ({ name: dest.name, label: dest.label || dest.name })),
]
const modeText = DICTS.zh.modes[state.mode]
const modeBadge = modeText
? { text: modeText, tone: state.mode === 'change' ? 'err' : state.mode === 'debug' ? 'brand' : 'warn' }
: null
const switchDefault = async (value) => {
if (value === selected) return
setBusy(true)
setMessage('')
try {
await api('/default', {
method: 'POST',
body: JSON.stringify({ workspace: state.workspace, defaultDestination: value }),
})
setState((previous) => (previous ? { ...previous, defaultDestination: value } : previous))
setMessage(DICTS.zh.chipSwitched)
} catch (err) {
setMessage(`${DICTS.zh.chipSwitchFailed}: ${err.message}`)
} finally {
setBusy(false)
}
}
return h(
'span',
{
title: `${DICTS.zh.chipTitle}\n${state.file?.path || ''}`,
style: {
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '3px 8px',
borderRadius: 999,
border: `1px solid ${C.border}`,
background: 'transparent',
fontSize: 11.5,
color: C.muted,
whiteSpace: 'nowrap',
userSelect: 'none',
},
},
h('span', { style: { width: 6, height: 6, borderRadius: '50%', background: names.length ? C.ok : C.warn } }),
h('span', { style: { fontSize: 11.5, color: C.muted } }, DICTS.zh.chipPrefix),
h(
'select',
{
value: selected,
disabled: busy,
onChange: (event) => switchDefault(event.target.value),
style: {
appearance: 'none',
border: 'none',
background: 'transparent',
color: C.text,
fontSize: 11.5,
fontWeight: 600,
padding: '0 2px',
cursor: busy ? 'progress' : 'pointer',
maxWidth: 170,
},
},
options.map((option) =>
h(
'option',
{ key: option.name, value: option.name },
option.label && option.label !== option.name ? `${option.label}(${option.name})` : option.name,
),
),
),
names.length ? h('span', { style: { fontSize: 11, color: C.muted } }, `${names.length} ${DICTS.zh.chipDest}`) : null,
modeBadge ? h(Badge, { tone: modeBadge.tone }, modeBadge.text) : null,
message ? h('span', { style: { fontSize: 10.5, color: C.muted, maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis' } }, message) : null,
)
}
return {
inject: ['slots'],
apply(ctx) {
let t = (key) => DICTS.zh[key] ?? key
try {
if (ctx.locale) {
const disposeLocale = ctx.locale.register(NS, { en: DICTS.en, zh: DICTS.zh })
ctx.effect?.(() => disposeLocale)
const bound = ctx.locale.bind(NS)
t = (key, params) => bound(key, params)
}
} catch (err) {
// Locale is optional: fall back to the shipped Chinese strings.
console.warn('abap-adt-ui: locale registration failed', err)
}
ctx.slots.inject('settings.section', () =>
ctx.slots.register(
{
name: 'settings.section',
id: 'abap-adt',
order: 30,
label: () => t('sectionLabel'),
},
SettingsPage,
),
)
ctx.slots.inject('conversation.input.left', () =>
ctx.slots.register(
{
name: 'conversation.input.left',
id: 'abap-adt-status',
order: 40,
label: () => t('chipPrefix'),
},
StatusChip,
),
)
},
}
},
})