@chatcode/cco-market 1.45.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity entry and its panel: every install, update and uninstall the
|
|
3
|
+
* user started, with the action each outcome calls for.
|
|
4
|
+
*
|
|
5
|
+
* The entry lives in the tab row rather than above the plugin grid, so
|
|
6
|
+
* paginating, searching or switching tab cannot take a record — or a pending
|
|
7
|
+
* decision — off screen. It reports the batch as one aggregate ("installing
|
|
8
|
+
* 3 / 7") instead of one line per plugin.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
IconCheckOutline16,
|
|
14
|
+
IconLoadingOutline16,
|
|
15
|
+
IconWarningOutline16,
|
|
16
|
+
IconChevronDownOutline14,
|
|
17
|
+
IconChevronUpOutline14,
|
|
18
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
19
|
+
import { useEffect, useRef, useState } from 'react'
|
|
20
|
+
import type { ReactNode } from 'react'
|
|
21
|
+
import css from './Market.module.css'
|
|
22
|
+
import type { Translate } from './market-data.ts'
|
|
23
|
+
import type { ConflictGroup, OperationRecord } from './operations.ts'
|
|
24
|
+
import { bucketOf, isSettled, needsUser, queuePosition, sortForPanel, summarize } from './operations.ts'
|
|
25
|
+
|
|
26
|
+
/** What the two clash outcomes are, in the order they are offered. */
|
|
27
|
+
const CHOICES = [
|
|
28
|
+
{ id: 'keep' as const, label: 'conflictKeep', note: 'conflictKeepNote' },
|
|
29
|
+
{ id: 'swap' as const, label: 'conflictSwap', note: 'conflictSwapNote' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How the panel renders a plugin it only knows by package name: the catalog
|
|
34
|
+
* (or the installed list) supplies the author and avatar a card would show.
|
|
35
|
+
*/
|
|
36
|
+
export type DescribePlugin = (name: string) => {
|
|
37
|
+
title: string
|
|
38
|
+
author?: string | undefined
|
|
39
|
+
avatar?: ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OperationsPanelProps {
|
|
43
|
+
t: Translate
|
|
44
|
+
/** Resolves a package name to the identity a card would show for it. */
|
|
45
|
+
describe: DescribePlugin
|
|
46
|
+
records: readonly OperationRecord[]
|
|
47
|
+
open: boolean
|
|
48
|
+
/** Owned by the parent: a card's "cannot install" marker raises the panel. */
|
|
49
|
+
onOpenChange: (open: boolean) => void
|
|
50
|
+
/** True while a swap is running, which disables both clash outcomes. */
|
|
51
|
+
replacing: boolean
|
|
52
|
+
/** Blocks the destructive outcome when pnpm is not usable yet. */
|
|
53
|
+
envReady: boolean
|
|
54
|
+
onClearSettled: () => void
|
|
55
|
+
onCancel: (record: OperationRecord) => void
|
|
56
|
+
onDismiss: (record: OperationRecord) => void
|
|
57
|
+
onRefresh: () => void
|
|
58
|
+
/** Resolve a clash: keep what is installed, or uninstall it and retry. */
|
|
59
|
+
onResolveConflict: (record: OperationRecord, choice: 'keep' | 'swap') => void
|
|
60
|
+
/** Retry an operation the host refused for a fixable reason. */
|
|
61
|
+
onRetry?: ((record: OperationRecord) => void) | undefined
|
|
62
|
+
/** Approve the build scripts pnpm blocked, then retry — replaces the plain
|
|
63
|
+
* retry on a record that failed for that reason, so the fix sits next to
|
|
64
|
+
* the sentence describing the problem. */
|
|
65
|
+
onApproveBuilds?: ((record: OperationRecord) => void) | undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What each plugin ends up as under the selected outcome.
|
|
70
|
+
*
|
|
71
|
+
* The consequence is drawn ON the plugins rather than described beside them:
|
|
72
|
+
* pick "keep what I have" and the installed rows tick while the candidate
|
|
73
|
+
* crosses out; pick the other and they swap. The candidate is in this list
|
|
74
|
+
* for the same reason — a decision about which plugin survives has to show
|
|
75
|
+
* what happens to the one being installed, not only to the others.
|
|
76
|
+
*/
|
|
77
|
+
function OutcomePreview(props: {
|
|
78
|
+
t: Translate
|
|
79
|
+
record: OperationRecord
|
|
80
|
+
choice: 'keep' | 'swap'
|
|
81
|
+
describe: DescribePlugin
|
|
82
|
+
}) {
|
|
83
|
+
const { t, record, choice } = props
|
|
84
|
+
const swap = choice === 'swap'
|
|
85
|
+
const candidate = props.describe(record.name)
|
|
86
|
+
const row = (key: string, info: ReturnType<DescribePlugin>, kept: boolean, tag: string) => (
|
|
87
|
+
<div key={key} className={kept ? css.rosterRow : `${css.rosterRow} ${css.rosterRowOut}`}>
|
|
88
|
+
{info.avatar}
|
|
89
|
+
<span className={css.rosterMain}>
|
|
90
|
+
<span className={css.rosterName} title={key}>{info.title}</span>
|
|
91
|
+
{info.author !== undefined && <span className={css.rosterAuthor}>{info.author}</span>}
|
|
92
|
+
</span>
|
|
93
|
+
<span className={kept ? `${css.rosterTag} ${css.rosterTagKeep}` : `${css.rosterTag} ${css.rosterTagDrop}`}>
|
|
94
|
+
{kept ? '✓' : '✕'} {tag}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
return (
|
|
99
|
+
<div className={css.roster}>
|
|
100
|
+
{row(record.name, candidate, swap, t(swap ? 'conflictOutcomeInstall' : 'conflictOutcomeSkip'))}
|
|
101
|
+
<div className={css.rosterSplit} />
|
|
102
|
+
{(record.conflicts ?? []).map(group =>
|
|
103
|
+
row(group.owner, props.describe(group.owner), !swap, t(swap ? 'conflictOutcomeRemove' : 'conflictOutcomeKeep')))}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The decision attached to a clash. Two outcomes rather than an error plus a
|
|
110
|
+
* destructive button: the default changes nothing, and selecting the other
|
|
111
|
+
* one is itself the consent step, so its cost is stated here.
|
|
112
|
+
*/
|
|
113
|
+
function ConflictChoice(props: {
|
|
114
|
+
t: Translate
|
|
115
|
+
record: OperationRecord
|
|
116
|
+
replacing: boolean
|
|
117
|
+
envReady: boolean
|
|
118
|
+
describe: DescribePlugin
|
|
119
|
+
onResolve: (choice: 'keep' | 'swap') => void
|
|
120
|
+
}) {
|
|
121
|
+
const { t, record, replacing, envReady } = props
|
|
122
|
+
const [choice, setChoice] = useState<'keep' | 'swap'>('keep')
|
|
123
|
+
const [whyOpen, setWhyOpen] = useState(false)
|
|
124
|
+
return (
|
|
125
|
+
<div className={css.opDecision}>
|
|
126
|
+
<p className={css.conflictBody}>{t('conflictBody')}</p>
|
|
127
|
+
<OutcomePreview t={t} record={record} choice={choice} describe={props.describe} />
|
|
128
|
+
{/* Native radios: the platform already draws the control, groups it by
|
|
129
|
+
`name`, and gives arrow-key navigation. A hand-rolled ring gets the
|
|
130
|
+
look wrong and the keyboard behaviour with it. */}
|
|
131
|
+
<div className={css.choices}>
|
|
132
|
+
{CHOICES.map(({ id, label, note }) => (
|
|
133
|
+
<label
|
|
134
|
+
key={id}
|
|
135
|
+
className={choice === id ? `${css.choice} ${css.choiceOn}` : css.choice}
|
|
136
|
+
>
|
|
137
|
+
<input
|
|
138
|
+
type="radio"
|
|
139
|
+
className={css.choiceRadio}
|
|
140
|
+
name={`dshm-conflict-${record.id}`}
|
|
141
|
+
checked={choice === id}
|
|
142
|
+
disabled={replacing}
|
|
143
|
+
onChange={() => setChoice(id)}
|
|
144
|
+
/>
|
|
145
|
+
<span className={css.choiceMain}>
|
|
146
|
+
<span className={css.choiceTitle}>{t(label)}</span>
|
|
147
|
+
{t(note) !== '' && <span className={css.choiceNote}>{t(note)}</span>}
|
|
148
|
+
</span>
|
|
149
|
+
</label>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
{/* Both ways out of the decision share one row: the quiet one that
|
|
153
|
+
reveals evidence, and the one that commits. */}
|
|
154
|
+
<div className={css.opDecisionFoot}>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
className={css.conflictDetailsToggle}
|
|
158
|
+
aria-expanded={whyOpen}
|
|
159
|
+
onClick={() => setWhyOpen(open => !open)}
|
|
160
|
+
>
|
|
161
|
+
{t('conflictDetails')}
|
|
162
|
+
{whyOpen ? <IconChevronUpOutline14 size={12} /> : <IconChevronDownOutline14 size={12} />}
|
|
163
|
+
</button>
|
|
164
|
+
<span className={css.grow} />
|
|
165
|
+
<Button
|
|
166
|
+
variant={choice === 'swap' ? 'outline' : 'primary'}
|
|
167
|
+
size="sm"
|
|
168
|
+
className={choice === 'swap' ? css.dangerBtn : undefined}
|
|
169
|
+
disabled={replacing || (choice === 'swap' && !envReady)}
|
|
170
|
+
onClick={() => props.onResolve(choice)}
|
|
171
|
+
>{replacing ? t('conflictReplacing') : t('confirm')}</Button>
|
|
172
|
+
</div>
|
|
173
|
+
{whyOpen && (
|
|
174
|
+
<div className={css.conflictWhy}>
|
|
175
|
+
{(record.conflicts ?? []).map(group => (
|
|
176
|
+
<div key={group.owner}>{group.owner}: {group.ids.join(', ')}</div>
|
|
177
|
+
))}
|
|
178
|
+
<div className={css.conflictWhyText}>{t('conflictWhy')}</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Icon for a record's visual bucket — three, not one per state. */
|
|
186
|
+
function BucketIcon(props: { record: OperationRecord }) {
|
|
187
|
+
const bucket = bucketOf(props.record.state)
|
|
188
|
+
if (bucket === 'busy') {
|
|
189
|
+
return props.record.state === 'running'
|
|
190
|
+
? <span className={css.spin}><IconLoadingOutline16 size={13} /></span>
|
|
191
|
+
: <span className={css.opQueuedIcon}>⋯</span>
|
|
192
|
+
}
|
|
193
|
+
if (bucket === 'ok') return <IconCheckOutline16 size={13} className={css.reassureOk} />
|
|
194
|
+
return <IconWarningOutline16 size={14} className={css.conflictIcon} />
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The one-line status under a record's name; the bucket carries the rest. */
|
|
198
|
+
function statusLine(t: Translate, record: OperationRecord, ahead: number | null): string {
|
|
199
|
+
switch (record.state) {
|
|
200
|
+
case 'queued':
|
|
201
|
+
return ahead === null || ahead === 0 ? t('opQueued') : `${t('opQueued')} · ${t('opQueuedAhead')} ${String(ahead)}`
|
|
202
|
+
case 'running':
|
|
203
|
+
return record.detail ?? t('opRunning')
|
|
204
|
+
case 'input':
|
|
205
|
+
return t('opNeedsChoice')
|
|
206
|
+
case 'failed':
|
|
207
|
+
return record.reason ?? t('installFail')
|
|
208
|
+
case 'warned':
|
|
209
|
+
return record.reason ?? t('opDone')
|
|
210
|
+
case 'done':
|
|
211
|
+
return record.needsRefresh === true ? t('opDoneRefresh') : t('opDone')
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function OperationsPanel(props: OperationsPanelProps) {
|
|
216
|
+
const { t, records, open } = props
|
|
217
|
+
const setOpen = props.onOpenChange
|
|
218
|
+
const wrapRef = useRef<HTMLDivElement | null>(null)
|
|
219
|
+
const summary = summarize(records)
|
|
220
|
+
const busy = summary.running + summary.queued > 0
|
|
221
|
+
|
|
222
|
+
// Dismissing a popover by pressing the control that opened it is the one
|
|
223
|
+
// route nobody looks for. Escape and an outside click are; the header also
|
|
224
|
+
// carries an explicit collapse for anyone who wants a target to aim at.
|
|
225
|
+
// The listener covers the whole wrapper, button included, so the button's
|
|
226
|
+
// own toggle is not undone by this closing first.
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!open) return undefined
|
|
229
|
+
const onKey = (event: KeyboardEvent) => { if (event.key === 'Escape') setOpen(false) }
|
|
230
|
+
const onPointer = (event: MouseEvent) => {
|
|
231
|
+
const wrap = wrapRef.current
|
|
232
|
+
if (wrap !== null && !wrap.contains(event.target as Node)) setOpen(false)
|
|
233
|
+
}
|
|
234
|
+
document.addEventListener('keydown', onKey)
|
|
235
|
+
document.addEventListener('mousedown', onPointer)
|
|
236
|
+
return () => {
|
|
237
|
+
document.removeEventListener('keydown', onKey)
|
|
238
|
+
document.removeEventListener('mousedown', onPointer)
|
|
239
|
+
}
|
|
240
|
+
}, [open, setOpen])
|
|
241
|
+
|
|
242
|
+
// The entry label is the batch, not a verb with no object: a bare "3" says
|
|
243
|
+
// nothing about what is happening to the profile.
|
|
244
|
+
const label = busy
|
|
245
|
+
? `${t('opInstalling')} ${String(summary.progressed)}/${String(summary.total)}`
|
|
246
|
+
: summary.attention > 0
|
|
247
|
+
? `${String(summary.attention)} ${t('opNeedsYou')}`
|
|
248
|
+
: t('opTitle')
|
|
249
|
+
|
|
250
|
+
// Always render inside .opWrap so the tab-row bottom margin is stable —
|
|
251
|
+
// the old idle-only <button> skipped the wrapper and jumped on first open.
|
|
252
|
+
const quiet = records.length === 0 && !open
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className={css.opWrap} ref={wrapRef}>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className={quiet
|
|
259
|
+
? `${css.opEntry} ${css.opEntryQuiet}`
|
|
260
|
+
: summary.attention > 0
|
|
261
|
+
? `${css.opEntry} ${css.opEntryAlert}`
|
|
262
|
+
: css.opEntry}
|
|
263
|
+
aria-expanded={open}
|
|
264
|
+
onClick={() => setOpen(quiet ? true : !open)}
|
|
265
|
+
>
|
|
266
|
+
{!quiet && busy && <span className={css.spin}><IconLoadingOutline16 size={12} /></span>}
|
|
267
|
+
{quiet ? t('opTitle') : label}
|
|
268
|
+
{!quiet && summary.attention > 0 && <span className={css.opDot} />}
|
|
269
|
+
</button>
|
|
270
|
+
{open && (
|
|
271
|
+
<div className={css.opPanel}>
|
|
272
|
+
<div className={css.opHead}>
|
|
273
|
+
<span className={css.opPanelTitle}>{t('opTitle')}</span>
|
|
274
|
+
<span className={css.grow} />
|
|
275
|
+
{summary.settled > 0 && (
|
|
276
|
+
<Button variant="ghost" size="sm" onClick={props.onClearSettled}>{t('opClear')}</Button>
|
|
277
|
+
)}
|
|
278
|
+
<Button
|
|
279
|
+
variant="ghost"
|
|
280
|
+
size="sm"
|
|
281
|
+
aria-label={t('opClose')}
|
|
282
|
+
title={t('opClose')}
|
|
283
|
+
className={css.opCloseBtn}
|
|
284
|
+
onClick={() => setOpen(false)}
|
|
285
|
+
><IconChevronUpOutline14 size={14} /></Button>
|
|
286
|
+
</div>
|
|
287
|
+
{busy && (
|
|
288
|
+
<div className={css.opAggregate}>
|
|
289
|
+
<div className={css.opAggregateTop}>
|
|
290
|
+
<span>{t('opInstalling')} {summary.progressed}/{summary.total}</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div className={css.bar}>
|
|
293
|
+
<div
|
|
294
|
+
className={css.barFill}
|
|
295
|
+
style={{ width: `${String(Math.round(summary.progressed / Math.max(1, summary.total) * 100))}%` }}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
{/* Long installs are the norm; saying so is what makes leaving
|
|
299
|
+
the page an option rather than a gamble. */}
|
|
300
|
+
<div className={css.opAggregateHint}>{t('opLeaveHint')}</div>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
{records.length === 0 && (
|
|
304
|
+
<div className={css.opEmpty}>
|
|
305
|
+
{t('opEmpty')}
|
|
306
|
+
<div className={css.opEmptyHint}>{t('opEmptyHint')}</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
{sortForPanel(records).map((record) => {
|
|
310
|
+
const ahead = queuePosition(records, record.id)
|
|
311
|
+
return (
|
|
312
|
+
<div key={record.id} className={needsUser(record) ? `${css.opRow} ${css.opRowAlert}` : css.opRow}>
|
|
313
|
+
<span className={css.opIcon}><BucketIcon record={record} /></span>
|
|
314
|
+
<div className={css.opMain}>
|
|
315
|
+
<div className={css.opTop}>
|
|
316
|
+
<span className={css.opVerb}>{t(`opKind_${record.kind}`)}</span>
|
|
317
|
+
<span className={css.opName} title={record.name}>{record.name}</span>
|
|
318
|
+
</div>
|
|
319
|
+
{record.state === 'running' && typeof record.percent === 'number' && (
|
|
320
|
+
<div className={css.bar}>
|
|
321
|
+
<div className={css.barFill} style={{ width: `${String(record.percent)}%` }} />
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
<div className={bucketOf(record.state) === 'attention' ? `${css.opStatus} ${css.opStatusBad}` : css.opStatus}>
|
|
325
|
+
{statusLine(t, record, ahead)}
|
|
326
|
+
</div>
|
|
327
|
+
{needsUser(record) && (
|
|
328
|
+
<ConflictChoice
|
|
329
|
+
t={t}
|
|
330
|
+
record={record}
|
|
331
|
+
replacing={props.replacing}
|
|
332
|
+
envReady={props.envReady}
|
|
333
|
+
describe={props.describe}
|
|
334
|
+
onResolve={choice => props.onResolveConflict(record, choice)}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
<div className={css.opActions}>
|
|
339
|
+
{record.state === 'running' && (
|
|
340
|
+
<Button variant="outline" size="sm" onClick={() => props.onCancel(record)}>{t('cancelOp')}</Button>
|
|
341
|
+
)}
|
|
342
|
+
{record.state === 'queued' && (
|
|
343
|
+
<Button variant="ghost" size="sm" onClick={() => props.onDismiss(record)}>{t('opDequeue')}</Button>
|
|
344
|
+
)}
|
|
345
|
+
{record.state === 'done' && record.needsRefresh === true && (
|
|
346
|
+
<Button variant="primary" size="sm" onClick={props.onRefresh}>{t('refresh')}</Button>
|
|
347
|
+
)}
|
|
348
|
+
{record.state === 'failed' && (record.blockedBuilds ?? []).length > 0 && props.onApproveBuilds !== undefined && (
|
|
349
|
+
<Button variant="primary" size="sm" onClick={() => props.onApproveBuilds?.(record)}>{t('approveBuilds')}</Button>
|
|
350
|
+
)}
|
|
351
|
+
{record.state === 'failed' && (record.blockedBuilds ?? []).length === 0 && props.onRetry !== undefined && (
|
|
352
|
+
<Button variant="outline" size="sm" onClick={() => props.onRetry?.(record)}>{t('opRetry')}</Button>
|
|
353
|
+
)}
|
|
354
|
+
{isSettled(record) && (
|
|
355
|
+
<Button variant="ghost" size="sm" onClick={() => props.onDismiss(record)}>{t('dismissNotice')}</Button>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
})}
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
}
|