Files

831 lines
35 KiB
JavaScript
Raw Permalink Normal View History

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, execution and the debugger are off; search, read source, READ transport requests/versions and preview data.',
debug: 'Debug: read-only plus execution to trigger breakpoints and adt_debug; writes and transport changes 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 workspacePaths = workspaces.map((item) => item.path).filter(Boolean)
const workspaceOptions =
workspace && !workspacePaths.includes(workspace) ? [workspace, ...workspacePaths] : workspacePaths
const workspaceTitles = Object.fromEntries(workspaces.map((item) => [item.path, item.title]))
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(
'select',
{
style: inputStyle,
value: workspace,
onChange: (event) => {
const next = event.target.value
setWorkspace(next)
if (next) load(next)
},
},
workspaceOptions.map((path) => {
const title = workspaceTitles[path]
return h('option', { key: path, value: path }, title && title !== path ? title + ' — ' + path : path)
}),
),
),
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,
),
),
)
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,
),
)
},
}
},
})