dsh-plugin-upgrader 1.0.0 → 1.0.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/CHANGELOG.md +28 -0
- package/README.en.md +52 -21
- package/README.md +57 -20
- package/lib/client.js +959 -0
- package/lib/host/bundle.d.ts +52 -0
- package/lib/host/bundle.d.ts.map +1 -0
- package/lib/host/bundle.js +73 -0
- package/lib/host/bundle.js.map +1 -0
- package/lib/host/http.d.ts +46 -0
- package/lib/host/http.d.ts.map +1 -0
- package/lib/host/http.js +104 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/install.d.ts +71 -0
- package/lib/host/install.d.ts.map +1 -0
- package/lib/host/install.js +161 -0
- package/lib/host/install.js.map +1 -0
- package/lib/host/manager.d.ts +151 -0
- package/lib/host/manager.d.ts.map +1 -0
- package/lib/host/manager.js +28 -0
- package/lib/host/manager.js.map +1 -0
- package/lib/host/overview.d.ts +52 -0
- package/lib/host/overview.d.ts.map +1 -0
- package/lib/host/overview.js +109 -0
- package/lib/host/overview.js.map +1 -0
- package/lib/host/registry.d.ts +116 -0
- package/lib/host/registry.d.ts.map +1 -0
- package/lib/host/registry.js +177 -0
- package/lib/host/registry.js.map +1 -0
- package/lib/host/repo.d.ts +62 -0
- package/lib/host/repo.d.ts.map +1 -0
- package/lib/host/repo.js +102 -0
- package/lib/host/repo.js.map +1 -0
- package/lib/host/version.d.ts +67 -0
- package/lib/host/version.d.ts.map +1 -0
- package/lib/host/version.js +114 -0
- package/lib/host/version.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +254 -0
- package/lib/index.js.map +1 -0
- package/package.json +32 -14
- package/screenshots/history.png +0 -0
- package/screenshots/selection.png +0 -0
- package/screenshots.json +0 -4
- package/src/client.js +0 -1136
- package/src/index.js +0 -275
- package/src/lib/bundle.js +0 -64
- package/src/lib/http.js +0 -93
- package/src/lib/install.js +0 -163
- package/src/lib/overview.js +0 -108
- package/src/lib/registry.js +0 -188
- package/src/lib/repo.js +0 -91
- package/src/lib/version.js +0 -107
package/src/client.js
DELETED
|
@@ -1,1136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser half of the plugin-upgrader bundle.
|
|
3
|
-
*
|
|
4
|
-
* Registers one tab on the Plugins page, beside the page's own Installed tab.
|
|
5
|
-
* The tab lists every profile-managed plugin with its installed version, the
|
|
6
|
-
* newest published version, and the full published version history, so a
|
|
7
|
-
* person can upgrade one plugin, upgrade everything at once, or switch a
|
|
8
|
-
* plugin back to an older version.
|
|
9
|
-
*
|
|
10
|
-
* All data and every package change go through this bundle's own host routes
|
|
11
|
-
* (`/api/plugin-upgrader/…`): the browser cannot read the profile's dependency
|
|
12
|
-
* list, and a private registry would be unreachable from a page anyway.
|
|
13
|
-
*
|
|
14
|
-
* @module dsh-plugin-upgrader/client
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
window.__ModuleLoader__.load({
|
|
18
|
-
id: 'dsh-plugin-upgrader',
|
|
19
|
-
factory(require) {
|
|
20
|
-
const React = require('react')
|
|
21
|
-
const h = React.createElement
|
|
22
|
-
|
|
23
|
-
/** Locale namespace owned by this bundle. */
|
|
24
|
-
const NS = 'plugin-upgrader'
|
|
25
|
-
/** Host route prefix registered by `index.js`. */
|
|
26
|
-
const API = '/api/plugin-upgrader'
|
|
27
|
-
/** How often a running batch job is polled. */
|
|
28
|
-
const POLL_MS = 800
|
|
29
|
-
|
|
30
|
-
// -----------------------------------------------------------------------
|
|
31
|
-
// Copy
|
|
32
|
-
// -----------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
const ZH = {
|
|
35
|
-
tab: '版本',
|
|
36
|
-
title: '插件版本管理',
|
|
37
|
-
subtitle: '一键升级,或切换到任意历史版本。',
|
|
38
|
-
refresh: '检查更新',
|
|
39
|
-
refreshing: '检查中…',
|
|
40
|
-
upgradeAll: '一键升级',
|
|
41
|
-
upgradeSelected: '升级选中',
|
|
42
|
-
selectAll: '全选可升级',
|
|
43
|
-
clearSelection: '清除选择',
|
|
44
|
-
selected: (n) => `已选 ${n} 项`,
|
|
45
|
-
selectRow: '选中这个插件',
|
|
46
|
-
nothingSelected: '先勾选要升级的插件',
|
|
47
|
-
installed: '已安装',
|
|
48
|
-
upgradable: '可升级',
|
|
49
|
-
upToDate: '全部已是最新',
|
|
50
|
-
upgrade: '升级',
|
|
51
|
-
upgrading: '升级中…',
|
|
52
|
-
history: '历史版本',
|
|
53
|
-
hideHistory: '收起历史',
|
|
54
|
-
github: 'GitHub 主页',
|
|
55
|
-
githubVersion: '在 GitHub 上打开这个版本的发布页',
|
|
56
|
-
githubReleases: '在 GitHub 上打开发布列表',
|
|
57
|
-
noRepo: '这个包没有登记 GitHub 仓库',
|
|
58
|
-
switchTo: '切换',
|
|
59
|
-
current: '当前',
|
|
60
|
-
latest: '最新',
|
|
61
|
-
older: '降级',
|
|
62
|
-
back: '返回最新',
|
|
63
|
-
empty: '还没有可管理的插件',
|
|
64
|
-
emptyHint: '这个标签页列出随配置安装(profile 依赖)的插件;内置插件不在此列。',
|
|
65
|
-
search: '搜索插件',
|
|
66
|
-
noMatch: '没有匹配的插件',
|
|
67
|
-
loading: '正在读取已安装插件…',
|
|
68
|
-
loadFailed: '读取失败',
|
|
69
|
-
retry: '重试',
|
|
70
|
-
openPage: '在已安装中查看',
|
|
71
|
-
noHistory: '没有可用的版本记录',
|
|
72
|
-
noHistoryWhy: '这个包不是从 npm 注册表安装的,没有发布历史。',
|
|
73
|
-
versions: (n) => `${n} 个版本`,
|
|
74
|
-
showingNewest: (n) => `列出最新 ${n} 个`,
|
|
75
|
-
progress: (done, total) => `正在处理 ${done}/${total}`,
|
|
76
|
-
cancel: '取消',
|
|
77
|
-
cancelling: '正在取消…',
|
|
78
|
-
restart: '需要重启 DSH 才会生效。',
|
|
79
|
-
restartHint: '已保存的改动在下次启动时生效。',
|
|
80
|
-
done: '完成',
|
|
81
|
-
failed: '失败',
|
|
82
|
-
failedSome: (n) => `${n} 个失败`,
|
|
83
|
-
jobDone: '批量升级完成',
|
|
84
|
-
jobCancelled: '已取消',
|
|
85
|
-
busyAdopted: '已有一批升级正在进行,已接着显示它的进度。',
|
|
86
|
-
log: '运行日志',
|
|
87
|
-
showLog: '查看日志',
|
|
88
|
-
hideLog: '收起日志',
|
|
89
|
-
buildBlocked: '这个包有未批准的构建脚本',
|
|
90
|
-
approve: '批准并重启',
|
|
91
|
-
registry: '注册表',
|
|
92
|
-
generated: '数据更新于',
|
|
93
|
-
sourcePath: '本地路径安装,无法检查版本',
|
|
94
|
-
sourceGit: 'Git 安装,无法检查版本',
|
|
95
|
-
unknownVersion: '版本未知',
|
|
96
|
-
dialogHint: '也可以逐个打开安装对话框完成同样的升级。',
|
|
97
|
-
danger: '降级会回到旧版本,旧版本可能含有已修复的问题。',
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const EN = {
|
|
101
|
-
tab: 'Versions',
|
|
102
|
-
title: 'Plugin versions',
|
|
103
|
-
subtitle: 'Upgrade everything at once, or switch to any published version.',
|
|
104
|
-
refresh: 'Check for updates',
|
|
105
|
-
refreshing: 'Checking…',
|
|
106
|
-
upgradeAll: 'Upgrade all',
|
|
107
|
-
upgradeSelected: 'Upgrade selected',
|
|
108
|
-
selectAll: 'Select all upgradable',
|
|
109
|
-
clearSelection: 'Clear selection',
|
|
110
|
-
selected: (n) => `${n} selected`,
|
|
111
|
-
selectRow: 'Select this plugin',
|
|
112
|
-
nothingSelected: 'Pick the plugins you want to upgrade first.',
|
|
113
|
-
installed: 'installed',
|
|
114
|
-
upgradable: 'upgradable',
|
|
115
|
-
upToDate: 'Everything is up to date',
|
|
116
|
-
upgrade: 'Upgrade',
|
|
117
|
-
upgrading: 'Upgrading…',
|
|
118
|
-
history: 'Version history',
|
|
119
|
-
hideHistory: 'Hide history',
|
|
120
|
-
github: 'GitHub page',
|
|
121
|
-
githubVersion: 'Open this version on GitHub',
|
|
122
|
-
githubReleases: 'Open the releases list on GitHub',
|
|
123
|
-
noRepo: 'This package lists no GitHub repository',
|
|
124
|
-
switchTo: 'Switch',
|
|
125
|
-
current: 'installed',
|
|
126
|
-
latest: 'latest',
|
|
127
|
-
older: 'downgrade',
|
|
128
|
-
back: 'Back to latest',
|
|
129
|
-
empty: 'No manageable plugins yet.',
|
|
130
|
-
emptyHint: 'This tab lists plugins installed as profile dependencies; built-in ones are not listed.',
|
|
131
|
-
search: 'Search plugins',
|
|
132
|
-
noMatch: 'No plugin matches that search.',
|
|
133
|
-
loading: 'Reading installed plugins…',
|
|
134
|
-
loadFailed: 'Could not read the plugin list',
|
|
135
|
-
retry: 'Retry',
|
|
136
|
-
openPage: 'Open in Installed',
|
|
137
|
-
noHistory: 'No published versions found',
|
|
138
|
-
noHistoryWhy: 'This package was not installed from an npm registry, so it has no published history.',
|
|
139
|
-
versions: (n) => `${n} versions`,
|
|
140
|
-
showingNewest: (n) => `showing newest ${n}`,
|
|
141
|
-
progress: (done, total) => `Working on ${done}/${total}`,
|
|
142
|
-
cancel: 'Cancel',
|
|
143
|
-
cancelling: 'Cancelling…',
|
|
144
|
-
restart: 'Restart DSH to finish.',
|
|
145
|
-
restartHint: 'Saved changes take effect on the next start.',
|
|
146
|
-
done: 'done',
|
|
147
|
-
failed: 'failed',
|
|
148
|
-
failedSome: (n) => `${n} failed`,
|
|
149
|
-
jobDone: 'Batch upgrade finished',
|
|
150
|
-
jobCancelled: 'Cancelled',
|
|
151
|
-
busyAdopted: 'A batch is already running; showing its progress instead.',
|
|
152
|
-
log: 'Run log',
|
|
153
|
-
showLog: 'Show log',
|
|
154
|
-
hideLog: 'Hide log',
|
|
155
|
-
buildBlocked: 'This package has unapproved build scripts',
|
|
156
|
-
approve: 'Approve and retry',
|
|
157
|
-
registry: 'Registry',
|
|
158
|
-
generated: 'Updated',
|
|
159
|
-
sourcePath: 'Installed from a local path, so no versions are published',
|
|
160
|
-
sourceGit: 'Installed from git, so no versions are published',
|
|
161
|
-
unknownVersion: 'version unknown',
|
|
162
|
-
dialogHint: 'You can also upgrade one at a time through the install dialog.',
|
|
163
|
-
danger: 'A downgrade returns to an older build that may contain fixed bugs.',
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// -----------------------------------------------------------------------
|
|
167
|
-
// Helpers
|
|
168
|
-
// -----------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Best-effort copy lookup that survives an unregistered locale namespace.
|
|
172
|
-
* @param {((key: string, ...args: any[]) => string) | undefined} bound - the locale service translator.
|
|
173
|
-
* @param {() => string} active - current locale id.
|
|
174
|
-
* @returns {(key: string, ...args: any[]) => string} a translator.
|
|
175
|
-
*/
|
|
176
|
-
function makeT(bound, active) {
|
|
177
|
-
return (key, ...args) => {
|
|
178
|
-
let text
|
|
179
|
-
if (bound !== undefined) {
|
|
180
|
-
try {
|
|
181
|
-
const value = bound(key, ...args)
|
|
182
|
-
if (typeof value === 'string' && value !== key) text = value
|
|
183
|
-
} catch {
|
|
184
|
-
/* fall through to the local dictionaries */
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (text === undefined) {
|
|
188
|
-
const dict = String(active()).toLowerCase().startsWith('zh') ? ZH : EN
|
|
189
|
-
const fallback = dict[key] ?? EN[key] ?? key
|
|
190
|
-
text = typeof fallback === 'function' ? fallback(...args) : String(fallback)
|
|
191
|
-
}
|
|
192
|
-
return text
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Format a registry timestamp.
|
|
198
|
-
* @param {string | undefined} iso - ISO timestamp.
|
|
199
|
-
* @returns {string} a short local date, or an empty string.
|
|
200
|
-
*/
|
|
201
|
-
function formatDate(iso) {
|
|
202
|
-
if (typeof iso !== 'string') return ''
|
|
203
|
-
const date = new Date(iso)
|
|
204
|
-
if (Number.isNaN(date.getTime())) return ''
|
|
205
|
-
return date.toLocaleDateString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Open a URL in a new browser tab. Only http(s) links are ever passed in,
|
|
210
|
-
* and `noopener,noreferrer` keeps the opened page away from this window.
|
|
211
|
-
* @param {string | undefined} url - the address to open.
|
|
212
|
-
*/
|
|
213
|
-
function openExternal(url) {
|
|
214
|
-
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) return
|
|
215
|
-
if (typeof window !== 'undefined' && typeof window.open === 'function') {
|
|
216
|
-
window.open(url, '_blank', 'noopener,noreferrer')
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Read a JSON response, turning a failure into a thrown Error.
|
|
222
|
-
* @param {Response} response - the fetch response.
|
|
223
|
-
* @returns {Promise<any>} the parsed body.
|
|
224
|
-
*/
|
|
225
|
-
async function readJson(response) {
|
|
226
|
-
let body
|
|
227
|
-
try {
|
|
228
|
-
body = await response.json()
|
|
229
|
-
} catch {
|
|
230
|
-
body = undefined
|
|
231
|
-
}
|
|
232
|
-
if (!response.ok) {
|
|
233
|
-
const message = typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`
|
|
234
|
-
const failure = new Error(message)
|
|
235
|
-
failure.status = response.status
|
|
236
|
-
failure.body = body
|
|
237
|
-
throw failure
|
|
238
|
-
}
|
|
239
|
-
return body
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// -----------------------------------------------------------------------
|
|
243
|
-
// Hooks
|
|
244
|
-
// -----------------------------------------------------------------------
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Re-render on locale changes so copy follows the active language live.
|
|
248
|
-
* @param {any} locale - the locale service.
|
|
249
|
-
* @returns {number} a revision counter.
|
|
250
|
-
*/
|
|
251
|
-
function useLocaleRevision(locale) {
|
|
252
|
-
const [revision, setRevision] = React.useState(0)
|
|
253
|
-
React.useEffect(() => {
|
|
254
|
-
if (locale === undefined || typeof locale.subscribe !== 'function') return undefined
|
|
255
|
-
return locale.subscribe(() => setRevision((value) => value + 1))
|
|
256
|
-
}, [locale])
|
|
257
|
-
return revision
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Load the overview once and on demand.
|
|
262
|
-
* @param {string} localeId - active locale id for localized metadata.
|
|
263
|
-
* @returns {{ data: any, error: string | undefined, loading: boolean, reload: (force?: boolean) => void }} state.
|
|
264
|
-
*/
|
|
265
|
-
function useOverview(localeId) {
|
|
266
|
-
const [state, setState] = React.useState({ data: undefined, error: undefined, loading: true })
|
|
267
|
-
const [tick, setTick] = React.useState(0)
|
|
268
|
-
const [force, setForce] = React.useState(false)
|
|
269
|
-
const alive = React.useRef(true)
|
|
270
|
-
|
|
271
|
-
React.useEffect(() => () => {
|
|
272
|
-
alive.current = false
|
|
273
|
-
}, [])
|
|
274
|
-
|
|
275
|
-
React.useEffect(() => {
|
|
276
|
-
let cancelled = false
|
|
277
|
-
setState((current) => ({ ...current, loading: true, error: undefined }))
|
|
278
|
-
const query = new URLSearchParams({ refresh: force ? '1' : '0', locale: localeId })
|
|
279
|
-
fetch(`${API}/overview?${query.toString()}`, { credentials: 'same-origin' })
|
|
280
|
-
.then(readJson)
|
|
281
|
-
.then((data) => {
|
|
282
|
-
if (cancelled) return
|
|
283
|
-
setState({ data, error: undefined, loading: false })
|
|
284
|
-
})
|
|
285
|
-
.catch((error) => {
|
|
286
|
-
if (cancelled) return
|
|
287
|
-
setState((current) => ({
|
|
288
|
-
data: current.data,
|
|
289
|
-
error: error instanceof Error ? error.message : String(error),
|
|
290
|
-
loading: false,
|
|
291
|
-
}))
|
|
292
|
-
})
|
|
293
|
-
return () => {
|
|
294
|
-
cancelled = true
|
|
295
|
-
}
|
|
296
|
-
}, [localeId, tick, force])
|
|
297
|
-
|
|
298
|
-
const reload = React.useCallback((shouldForce = false) => {
|
|
299
|
-
setForce(shouldForce)
|
|
300
|
-
setTick((value) => value + 1)
|
|
301
|
-
}, [])
|
|
302
|
-
|
|
303
|
-
return { ...state, reload }
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// -----------------------------------------------------------------------
|
|
307
|
-
// Components
|
|
308
|
-
// -----------------------------------------------------------------------
|
|
309
|
-
|
|
310
|
-
const STYLE_ID = 'dsh-plugin-upgrader-style'
|
|
311
|
-
|
|
312
|
-
/** The tab's stylesheet, scoped to this bundle's class prefix. */
|
|
313
|
-
const CSS = `
|
|
314
|
-
.dsh-pu { display: flex; flex-direction: column; gap: 14px; padding: 4px 2px 24px; color: var(--dsw-alias-label-primary); }
|
|
315
|
-
.dsh-pu-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
|
|
316
|
-
.dsh-pu-head-main { display: flex; flex-direction: column; gap: 4px; min-width: 220px; flex: 1; }
|
|
317
|
-
.dsh-pu-title { font-size: 15px; font-weight: 600; letter-spacing: .01em; }
|
|
318
|
-
.dsh-pu-sub { font-size: 12px; color: var(--dsw-alias-label-secondary); }
|
|
319
|
-
.dsh-pu-stats { display: flex; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary); align-items: center; flex-wrap: wrap; }
|
|
320
|
-
.dsh-pu-stat { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); }
|
|
321
|
-
.dsh-pu-stat b { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
322
|
-
.dsh-pu-stat.is-warn b { color: var(--dsw-alias-brand-primary); }
|
|
323
|
-
.dsh-pu-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
324
|
-
.dsh-pu-btn { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border-radius: 8px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); transition: background .15s ease, border-color .15s ease, opacity .15s ease; font-family: inherit; }
|
|
325
|
-
.dsh-pu-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); border-color: var(--dsw-alias-border-l2); }
|
|
326
|
-
.dsh-pu-btn:disabled { opacity: .5; cursor: default; }
|
|
327
|
-
.dsh-pu-btn.is-primary { background: var(--dsw-alias-button-primary-fill); border-color: transparent; color: #fff; }
|
|
328
|
-
.dsh-pu-btn.is-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
|
|
329
|
-
.dsh-pu-btn.is-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); padding: 0 8px; }
|
|
330
|
-
.dsh-pu-btn.is-ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
331
|
-
.dsh-pu-btn.is-tiny { height: 24px; padding: 0 9px; font-size: 11px; border-radius: 6px; }
|
|
332
|
-
.dsh-pu-search { display: flex; align-items: center; gap: 8px; }
|
|
333
|
-
.dsh-pu-search input { height: 30px; width: 220px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); padding: 0 10px; font-size: 12px; font-family: inherit; outline: none; }
|
|
334
|
-
.dsh-pu-search input:focus { border-color: var(--dsw-alias-brand-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent); }
|
|
335
|
-
.dsh-pu-list { display: flex; flex-direction: column; gap: 8px; }
|
|
336
|
-
.dsh-pu-row { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; transition: border-color .15s ease; }
|
|
337
|
-
.dsh-pu-row:hover { border-color: var(--dsw-alias-border-l2); }
|
|
338
|
-
.dsh-pu-row.is-open { border-color: var(--dsw-alias-border-l2); }
|
|
339
|
-
.dsh-pu-row.is-picked { border-color: var(--dsw-alias-brand-primary); box-shadow: 0 0 0 1px color-mix(in srgb, var(--dsw-alias-brand-primary) 32%, transparent); }
|
|
340
|
-
.dsh-pu-row-main { display: flex; align-items: center; gap: 12px; padding: 10px 12px; }
|
|
341
|
-
.dsh-pu-check { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; flex: none; }
|
|
342
|
-
.dsh-pu-check input { width: 14px; height: 14px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-brand-primary); }
|
|
343
|
-
.dsh-pu-check input:disabled { cursor: default; opacity: .45; }
|
|
344
|
-
.dsh-pu-selbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; color: var(--dsw-alias-label-secondary); padding: 7px 10px; border-radius: 8px; border: 1px dashed var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); }
|
|
345
|
-
.dsh-pu-selbar.is-active { border-style: solid; border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-label-primary); }
|
|
346
|
-
.dsh-pu-selbar .dsh-pu-grow { flex: 1; min-width: 0; }
|
|
347
|
-
.dsh-pu-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dsw-alias-label-dimmed); }
|
|
348
|
-
.dsh-pu-dot.is-new { background: var(--dsw-alias-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent); }
|
|
349
|
-
.dsh-pu-dot.is-err { background: var(--dsw-alias-state-error-primary); }
|
|
350
|
-
.dsh-pu-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
|
|
351
|
-
.dsh-pu-name { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
352
|
-
.dsh-pu-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
353
|
-
.dsh-pu-desc { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
354
|
-
.dsh-pu-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
355
|
-
.dsh-pu-tag { font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); font-weight: 500; white-space: nowrap; }
|
|
356
|
-
.dsh-pu-tag.is-new { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
|
|
357
|
-
.dsh-pu-tag.is-err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
|
|
358
|
-
.dsh-pu-vers { display: flex; align-items: center; gap: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-label-secondary); flex: none; }
|
|
359
|
-
.dsh-pu-vers b { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
360
|
-
.dsh-pu-vers i { font-style: normal; color: var(--dsw-alias-label-tertiary); }
|
|
361
|
-
.dsh-pu-row-actions { display: flex; align-items: center; gap: 6px; flex: none; }
|
|
362
|
-
.dsh-pu-hist { border-top: 1px dashed var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); padding: 8px 12px 10px; }
|
|
363
|
-
.dsh-pu-hist-head { display: flex; align-items: center; gap: 8px; justify-content: flex-start; font-size: 11px; color: var(--dsw-alias-label-secondary); padding-bottom: 6px; }
|
|
364
|
-
.dsh-pu-hist-head .dsh-pu-mono { margin-left: auto; }
|
|
365
|
-
.dsh-pu-dim { color: var(--dsw-alias-label-tertiary); }
|
|
366
|
-
.dsh-pu-hist-list { display: flex; flex-direction: column; max-height: 260px; overflow: auto; }
|
|
367
|
-
.dsh-pu-hist-row { display: flex; align-items: center; gap: 10px; padding: 5px 6px; border-radius: 6px; font-size: 11px; }
|
|
368
|
-
.dsh-pu-hist-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
369
|
-
.dsh-pu-hist-row.is-current { background: var(--dsw-alias-interactive-bg-hover); box-shadow: inset 2px 0 0 var(--dsw-alias-brand-primary); }
|
|
370
|
-
.dsh-pu-hist-row .dsh-pu-mono { flex: none; min-width: 108px; color: var(--dsw-alias-label-primary); }
|
|
371
|
-
.dsh-pu-link { display: inline-flex; align-items: center; gap: 4px; border: 0; padding: 0; margin: 0; background: none; cursor: pointer; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-link); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--dsw-alias-link) 40%, transparent); text-underline-offset: 2px; }
|
|
372
|
-
.dsh-pu-link:hover { color: var(--dsw-alias-brand-primary); text-decoration-color: currentColor; }
|
|
373
|
-
.dsh-pu-link:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; border-radius: 3px; }
|
|
374
|
-
.dsh-pu-hist-row time { color: var(--dsw-alias-label-tertiary); flex: none; min-width: 84px; }
|
|
375
|
-
.dsh-pu-hist-row .dsh-pu-note { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); }
|
|
376
|
-
.dsh-pu-empty { border: 1px dashed var(--dsw-alias-border-l1); border-radius: 10px; padding: 28px 16px; text-align: center; font-size: 12px; color: var(--dsw-alias-label-secondary); display: flex; flex-direction: column; gap: 6px; }
|
|
377
|
-
.dsh-pu-empty small { color: var(--dsw-alias-label-tertiary); font-size: 11px; }
|
|
378
|
-
.dsh-pu-job { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
379
|
-
.dsh-pu-job-head { display: flex; align-items: center; gap: 10px; font-size: 12px; }
|
|
380
|
-
.dsh-pu-job-head .dsh-pu-grow { flex: 1; min-width: 0; }
|
|
381
|
-
.dsh-pu-bar { height: 4px; border-radius: 999px; background: var(--dsw-alias-bg-layer-3); overflow: hidden; }
|
|
382
|
-
.dsh-pu-bar div { height: 100%; border-radius: 999px; background: var(--dsw-alias-brand-primary); transition: width .25s ease; }
|
|
383
|
-
.dsh-pu-log { max-height: 160px; overflow: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 10.5px; line-height: 1.55; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; word-break: break-word; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 8px; margin: 0; }
|
|
384
|
-
.dsh-pu-banner { display: flex; align-items: center; gap: 8px; font-size: 11.5px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); }
|
|
385
|
-
.dsh-pu-banner.is-warn { border-color: var(--dsw-alias-state-warn-primary); color: var(--dsw-alias-state-warn-primary); }
|
|
386
|
-
.dsh-pu-banner.is-err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
|
|
387
|
-
.dsh-pu-foot { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--dsw-alias-label-tertiary); flex-wrap: wrap; }
|
|
388
|
-
.dsh-pu-spin { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--dsw-alias-border-l2); border-top-color: var(--dsw-alias-brand-primary); animation: dsh-pu-spin .8s linear infinite; flex: none; }
|
|
389
|
-
@keyframes dsh-pu-spin { to { transform: rotate(360deg); } }
|
|
390
|
-
@media (prefers-reduced-motion: reduce) { .dsh-pu-spin { animation-duration: 2.4s; } }
|
|
391
|
-
`
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* One version line inside a row's history.
|
|
395
|
-
* @param {{ row: any, version: any, t: Function, busy: boolean, onSwitch: Function }} props - render props.
|
|
396
|
-
* @returns {any} the element.
|
|
397
|
-
*/
|
|
398
|
-
function VersionLine({ row, version, t, busy, onSwitch }) {
|
|
399
|
-
const note = version.deprecated !== undefined ? `⚠ ${version.deprecated}` : ''
|
|
400
|
-
const raw = typeof version.href === 'string'
|
|
401
|
-
? version.href
|
|
402
|
-
: typeof version.url === 'string' && typeof row.repoUrl === 'string' && version.url.startsWith('/')
|
|
403
|
-
? `${row.repoUrl}${version.url}`
|
|
404
|
-
: version.url
|
|
405
|
-
const link = typeof raw === 'string' && /^https?:\/\//i.test(raw) ? raw : undefined
|
|
406
|
-
/** A row without a tag of its own points at the repository's release feed. */
|
|
407
|
-
const linkLabel = link !== undefined && /\/releases\/tag\//.test(link)
|
|
408
|
-
? t('githubVersion')
|
|
409
|
-
: t('githubReleases')
|
|
410
|
-
return h(
|
|
411
|
-
'div',
|
|
412
|
-
{ className: `dsh-pu-hist-row${version.current ? ' is-current' : ''}` },
|
|
413
|
-
link !== undefined
|
|
414
|
-
? h(
|
|
415
|
-
'button',
|
|
416
|
-
{
|
|
417
|
-
type: 'button',
|
|
418
|
-
className: 'dsh-pu-mono dsh-pu-link',
|
|
419
|
-
title: `${linkLabel} →${link}`,
|
|
420
|
-
onClick: () => openExternal(link),
|
|
421
|
-
},
|
|
422
|
-
version.version,
|
|
423
|
-
)
|
|
424
|
-
: h('span', { className: 'dsh-pu-mono' }, version.version),
|
|
425
|
-
h('time', null, formatDate(version.time)),
|
|
426
|
-
version.latest ? h('span', { className: 'dsh-pu-tag is-new' }, t('latest')) : null,
|
|
427
|
-
version.current ? h('span', { className: 'dsh-pu-tag' }, t('current')) : null,
|
|
428
|
-
h('span', { className: 'dsh-pu-note' }, note),
|
|
429
|
-
link !== undefined
|
|
430
|
-
? h(
|
|
431
|
-
'button',
|
|
432
|
-
{
|
|
433
|
-
type: 'button',
|
|
434
|
-
className: 'dsh-pu-btn is-ghost is-tiny',
|
|
435
|
-
title: `${linkLabel} →${link}`,
|
|
436
|
-
'aria-label': linkLabel === t('githubVersion')
|
|
437
|
-
? `${linkLabel}: ${version.version}`
|
|
438
|
-
: linkLabel,
|
|
439
|
-
onClick: () => openExternal(link),
|
|
440
|
-
},
|
|
441
|
-
'→',
|
|
442
|
-
)
|
|
443
|
-
: null,
|
|
444
|
-
h(
|
|
445
|
-
'button',
|
|
446
|
-
{
|
|
447
|
-
type: 'button',
|
|
448
|
-
className: 'dsh-pu-btn is-tiny',
|
|
449
|
-
disabled: busy || version.current === true,
|
|
450
|
-
onClick: () => onSwitch(row, version.version),
|
|
451
|
-
},
|
|
452
|
-
version.version === row.latest ? t('back') : t('switchTo'),
|
|
453
|
-
),
|
|
454
|
-
)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* One installed plugin.
|
|
459
|
-
* @param {{ row: any, t: Function, busy: boolean, onUpgrade: Function, onSwitch: Function, onApprove: Function, onOpenPage: Function }} props - render props.
|
|
460
|
-
* @returns {any} the element.
|
|
461
|
-
*/
|
|
462
|
-
function PluginRow({ row, t, busy, picked, onToggle, onUpgrade, onSwitch, onApprove, onOpenPage }) {
|
|
463
|
-
const [open, setOpen] = React.useState(false)
|
|
464
|
-
const hasHistory = Array.isArray(row.versions) && row.versions.length > 0
|
|
465
|
-
const selectable = row.upgradable === true
|
|
466
|
-
const status = row.error !== undefined && row.error !== null
|
|
467
|
-
? 'err'
|
|
468
|
-
: row.upgradable
|
|
469
|
-
? 'new'
|
|
470
|
-
: 'idle'
|
|
471
|
-
const versionNote = row.versionError !== undefined
|
|
472
|
-
? row.versionError === 'not a profile-managed package'
|
|
473
|
-
? t('sourcePath')
|
|
474
|
-
: row.versionError
|
|
475
|
-
: undefined
|
|
476
|
-
|
|
477
|
-
return h(
|
|
478
|
-
'div',
|
|
479
|
-
{ className: `dsh-pu-row${open ? ' is-open' : ''}${picked ? ' is-picked' : ''}` },
|
|
480
|
-
h(
|
|
481
|
-
'div',
|
|
482
|
-
{ className: 'dsh-pu-row-main' },
|
|
483
|
-
h(
|
|
484
|
-
'span',
|
|
485
|
-
{ className: 'dsh-pu-check' },
|
|
486
|
-
h('input', {
|
|
487
|
-
type: 'checkbox',
|
|
488
|
-
checked: picked === true,
|
|
489
|
-
disabled: busy || !selectable,
|
|
490
|
-
title: selectable ? t('selectRow') : undefined,
|
|
491
|
-
'aria-label': `${t('selectRow')}: ${row.name}`,
|
|
492
|
-
onChange: () => onToggle(row.name),
|
|
493
|
-
}),
|
|
494
|
-
),
|
|
495
|
-
h('span', { className: `dsh-pu-dot${status === 'idle' ? '' : status === 'new' ? ' is-new' : ' is-err'}` }),
|
|
496
|
-
h(
|
|
497
|
-
'div',
|
|
498
|
-
{ className: 'dsh-pu-id' },
|
|
499
|
-
h(
|
|
500
|
-
'div',
|
|
501
|
-
{ className: 'dsh-pu-name' },
|
|
502
|
-
h('span', { title: row.name }, row.title === row.name ? row.name : `${row.title} · ${row.name}`),
|
|
503
|
-
row.enabled === false ? h('span', { className: 'dsh-pu-tag' }, 'off') : null,
|
|
504
|
-
row.upgradable ? h('span', { className: 'dsh-pu-tag is-new' }, t('upgradable')) : null,
|
|
505
|
-
),
|
|
506
|
-
h(
|
|
507
|
-
'div',
|
|
508
|
-
{ className: 'dsh-pu-desc' },
|
|
509
|
-
row.description !== undefined && row.description !== ''
|
|
510
|
-
? row.description
|
|
511
|
-
: versionNote ?? row.name,
|
|
512
|
-
),
|
|
513
|
-
),
|
|
514
|
-
h(
|
|
515
|
-
'div',
|
|
516
|
-
{ className: 'dsh-pu-vers' },
|
|
517
|
-
h('b', null, row.version ?? t('unknownVersion')),
|
|
518
|
-
row.upgradable ? h('i', null, '→') : null,
|
|
519
|
-
row.upgradable ? h('b', null, row.latest) : null,
|
|
520
|
-
),
|
|
521
|
-
h(
|
|
522
|
-
'div',
|
|
523
|
-
{ className: 'dsh-pu-row-actions' },
|
|
524
|
-
row.upgradable
|
|
525
|
-
? h(
|
|
526
|
-
'button',
|
|
527
|
-
{
|
|
528
|
-
type: 'button',
|
|
529
|
-
className: 'dsh-pu-btn is-primary is-tiny',
|
|
530
|
-
disabled: busy,
|
|
531
|
-
onClick: () => onUpgrade(row),
|
|
532
|
-
},
|
|
533
|
-
t('upgrade'),
|
|
534
|
-
)
|
|
535
|
-
: null,
|
|
536
|
-
typeof row.repoUrl === 'string'
|
|
537
|
-
? h(
|
|
538
|
-
'button',
|
|
539
|
-
{
|
|
540
|
-
type: 'button',
|
|
541
|
-
className: 'dsh-pu-btn is-tiny',
|
|
542
|
-
title: `${t('github')} →${row.repoUrl}`,
|
|
543
|
-
onClick: () => openExternal(row.repoUrl),
|
|
544
|
-
},
|
|
545
|
-
'GH',
|
|
546
|
-
)
|
|
547
|
-
: null,
|
|
548
|
-
hasHistory
|
|
549
|
-
? h(
|
|
550
|
-
'button',
|
|
551
|
-
{
|
|
552
|
-
type: 'button',
|
|
553
|
-
className: 'dsh-pu-btn is-tiny',
|
|
554
|
-
onClick: () => setOpen((value) => !value),
|
|
555
|
-
'aria-expanded': open,
|
|
556
|
-
},
|
|
557
|
-
open ? t('hideHistory') : `${t('history')} (${row.versions.length})`,
|
|
558
|
-
)
|
|
559
|
-
: null,
|
|
560
|
-
typeof onOpenPage === 'function'
|
|
561
|
-
? h(
|
|
562
|
-
'button',
|
|
563
|
-
{
|
|
564
|
-
type: 'button',
|
|
565
|
-
className: 'dsh-pu-btn is-ghost is-tiny',
|
|
566
|
-
title: t('openPage'),
|
|
567
|
-
onClick: () => onOpenPage(row.name),
|
|
568
|
-
},
|
|
569
|
-
'→',
|
|
570
|
-
)
|
|
571
|
-
: null,
|
|
572
|
-
),
|
|
573
|
-
),
|
|
574
|
-
row.pendingBuilds !== undefined && Array.isArray(row.pendingBuilds) && row.pendingBuilds.length > 0
|
|
575
|
-
? h(
|
|
576
|
-
'div',
|
|
577
|
-
{ className: 'dsh-pu-hist' },
|
|
578
|
-
h(
|
|
579
|
-
'div',
|
|
580
|
-
{ className: 'dsh-pu-banner is-warn' },
|
|
581
|
-
h('span', null, t('buildBlocked')),
|
|
582
|
-
h(
|
|
583
|
-
'button',
|
|
584
|
-
{ type: 'button', className: 'dsh-pu-btn is-tiny', onClick: () => onApprove(row) },
|
|
585
|
-
t('approve'),
|
|
586
|
-
),
|
|
587
|
-
),
|
|
588
|
-
)
|
|
589
|
-
: null,
|
|
590
|
-
open
|
|
591
|
-
? h(
|
|
592
|
-
'div',
|
|
593
|
-
{ className: 'dsh-pu-hist' },
|
|
594
|
-
h(
|
|
595
|
-
'div',
|
|
596
|
-
{ className: 'dsh-pu-hist-head' },
|
|
597
|
-
h('span', null, t('versions', row.totalVersions ?? row.versions.length)),
|
|
598
|
-
typeof row.totalVersions === 'number' && row.totalVersions > row.versions.length
|
|
599
|
-
? h('span', { className: 'dsh-pu-dim' }, t('showingNewest', row.versions.length))
|
|
600
|
-
: null,
|
|
601
|
-
h('span', { className: 'dsh-pu-mono' }, row.name),
|
|
602
|
-
),
|
|
603
|
-
h(
|
|
604
|
-
'div',
|
|
605
|
-
{ className: 'dsh-pu-hist-list' },
|
|
606
|
-
row.versions.map((version) =>
|
|
607
|
-
h(VersionLine, {
|
|
608
|
-
key: version.version,
|
|
609
|
-
row,
|
|
610
|
-
version,
|
|
611
|
-
t,
|
|
612
|
-
busy,
|
|
613
|
-
onSwitch,
|
|
614
|
-
}),
|
|
615
|
-
),
|
|
616
|
-
),
|
|
617
|
-
)
|
|
618
|
-
: null,
|
|
619
|
-
)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Batch progress panel.
|
|
624
|
-
* @param {{ job: any, t: Function, onCancel: Function, cancelling: boolean }} props - render props.
|
|
625
|
-
* @returns {any} the element.
|
|
626
|
-
*/
|
|
627
|
-
function JobPanel({ job, t, onCancel, cancelling }) {
|
|
628
|
-
const [showLog, setShowLog] = React.useState(true)
|
|
629
|
-
const total = job.total ?? 0
|
|
630
|
-
const done = job.done ?? 0
|
|
631
|
-
const percent = total === 0 ? 0 : Math.round((done / total) * 100)
|
|
632
|
-
const running = job.status === 'running' || job.status === 'queued'
|
|
633
|
-
const failed = job.failed ?? 0
|
|
634
|
-
return h(
|
|
635
|
-
'div',
|
|
636
|
-
{ className: 'dsh-pu-job' },
|
|
637
|
-
h(
|
|
638
|
-
'div',
|
|
639
|
-
{ className: 'dsh-pu-job-head' },
|
|
640
|
-
running ? h('span', { className: 'dsh-pu-spin' }) : null,
|
|
641
|
-
h(
|
|
642
|
-
'span',
|
|
643
|
-
{ className: 'dsh-pu-grow' },
|
|
644
|
-
running
|
|
645
|
-
? t('progress', done, total)
|
|
646
|
-
: job.status === 'cancelled'
|
|
647
|
-
? t('jobCancelled')
|
|
648
|
-
: t('jobDone'),
|
|
649
|
-
job.current !== undefined ? h('span', { className: 'dsh-pu-mono' }, ` ${job.current}`) : null,
|
|
650
|
-
),
|
|
651
|
-
failed > 0 ? h('span', { className: 'dsh-pu-tag is-err' }, t('failedSome', failed)) : null,
|
|
652
|
-
running
|
|
653
|
-
? h(
|
|
654
|
-
'button',
|
|
655
|
-
{
|
|
656
|
-
type: 'button',
|
|
657
|
-
className: 'dsh-pu-btn is-tiny',
|
|
658
|
-
disabled: cancelling,
|
|
659
|
-
onClick: onCancel,
|
|
660
|
-
},
|
|
661
|
-
cancelling ? t('cancelling') : t('cancel'),
|
|
662
|
-
)
|
|
663
|
-
: null,
|
|
664
|
-
h(
|
|
665
|
-
'button',
|
|
666
|
-
{ type: 'button', className: 'dsh-pu-btn is-ghost is-tiny', onClick: () => setShowLog((value) => !value) },
|
|
667
|
-
showLog ? t('hideLog') : t('showLog'),
|
|
668
|
-
),
|
|
669
|
-
),
|
|
670
|
-
h('div', { className: 'dsh-pu-bar' }, h('div', { style: { width: `${percent}%` } })),
|
|
671
|
-
showLog && Array.isArray(job.log)
|
|
672
|
-
? h('pre', { className: 'dsh-pu-log' }, job.log.join('\n'))
|
|
673
|
-
: null,
|
|
674
|
-
)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* The Plugins-page tab.
|
|
679
|
-
* @param {any} props - owner props (`install`, `openPackage`) plus standard props.
|
|
680
|
-
* @returns {any} the element.
|
|
681
|
-
*/
|
|
682
|
-
function Tab(props) {
|
|
683
|
-
const { t, locale } = props
|
|
684
|
-
const revision = useLocaleRevision(locale)
|
|
685
|
-
const localeId = React.useMemo(() => {
|
|
686
|
-
try {
|
|
687
|
-
return String(locale?.getLocale?.().active ?? 'en')
|
|
688
|
-
} catch {
|
|
689
|
-
return 'en'
|
|
690
|
-
}
|
|
691
|
-
}, [locale, revision])
|
|
692
|
-
const { data, error, loading, reload } = useOverview(localeId)
|
|
693
|
-
|
|
694
|
-
const [query, setQuery] = React.useState('')
|
|
695
|
-
const [picked, setPicked] = React.useState(() => new Set())
|
|
696
|
-
const [job, setJob] = React.useState(undefined)
|
|
697
|
-
const [busyName, setBusyName] = React.useState(undefined)
|
|
698
|
-
const [cancelling, setCancelling] = React.useState(false)
|
|
699
|
-
const [banner, setBanner] = React.useState(undefined)
|
|
700
|
-
|
|
701
|
-
const alive = React.useRef(true)
|
|
702
|
-
React.useEffect(() => () => {
|
|
703
|
-
alive.current = false
|
|
704
|
-
}, [])
|
|
705
|
-
|
|
706
|
-
/** Poll a job until it settles, then reload the overview. */
|
|
707
|
-
const watch = React.useCallback(
|
|
708
|
-
async (id) => {
|
|
709
|
-
let settled
|
|
710
|
-
for (;;) {
|
|
711
|
-
if (!alive.current) return
|
|
712
|
-
// eslint-disable-next-line no-await-in-loop
|
|
713
|
-
const response = await fetch(`${API}/job?id=${encodeURIComponent(id)}`, { credentials: 'same-origin' })
|
|
714
|
-
// eslint-disable-next-line no-await-in-loop
|
|
715
|
-
const snapshot = await readJson(response).catch(() => undefined)
|
|
716
|
-
if (snapshot === undefined) break
|
|
717
|
-
if (alive.current) setJob(snapshot)
|
|
718
|
-
if (snapshot.status !== 'running' && snapshot.status !== 'queued') {
|
|
719
|
-
settled = snapshot
|
|
720
|
-
break
|
|
721
|
-
}
|
|
722
|
-
// eslint-disable-next-line no-await-in-loop
|
|
723
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_MS))
|
|
724
|
-
}
|
|
725
|
-
if (!alive.current) return
|
|
726
|
-
setBusyName(undefined)
|
|
727
|
-
setCancelling(false)
|
|
728
|
-
reload(false)
|
|
729
|
-
const failures = settled?.results?.filter((result) => result.ok !== true) ?? []
|
|
730
|
-
if (failures.length > 0) {
|
|
731
|
-
setBanner({
|
|
732
|
-
tone: 'err',
|
|
733
|
-
text: failures.map((failure) => `${failure.name}: ${failure.message ?? failure.detail ?? 'error'}`).join(' · '),
|
|
734
|
-
})
|
|
735
|
-
} else if (settled?.results?.some((result) => result.application === 'restart-required')) {
|
|
736
|
-
setBanner({ tone: 'warn', text: t('restart') })
|
|
737
|
-
}
|
|
738
|
-
},
|
|
739
|
-
[reload, t],
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Adopt a job that is still running on the host. The tab is remounted
|
|
744
|
-
* whenever the Plugins page is reopened, and the host keeps the job, so
|
|
745
|
-
* a fresh tab would otherwise look idle while a batch is in flight.
|
|
746
|
-
*/
|
|
747
|
-
const adopted = React.useRef(undefined)
|
|
748
|
-
React.useEffect(() => {
|
|
749
|
-
const live = data?.active
|
|
750
|
-
if (live === undefined || live === null) return
|
|
751
|
-
if (adopted.current === live.id) return
|
|
752
|
-
adopted.current = live.id
|
|
753
|
-
setJob(live)
|
|
754
|
-
setBusyName(live.current)
|
|
755
|
-
void watch(live.id)
|
|
756
|
-
}, [data, watch])
|
|
757
|
-
|
|
758
|
-
/** Start a batch of steps. */
|
|
759
|
-
const run = React.useCallback(
|
|
760
|
-
async (steps, label) => {
|
|
761
|
-
if (steps.length === 0) return
|
|
762
|
-
setBanner(undefined)
|
|
763
|
-
setBusyName(label)
|
|
764
|
-
try {
|
|
765
|
-
const response = await fetch(`${API}/job`, {
|
|
766
|
-
method: 'POST',
|
|
767
|
-
credentials: 'same-origin',
|
|
768
|
-
headers: { 'content-type': 'application/json' },
|
|
769
|
-
body: JSON.stringify({ steps }),
|
|
770
|
-
})
|
|
771
|
-
const started = await readJson(response)
|
|
772
|
-
setJob({ id: started.id, status: 'queued', total: steps.length, done: 0, failed: 0, log: [] })
|
|
773
|
-
await watch(started.id)
|
|
774
|
-
} catch (failure) {
|
|
775
|
-
setBusyName(undefined)
|
|
776
|
-
// A batch already running is not an error the user caused: adopt it
|
|
777
|
-
// and keep reporting its progress rather than showing a dead "busy".
|
|
778
|
-
if (failure instanceof Error && failure.status === 409 && typeof failure.body?.id === 'string') {
|
|
779
|
-
setJob({
|
|
780
|
-
id: failure.body.id,
|
|
781
|
-
status: 'running',
|
|
782
|
-
total: failure.body.total ?? 0,
|
|
783
|
-
done: failure.body.done ?? 0,
|
|
784
|
-
failed: 0,
|
|
785
|
-
current: failure.body.current,
|
|
786
|
-
log: [],
|
|
787
|
-
})
|
|
788
|
-
setBanner({ tone: 'warn', text: t('busyAdopted') })
|
|
789
|
-
await watch(failure.body.id)
|
|
790
|
-
return
|
|
791
|
-
}
|
|
792
|
-
setBanner({ tone: 'err', text: failure instanceof Error ? failure.message : String(failure) })
|
|
793
|
-
}
|
|
794
|
-
},
|
|
795
|
-
[watch, t],
|
|
796
|
-
)
|
|
797
|
-
|
|
798
|
-
const rows = Array.isArray(data?.bundles) ? data.bundles : []
|
|
799
|
-
const upgradable = rows.filter((row) => row.upgradable === true)
|
|
800
|
-
const filtered = query.trim() === ''
|
|
801
|
-
? rows
|
|
802
|
-
: rows.filter((row) => {
|
|
803
|
-
const needle = query.trim().toLowerCase()
|
|
804
|
-
return (
|
|
805
|
-
row.name.toLowerCase().includes(needle) ||
|
|
806
|
-
String(row.title ?? '').toLowerCase().includes(needle)
|
|
807
|
-
)
|
|
808
|
-
})
|
|
809
|
-
|
|
810
|
-
const busy = busyName !== undefined || (job !== undefined && (job.status === 'running' || job.status === 'queued'))
|
|
811
|
-
|
|
812
|
-
// Only upgradable rows can be picked, so a stale pick (a plugin that just
|
|
813
|
-
// got upgraded, or dropped out of the registry) never blocks the button.
|
|
814
|
-
const pickedCount = upgradable.reduce((count, row) => (picked.has(row.name) ? count + 1 : count), 0)
|
|
815
|
-
|
|
816
|
-
const upgradeOne = (row) =>
|
|
817
|
-
run([{ name: row.name, version: row.latest, action: 'upgrade', enabled: row.enabled !== false }], row.name)
|
|
818
|
-
|
|
819
|
-
const switchOne = (row, version) =>
|
|
820
|
-
run([{ name: row.name, version, action: 'switch', enabled: row.enabled !== false }], row.name)
|
|
821
|
-
|
|
822
|
-
const approveAndRetry = (row) => {
|
|
823
|
-
const pending = Array.isArray(row.pendingBuilds) ? row.pendingBuilds : []
|
|
824
|
-
run(
|
|
825
|
-
[
|
|
826
|
-
{
|
|
827
|
-
name: row.name,
|
|
828
|
-
version: row.latest ?? row.version,
|
|
829
|
-
action: 'upgrade',
|
|
830
|
-
enabled: row.enabled !== false,
|
|
831
|
-
approveBuilds: pending,
|
|
832
|
-
},
|
|
833
|
-
],
|
|
834
|
-
row.name,
|
|
835
|
-
)
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
const upgradeAll = () =>
|
|
839
|
-
run(
|
|
840
|
-
upgradable.map((row) => ({
|
|
841
|
-
name: row.name,
|
|
842
|
-
version: row.latest,
|
|
843
|
-
action: 'upgrade',
|
|
844
|
-
enabled: row.enabled !== false,
|
|
845
|
-
})),
|
|
846
|
-
t('upgradeAll'),
|
|
847
|
-
)
|
|
848
|
-
|
|
849
|
-
/** Toggle one plugin in the selection. */
|
|
850
|
-
const togglePick = (name) =>
|
|
851
|
-
setPicked((current) => {
|
|
852
|
-
const next = new Set(current)
|
|
853
|
-
if (next.has(name)) next.delete(name)
|
|
854
|
-
else next.add(name)
|
|
855
|
-
return next
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
/** Select every upgradable row that is currently visible. */
|
|
859
|
-
const pickAll = () => setPicked(new Set(upgradable.map((row) => row.name)))
|
|
860
|
-
|
|
861
|
-
/** Drop the whole selection. */
|
|
862
|
-
const clearPicked = () => setPicked(new Set())
|
|
863
|
-
|
|
864
|
-
/** Upgrade only the checked plugins, in list order. */
|
|
865
|
-
const upgradePicked = () =>
|
|
866
|
-
run(
|
|
867
|
-
upgradable
|
|
868
|
-
.filter((row) => picked.has(row.name))
|
|
869
|
-
.map((row) => ({
|
|
870
|
-
name: row.name,
|
|
871
|
-
version: row.latest,
|
|
872
|
-
action: 'upgrade',
|
|
873
|
-
enabled: row.enabled !== false,
|
|
874
|
-
})),
|
|
875
|
-
t('upgradeSelected'),
|
|
876
|
-
)
|
|
877
|
-
|
|
878
|
-
const cancel = async () => {
|
|
879
|
-
setCancelling(true)
|
|
880
|
-
try {
|
|
881
|
-
await fetch(`${API}/cancel`, { method: 'POST', credentials: 'same-origin' })
|
|
882
|
-
} catch {
|
|
883
|
-
/* the poll loop reports the final state anyway */
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return h(
|
|
888
|
-
'div',
|
|
889
|
-
{ className: 'dsh-pu' },
|
|
890
|
-
h('style', { key: STYLE_ID }, CSS),
|
|
891
|
-
h(
|
|
892
|
-
'div',
|
|
893
|
-
{ className: 'dsh-pu-head' },
|
|
894
|
-
h(
|
|
895
|
-
'div',
|
|
896
|
-
{ className: 'dsh-pu-head-main' },
|
|
897
|
-
h('div', { className: 'dsh-pu-title' }, t('title')),
|
|
898
|
-
h('div', { className: 'dsh-pu-sub' }, t('subtitle')),
|
|
899
|
-
h(
|
|
900
|
-
'div',
|
|
901
|
-
{ className: 'dsh-pu-stats' },
|
|
902
|
-
h(
|
|
903
|
-
'span',
|
|
904
|
-
{ className: 'dsh-pu-stat' },
|
|
905
|
-
t('installed'),
|
|
906
|
-
' ',
|
|
907
|
-
h('b', null, String(rows.length)),
|
|
908
|
-
),
|
|
909
|
-
h(
|
|
910
|
-
'span',
|
|
911
|
-
{ className: `dsh-pu-stat${upgradable.length > 0 ? ' is-warn' : ''}` },
|
|
912
|
-
t('upgradable'),
|
|
913
|
-
' ',
|
|
914
|
-
h('b', null, String(upgradable.length)),
|
|
915
|
-
),
|
|
916
|
-
data?.registry !== undefined
|
|
917
|
-
? h('span', { className: 'dsh-pu-mono' }, `${t('registry')}: ${data.registry}`)
|
|
918
|
-
: null,
|
|
919
|
-
),
|
|
920
|
-
),
|
|
921
|
-
h(
|
|
922
|
-
'div',
|
|
923
|
-
{ className: 'dsh-pu-actions' },
|
|
924
|
-
h(
|
|
925
|
-
'div',
|
|
926
|
-
{ className: 'dsh-pu-search' },
|
|
927
|
-
h('input', {
|
|
928
|
-
type: 'search',
|
|
929
|
-
placeholder: t('search'),
|
|
930
|
-
value: query,
|
|
931
|
-
onChange: (event) => setQuery(event.target.value),
|
|
932
|
-
'aria-label': t('search'),
|
|
933
|
-
}),
|
|
934
|
-
),
|
|
935
|
-
h(
|
|
936
|
-
'button',
|
|
937
|
-
{
|
|
938
|
-
type: 'button',
|
|
939
|
-
className: 'dsh-pu-btn',
|
|
940
|
-
disabled: loading === true || busy,
|
|
941
|
-
onClick: () => reload(true),
|
|
942
|
-
},
|
|
943
|
-
loading === true ? h('span', { className: 'dsh-pu-spin' }) : null,
|
|
944
|
-
loading === true ? t('refreshing') : t('refresh'),
|
|
945
|
-
),
|
|
946
|
-
h(
|
|
947
|
-
'button',
|
|
948
|
-
{
|
|
949
|
-
type: 'button',
|
|
950
|
-
className: 'dsh-pu-btn is-primary',
|
|
951
|
-
disabled: upgradable.length === 0 || busy,
|
|
952
|
-
onClick: upgradeAll,
|
|
953
|
-
},
|
|
954
|
-
t('upgradeAll'),
|
|
955
|
-
upgradable.length > 0 ? ` (${upgradable.length})` : '',
|
|
956
|
-
),
|
|
957
|
-
),
|
|
958
|
-
),
|
|
959
|
-
|
|
960
|
-
banner !== undefined
|
|
961
|
-
? h(
|
|
962
|
-
'div',
|
|
963
|
-
{ className: `dsh-pu-banner${banner.tone === 'err' ? ' is-err' : banner.tone === 'warn' ? ' is-warn' : ''}` },
|
|
964
|
-
h('span', null, banner.text),
|
|
965
|
-
banner.tone === 'warn' ? h('span', null, t('restartHint')) : null,
|
|
966
|
-
h(
|
|
967
|
-
'button',
|
|
968
|
-
{ type: 'button', className: 'dsh-pu-btn is-ghost is-tiny', onClick: () => setBanner(undefined) },
|
|
969
|
-
'✕',
|
|
970
|
-
),
|
|
971
|
-
)
|
|
972
|
-
: null,
|
|
973
|
-
|
|
974
|
-
job !== undefined ? h(JobPanel, { job, t, onCancel: cancel, cancelling }) : null,
|
|
975
|
-
|
|
976
|
-
rows.length > 0
|
|
977
|
-
? h(
|
|
978
|
-
'div',
|
|
979
|
-
{ className: `dsh-pu-selbar${pickedCount > 0 ? ' is-active' : ''}` },
|
|
980
|
-
h(
|
|
981
|
-
'span',
|
|
982
|
-
{ className: 'dsh-pu-grow' },
|
|
983
|
-
pickedCount > 0 ? t('selected', pickedCount) : t('nothingSelected'),
|
|
984
|
-
),
|
|
985
|
-
h(
|
|
986
|
-
'button',
|
|
987
|
-
{
|
|
988
|
-
type: 'button',
|
|
989
|
-
className: 'dsh-pu-btn is-tiny',
|
|
990
|
-
disabled: busy || upgradable.length === 0,
|
|
991
|
-
onClick: pickAll,
|
|
992
|
-
},
|
|
993
|
-
t('selectAll'),
|
|
994
|
-
),
|
|
995
|
-
pickedCount > 0
|
|
996
|
-
? h(
|
|
997
|
-
'button',
|
|
998
|
-
{
|
|
999
|
-
type: 'button',
|
|
1000
|
-
className: 'dsh-pu-btn is-tiny',
|
|
1001
|
-
disabled: busy,
|
|
1002
|
-
onClick: clearPicked,
|
|
1003
|
-
},
|
|
1004
|
-
t('clearSelection'),
|
|
1005
|
-
)
|
|
1006
|
-
: null,
|
|
1007
|
-
h(
|
|
1008
|
-
'button',
|
|
1009
|
-
{
|
|
1010
|
-
type: 'button',
|
|
1011
|
-
className: 'dsh-pu-btn is-tiny is-primary',
|
|
1012
|
-
disabled: busy || pickedCount === 0,
|
|
1013
|
-
onClick: upgradePicked,
|
|
1014
|
-
},
|
|
1015
|
-
t('upgradeSelected'),
|
|
1016
|
-
pickedCount > 0 ? ` (${pickedCount})` : '',
|
|
1017
|
-
),
|
|
1018
|
-
)
|
|
1019
|
-
: null,
|
|
1020
|
-
|
|
1021
|
-
loading === true && rows.length === 0
|
|
1022
|
-
? h('div', { className: 'dsh-pu-empty' }, h('span', null, t('loading')))
|
|
1023
|
-
: null,
|
|
1024
|
-
|
|
1025
|
-
error !== undefined && rows.length === 0
|
|
1026
|
-
? h(
|
|
1027
|
-
'div',
|
|
1028
|
-
{ className: 'dsh-pu-empty' },
|
|
1029
|
-
h('span', null, `${t('loadFailed')} →${error}`),
|
|
1030
|
-
h(
|
|
1031
|
-
'div',
|
|
1032
|
-
null,
|
|
1033
|
-
h('button', { type: 'button', className: 'dsh-pu-btn is-tiny', onClick: () => reload(true) }, t('retry')),
|
|
1034
|
-
),
|
|
1035
|
-
)
|
|
1036
|
-
: null,
|
|
1037
|
-
|
|
1038
|
-
rows.length === 0 && loading !== true && error === undefined
|
|
1039
|
-
? h(
|
|
1040
|
-
'div',
|
|
1041
|
-
{ className: 'dsh-pu-empty' },
|
|
1042
|
-
h('span', null, t('empty')),
|
|
1043
|
-
h('small', null, t('emptyHint')),
|
|
1044
|
-
)
|
|
1045
|
-
: null,
|
|
1046
|
-
|
|
1047
|
-
filtered.length === 0 && rows.length > 0
|
|
1048
|
-
? h('div', { className: 'dsh-pu-empty' }, h('span', null, t('noMatch')))
|
|
1049
|
-
: null,
|
|
1050
|
-
|
|
1051
|
-
filtered.length > 0
|
|
1052
|
-
? h(
|
|
1053
|
-
'div',
|
|
1054
|
-
{ className: 'dsh-pu-list' },
|
|
1055
|
-
filtered.map((row) =>
|
|
1056
|
-
h(PluginRow, {
|
|
1057
|
-
key: row.name,
|
|
1058
|
-
row,
|
|
1059
|
-
t,
|
|
1060
|
-
busy,
|
|
1061
|
-
picked: picked.has(row.name),
|
|
1062
|
-
onToggle: togglePick,
|
|
1063
|
-
onUpgrade: upgradeOne,
|
|
1064
|
-
onSwitch: switchOne,
|
|
1065
|
-
onApprove: approveAndRetry,
|
|
1066
|
-
onOpenPage: typeof props.openPackage === 'function' ? props.openPackage : undefined,
|
|
1067
|
-
}),
|
|
1068
|
-
),
|
|
1069
|
-
)
|
|
1070
|
-
: null,
|
|
1071
|
-
|
|
1072
|
-
rows.length > 0 && upgradable.length === 0
|
|
1073
|
-
? h('div', { className: 'dsh-pu-banner' }, h('span', null, t('upToDate')))
|
|
1074
|
-
: null,
|
|
1075
|
-
|
|
1076
|
-
h(
|
|
1077
|
-
'div',
|
|
1078
|
-
{ className: 'dsh-pu-foot' },
|
|
1079
|
-
data?.generatedAt !== undefined
|
|
1080
|
-
? h('span', null, `${t('generated')} ${new Date(data.generatedAt).toLocaleTimeString()}`)
|
|
1081
|
-
: null,
|
|
1082
|
-
data?.registry !== undefined ? h('span', { className: 'dsh-pu-mono' }, data.registry) : null,
|
|
1083
|
-
),
|
|
1084
|
-
)
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// -----------------------------------------------------------------------
|
|
1088
|
-
// Plugin
|
|
1089
|
-
// -----------------------------------------------------------------------
|
|
1090
|
-
|
|
1091
|
-
/** Stable plugin name; matches the package and the patch insert id. */
|
|
1092
|
-
const name = 'plugin-upgrader'
|
|
1093
|
-
|
|
1094
|
-
/** Hard client dependencies: the slot registry and the locale registry. */
|
|
1095
|
-
const inject = ['slots', 'locale']
|
|
1096
|
-
|
|
1097
|
-
/**
|
|
1098
|
-
* Mount the browser half.
|
|
1099
|
-
* @param {any} ctx - the client plugin context.
|
|
1100
|
-
*/
|
|
1101
|
-
function apply(ctx) {
|
|
1102
|
-
const locale = ctx.get('locale', false)
|
|
1103
|
-
const active = () => {
|
|
1104
|
-
try {
|
|
1105
|
-
return String(locale?.getLocale?.().active ?? 'en')
|
|
1106
|
-
} catch {
|
|
1107
|
-
return 'en'
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
let bound
|
|
1111
|
-
if (locale !== undefined && locale !== null) {
|
|
1112
|
-
try {
|
|
1113
|
-
locale.register(NS, { zh: ZH, en: EN })
|
|
1114
|
-
bound = locale.bind(NS)
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
console.warn('[plugin-upgrader] locale namespace unavailable, using built-in copy', error)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
const t = makeT(bound, active)
|
|
1120
|
-
|
|
1121
|
-
ctx.slots.inject('plugins.tab', () =>
|
|
1122
|
-
ctx.slots.register(
|
|
1123
|
-
{
|
|
1124
|
-
name: 'plugins.tab',
|
|
1125
|
-
id: name,
|
|
1126
|
-
order: 5,
|
|
1127
|
-
label: () => t('tab'),
|
|
1128
|
-
},
|
|
1129
|
-
(ownerProps) => h(Tab, { ...ownerProps, t, locale }),
|
|
1130
|
-
),
|
|
1131
|
-
)
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
return { name, inject, apply }
|
|
1135
|
-
},
|
|
1136
|
-
})
|