@dsh-plus/secret-env 0.1.0 → 0.1.2
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/lib/client.js +1222 -338
- package/lib/index.d.ts +40 -8
- package/lib/index.js +211 -57
- package/package.json +8 -4
- package/src/api.ts +24 -1
- package/src/client/api.ts +18 -0
- package/src/client/client.ts +59 -14
- package/src/client/command.ts +62 -0
- package/src/client/common.ts +14 -1
- package/src/client/i18n.ts +67 -33
- package/src/client/menu-core.ts +103 -0
- package/src/client/menu.tsx +301 -0
- package/src/client/panel-bus.ts +23 -0
- package/src/client/panel-host.tsx +63 -0
- package/src/client/panel.tsx +385 -0
- package/src/client/section.tsx +194 -38
- package/src/client/styles.ts +25 -14
- package/src/config.ts +4 -0
- package/src/contributors.ts +115 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +101 -0
- package/src/names.ts +3 -3
- package/src/service.ts +107 -94
- package/src/client/composer.tsx +0 -260
package/src/client/composer.tsx
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话注入控件(conversation.input.right 官方插槽,session 作用域):
|
|
3
|
-
* composer 工具行的钥匙胶囊,弹出本会话的会话级密钥面板(列表 + 增删)。
|
|
4
|
-
* 会话 id 由插槽 inject 工厂直接供给(框架解析的严格 session 作用域)。
|
|
5
|
-
* 窄屏(≤767px)popover 转底部抽屉;桌面点外部或 Esc 关闭。
|
|
6
|
-
* @module secret-env/client/composer
|
|
7
|
-
*/
|
|
8
|
-
import { type ReactElement, useEffect, useRef, useState } from 'react'
|
|
9
|
-
|
|
10
|
-
import { fetchSecrets, type SessionWireEntry, setSession, unsetSession } from './api.ts'
|
|
11
|
-
import { copyText, errorText } from './common.ts'
|
|
12
|
-
|
|
13
|
-
export interface ComposerSecretProps {
|
|
14
|
-
/** 插槽 inject 工厂注入的当前会话 id(严格 session 作用域,必存在)。 */
|
|
15
|
-
sessionId: string
|
|
16
|
-
t(key: string): string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface FormState {
|
|
20
|
-
name: string
|
|
21
|
-
value: string
|
|
22
|
-
description: string
|
|
23
|
-
once: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const EMPTY_FORM: FormState = { name: '', value: '', description: '', once: false }
|
|
27
|
-
|
|
28
|
-
function SessionRow(props: {
|
|
29
|
-
t: ComposerSecretProps['t']
|
|
30
|
-
sessionId: string
|
|
31
|
-
entry: SessionWireEntry
|
|
32
|
-
onDeleted(): void
|
|
33
|
-
onError(text: string): void
|
|
34
|
-
}): ReactElement {
|
|
35
|
-
const { t, entry } = props
|
|
36
|
-
const [copied, setCopied] = useState(false)
|
|
37
|
-
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
38
|
-
|
|
39
|
-
useEffect(
|
|
40
|
-
() => () => {
|
|
41
|
-
if (timer.current !== null) clearTimeout(timer.current)
|
|
42
|
-
},
|
|
43
|
-
[],
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div className="dse-row">
|
|
48
|
-
<div className="dse-rowMain">
|
|
49
|
-
<div className="dse-env">
|
|
50
|
-
<span className="dse-envName">${entry.envName}</span>
|
|
51
|
-
<button
|
|
52
|
-
type="button"
|
|
53
|
-
className="dse-iconBtn"
|
|
54
|
-
title={t('copy')}
|
|
55
|
-
aria-label={t('copy')}
|
|
56
|
-
onClick={() => {
|
|
57
|
-
void copyText(`$${entry.envName}`).then((ok) => {
|
|
58
|
-
if (!ok) return
|
|
59
|
-
setCopied(true)
|
|
60
|
-
if (timer.current !== null) clearTimeout(timer.current)
|
|
61
|
-
timer.current = setTimeout(() => setCopied(false), 1500)
|
|
62
|
-
})
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
{copied ? '✓' : '⧉'}
|
|
66
|
-
</button>
|
|
67
|
-
</div>
|
|
68
|
-
{entry.description !== '' ? <span className="dse-note">{entry.description}</span> : null}
|
|
69
|
-
</div>
|
|
70
|
-
<div className="dse-badges">
|
|
71
|
-
{entry.once ? <span className="dse-badge">{t('scopeOnce')}</span> : null}
|
|
72
|
-
<button
|
|
73
|
-
type="button"
|
|
74
|
-
className="dse-iconBtn dse-delBtn"
|
|
75
|
-
title={t('delete')}
|
|
76
|
-
aria-label={t('delete')}
|
|
77
|
-
onClick={() => {
|
|
78
|
-
unsetSession(props.sessionId, entry.name)
|
|
79
|
-
.then(() => props.onDeleted())
|
|
80
|
-
.catch((error: unknown) => props.onError(errorText(t, error)))
|
|
81
|
-
}}
|
|
82
|
-
>
|
|
83
|
-
✕
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function ComposerSecret(props: ComposerSecretProps): ReactElement {
|
|
91
|
-
const { sessionId, t } = props
|
|
92
|
-
const [open, setOpen] = useState(false)
|
|
93
|
-
const [items, setItems] = useState<SessionWireEntry[] | null>(null)
|
|
94
|
-
const [form, setForm] = useState<FormState>(EMPTY_FORM)
|
|
95
|
-
const [saving, setSaving] = useState(false)
|
|
96
|
-
const [status, setStatus] = useState<string | null>(null)
|
|
97
|
-
const wrapRef = useRef<HTMLSpanElement | null>(null)
|
|
98
|
-
|
|
99
|
-
const load = (): void => {
|
|
100
|
-
fetchSecrets(sessionId)
|
|
101
|
-
.then((list) => setItems(list.session))
|
|
102
|
-
.catch(() => setStatus(errorText(t, new Error('internal'))))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 打开面板与会话切换时拉取;闭合计时清零。
|
|
106
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: load 为闭包稳定函数,仅需随 open/sessionId 触发
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (!open) return
|
|
109
|
-
load()
|
|
110
|
-
}, [open, sessionId])
|
|
111
|
-
|
|
112
|
-
// 桌面:点外部 / Esc 关闭。
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (!open) return
|
|
115
|
-
const onDown = (event: MouseEvent): void => {
|
|
116
|
-
if (wrapRef.current !== null && !wrapRef.current.contains(event.target as Node)) {
|
|
117
|
-
setOpen(false)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const onKey = (event: KeyboardEvent): void => {
|
|
121
|
-
if (event.key === 'Escape') setOpen(false)
|
|
122
|
-
}
|
|
123
|
-
document.addEventListener('mousedown', onDown)
|
|
124
|
-
document.addEventListener('keydown', onKey)
|
|
125
|
-
return () => {
|
|
126
|
-
document.removeEventListener('mousedown', onDown)
|
|
127
|
-
document.removeEventListener('keydown', onKey)
|
|
128
|
-
}
|
|
129
|
-
}, [open])
|
|
130
|
-
|
|
131
|
-
const onSave = (): void => {
|
|
132
|
-
setSaving(true)
|
|
133
|
-
setStatus(null)
|
|
134
|
-
setSession(sessionId, form.name, form.value, form.description, form.once)
|
|
135
|
-
.then(() => {
|
|
136
|
-
setForm(EMPTY_FORM)
|
|
137
|
-
load()
|
|
138
|
-
})
|
|
139
|
-
.catch((error: unknown) => setStatus(errorText(t, error)))
|
|
140
|
-
.finally(() => setSaving(false))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const count = items?.length ?? 0
|
|
144
|
-
const canSave = form.name.trim() !== '' && form.value !== '' && !saving
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<span className="dse-wrap" ref={wrapRef}>
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
className="dse-chip"
|
|
151
|
-
aria-expanded={open}
|
|
152
|
-
aria-label={t('sessionSecrets')}
|
|
153
|
-
title={t('sessionSecrets')}
|
|
154
|
-
onClick={() => setOpen(!open)}
|
|
155
|
-
>
|
|
156
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
157
|
-
<path
|
|
158
|
-
d="M14.5 2a7.5 7.5 0 0 0-7.36 9.04L2 20.19V22h1.81l1.5-1.5v-1.81h1.81v-1.81h1.81l1.22-1.22A7.5 7.5 0 1 0 14.5 2Zm2 5.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z"
|
|
159
|
-
fill="currentColor"
|
|
160
|
-
/>
|
|
161
|
-
</svg>
|
|
162
|
-
{count > 0 ? <span className="dse-count">{count}</span> : null}
|
|
163
|
-
</button>
|
|
164
|
-
{open ? (
|
|
165
|
-
<div className="dse-pop" role="dialog" aria-label={t('sessionSecrets')}>
|
|
166
|
-
<div className="dse-popHead">
|
|
167
|
-
<p className="dse-popTitle">{t('sessionSecrets')}</p>
|
|
168
|
-
<button
|
|
169
|
-
type="button"
|
|
170
|
-
className="dse-iconBtn"
|
|
171
|
-
title={t('refresh')}
|
|
172
|
-
aria-label={t('refresh')}
|
|
173
|
-
onClick={load}
|
|
174
|
-
>
|
|
175
|
-
⟳
|
|
176
|
-
</button>
|
|
177
|
-
<button
|
|
178
|
-
type="button"
|
|
179
|
-
className="dse-iconBtn"
|
|
180
|
-
title={t('close')}
|
|
181
|
-
aria-label={t('close')}
|
|
182
|
-
onClick={() => setOpen(false)}
|
|
183
|
-
>
|
|
184
|
-
✕
|
|
185
|
-
</button>
|
|
186
|
-
</div>
|
|
187
|
-
<p className="dse-popHint">{t('sessionHint')}</p>
|
|
188
|
-
<div className="dse-popList">
|
|
189
|
-
{items === null || items.length === 0 ? (
|
|
190
|
-
<p className="dse-empty">{t('sessionEmpty')}</p>
|
|
191
|
-
) : (
|
|
192
|
-
items.map((entry) => (
|
|
193
|
-
<SessionRow
|
|
194
|
-
key={entry.name}
|
|
195
|
-
t={t}
|
|
196
|
-
sessionId={sessionId}
|
|
197
|
-
entry={entry}
|
|
198
|
-
onDeleted={load}
|
|
199
|
-
onError={(text) => setStatus(text === '' ? null : text)}
|
|
200
|
-
/>
|
|
201
|
-
))
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
<div className="dse-popForm">
|
|
205
|
-
<div className="dse-field">
|
|
206
|
-
<div className="dse-head">
|
|
207
|
-
<label className="dse-label" htmlFor="dse-s-name">
|
|
208
|
-
{t('addSession')}
|
|
209
|
-
</label>
|
|
210
|
-
</div>
|
|
211
|
-
<input
|
|
212
|
-
id="dse-s-name"
|
|
213
|
-
className="dse-input"
|
|
214
|
-
placeholder="API_TOKEN"
|
|
215
|
-
value={form.name}
|
|
216
|
-
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
|
217
|
-
/>
|
|
218
|
-
<input
|
|
219
|
-
className="dse-input"
|
|
220
|
-
type="password"
|
|
221
|
-
autoComplete="off"
|
|
222
|
-
placeholder={t('valueLabel')}
|
|
223
|
-
value={form.value}
|
|
224
|
-
onChange={(event) => setForm({ ...form, value: event.target.value })}
|
|
225
|
-
/>
|
|
226
|
-
<input
|
|
227
|
-
className="dse-input"
|
|
228
|
-
placeholder={t('descLabel')}
|
|
229
|
-
value={form.description}
|
|
230
|
-
onChange={(event) => setForm({ ...form, description: event.target.value })}
|
|
231
|
-
/>
|
|
232
|
-
<div className="dse-checkRow">
|
|
233
|
-
<input
|
|
234
|
-
id="dse-s-once"
|
|
235
|
-
type="checkbox"
|
|
236
|
-
role="switch"
|
|
237
|
-
aria-checked={form.once}
|
|
238
|
-
checked={form.once}
|
|
239
|
-
onChange={(event) => setForm({ ...form, once: event.target.checked })}
|
|
240
|
-
/>
|
|
241
|
-
<label htmlFor="dse-s-once">{t('onceLabel')}</label>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
<div className="dse-foot">
|
|
245
|
-
{status !== null ? <p className="dse-status dse-statusError">{status}</p> : null}
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
className="dse-btn dse-btnPrimary"
|
|
249
|
-
disabled={!canSave}
|
|
250
|
-
onClick={onSave}
|
|
251
|
-
>
|
|
252
|
-
{saving ? t('saving') : t('save')}
|
|
253
|
-
</button>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
) : null}
|
|
258
|
-
</span>
|
|
259
|
-
)
|
|
260
|
-
}
|