dsh-plugin-container 0.0.0-stage → 0.3.0
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 +80 -2
- package/cordis.patch.yml +3 -0
- package/docs/ai-chat.png +0 -0
- package/docs/ai-new-session.png +0 -0
- package/docs/apple-container.png +0 -0
- package/docs/attach.png +0 -0
- package/docs/docker-console.png +0 -0
- package/docs/docker-overview.png +0 -0
- package/docs/docker-volumes.png +0 -0
- package/docs/image-architecture.png +0 -0
- package/docs/package-install.png +0 -0
- package/docs/provider-switch.png +0 -0
- package/docs/run-container.png +0 -0
- package/index.js +138 -0
- package/lib/client.js +3560 -0
- package/package.json +62 -4
- package/src/client/api.js +98 -0
- package/src/client/attach.jsx +134 -0
- package/src/client/components.jsx +320 -0
- package/src/client/hooks.js +161 -0
- package/src/client/i18n.js +502 -0
- package/src/client/icons.jsx +227 -0
- package/src/client/index.jsx +76 -0
- package/src/client/page.jsx +782 -0
- package/src/client/run-container.jsx +187 -0
- package/src/client/runs.js +182 -0
- package/src/client/store.js +91 -0
- package/src/client/styles.js +371 -0
- package/src/client/tabs/compose.jsx +94 -0
- package/src/client/tabs/console.jsx +211 -0
- package/src/client/tabs/containers.jsx +148 -0
- package/src/client/tabs/images.jsx +127 -0
- package/src/client/tabs/networks.jsx +91 -0
- package/src/client/tabs/overview.jsx +266 -0
- package/src/client/tabs/volumes.jsx +76 -0
- package/src/host/apple-cli.js +307 -0
- package/src/host/apple.js +471 -0
- package/src/host/attach.js +246 -0
- package/src/host/commands.js +235 -0
- package/src/host/docker-cli.js +405 -0
- package/src/host/jobs.js +245 -0
- package/src/host/parsers.js +484 -0
- package/src/host/runtime.js +285 -0
- package/src/host/service.js +836 -0
- package/src/host/tools.js +198 -0
- package/src/host/util.js +97 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Command terminal: shell-free docker argv, live output, history, cancel. */
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
CopyButton,
|
|
7
|
+
Spinner,
|
|
8
|
+
cx,
|
|
9
|
+
formatDuration,
|
|
10
|
+
formatTime,
|
|
11
|
+
} from '../components.jsx'
|
|
12
|
+
import { IconTerminal, IconX } from '../icons.jsx'
|
|
13
|
+
import { readHistory, writeHistory } from '../store.js'
|
|
14
|
+
|
|
15
|
+
const QUICK_DOCKER = ['ps -a', 'images', 'volume ls', 'network ls', 'compose ls', 'system df', 'info', 'version']
|
|
16
|
+
const QUICK_APPLE = ['ls -a', 'image ls', 'volume ls', 'network ls', 'system df', 'system version']
|
|
17
|
+
const SENSITIVE = /(?:^|\s)(login|logout|--password(?:-stdin)?|--token|--registry-token)(?:\s|=|$)/u
|
|
18
|
+
|
|
19
|
+
function statusLabel(t, status) {
|
|
20
|
+
if (status === 'running') return t('commandRunning')
|
|
21
|
+
if (status === 'cancelling') return t('stopping')
|
|
22
|
+
if (status === 'cancelled') return t('cancelled')
|
|
23
|
+
if (status === 'timed-out') return t('timedOut')
|
|
24
|
+
if (status === 'failed') return t('failed')
|
|
25
|
+
return t('commandFinished')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function ConsoleTab({ t, provider, runs, runState, context, status, cwd, onCwdChange, draft, setDraft, onRun }) {
|
|
29
|
+
const [history, setHistory] = useState(() => readHistory())
|
|
30
|
+
const [historyIndex, setHistoryIndex] = useState(-1)
|
|
31
|
+
const [now, setNow] = useState(() => Date.now())
|
|
32
|
+
const [stick, setStick] = useState(true)
|
|
33
|
+
const preRef = useRef(null)
|
|
34
|
+
const run = runState?.run ?? null
|
|
35
|
+
const active = run !== null && (run.status === 'running' || run.status === 'cancelling')
|
|
36
|
+
const output = runState?.stdout ?? ''
|
|
37
|
+
const stderr = runState?.stderr ?? ''
|
|
38
|
+
const combined = stderr === '' ? output : `${output}${output.endsWith('\n') || output === '' ? '' : '\n'}\n[stderr]\n${stderr}`
|
|
39
|
+
const hasOutput = combined.trim() !== ''
|
|
40
|
+
const runId = run?.id
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!active) return undefined
|
|
44
|
+
const timer = setInterval(() => setNow(Date.now()), 1000)
|
|
45
|
+
return () => clearInterval(timer)
|
|
46
|
+
}, [active])
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (runId !== undefined) setStick(true)
|
|
50
|
+
}, [runId])
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!stick) return
|
|
54
|
+
const element = preRef.current
|
|
55
|
+
if (element !== null) element.scrollTop = element.scrollHeight
|
|
56
|
+
}, [combined, stick, run?.status])
|
|
57
|
+
|
|
58
|
+
const addHistory = (command) => {
|
|
59
|
+
if (SENSITIVE.test(command)) return
|
|
60
|
+
setHistory((current) => {
|
|
61
|
+
const next = [command, ...current.filter((entry) => entry !== command)].slice(0, 50)
|
|
62
|
+
writeHistory(next)
|
|
63
|
+
return next
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const submit = async (event) => {
|
|
68
|
+
event?.preventDefault()
|
|
69
|
+
const command = draft.trim()
|
|
70
|
+
if (command === '' || active) return
|
|
71
|
+
addHistory(command)
|
|
72
|
+
setHistoryIndex(-1)
|
|
73
|
+
await onRun({
|
|
74
|
+
command,
|
|
75
|
+
context: context ?? undefined,
|
|
76
|
+
cwd: cwd.trim() === '' ? undefined : cwd.trim(),
|
|
77
|
+
title: provider === 'apple-container' ? 'container' : 'docker',
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const navigateHistory = (event) => {
|
|
82
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
|
|
83
|
+
if (history.length === 0) return
|
|
84
|
+
event.preventDefault()
|
|
85
|
+
if (event.key === 'ArrowUp') {
|
|
86
|
+
const next = historyIndex < 0 ? 0 : Math.min(historyIndex + 1, history.length - 1)
|
|
87
|
+
setHistoryIndex(next)
|
|
88
|
+
setDraft(history[next])
|
|
89
|
+
} else {
|
|
90
|
+
const next = historyIndex - 1
|
|
91
|
+
if (next < 0) {
|
|
92
|
+
setHistoryIndex(-1)
|
|
93
|
+
setDraft('')
|
|
94
|
+
} else {
|
|
95
|
+
setHistoryIndex(next)
|
|
96
|
+
setDraft(history[next])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const elapsed = run?.startedAt !== undefined ? (run.endedAt ?? (active ? now : run.endedAt)) - run.startedAt : undefined
|
|
102
|
+
const isApple = provider === 'apple-container'
|
|
103
|
+
const commandPrefix = isApple ? 'container' : 'docker'
|
|
104
|
+
const quick = useMemo(() => (isApple ? QUICK_APPLE : QUICK_DOCKER), [isApple])
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="dsh-docker-console">
|
|
108
|
+
<form className="dsh-docker-console-form" onSubmit={submit}>
|
|
109
|
+
<div className="dsh-docker-console-row">
|
|
110
|
+
<span className="dsh-docker-console-prompt">$</span>
|
|
111
|
+
<input
|
|
112
|
+
className="dsh-docker-input dsh-docker-console-input"
|
|
113
|
+
value={draft}
|
|
114
|
+
onChange={(event) => {
|
|
115
|
+
setDraft(event.target.value)
|
|
116
|
+
setHistoryIndex(-1)
|
|
117
|
+
}}
|
|
118
|
+
onKeyDown={navigateHistory}
|
|
119
|
+
placeholder={isApple ? t('appleCommandPlaceholder') : t('commandPlaceholder')}
|
|
120
|
+
aria-label={isApple ? t('appleCommandPlaceholder') : t('commandPlaceholder')}
|
|
121
|
+
spellCheck="false"
|
|
122
|
+
autoCapitalize="off"
|
|
123
|
+
autoCorrect="off"
|
|
124
|
+
/>
|
|
125
|
+
{active ? (
|
|
126
|
+
<Button type="button" variant="danger" onClick={() => runs.cancel()}><IconX size={13} /> {t('cancel')}</Button>
|
|
127
|
+
) : (
|
|
128
|
+
<Button type="submit" variant="primary" disabled={draft.trim() === ''}><IconTerminal size={13} /> {t('run')}</Button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="dsh-docker-console-meta">
|
|
132
|
+
<span className="dsh-docker-console-hint">{isApple ? t('providerLabel') : t('contextLabel')}: <span className="dsh-docker-mono">{isApple ? t('providerApple') : context ?? t('noContext')}</span></span>
|
|
133
|
+
<span className="dsh-docker-console-hint">·</span>
|
|
134
|
+
<span className="dsh-docker-console-hint">{t('cwd')}</span>
|
|
135
|
+
<input
|
|
136
|
+
className="dsh-docker-input dsh-docker-console-cwd"
|
|
137
|
+
value={cwd}
|
|
138
|
+
onChange={(event) => onCwdChange(event.target.value)}
|
|
139
|
+
placeholder={t('cwdPlaceholder')}
|
|
140
|
+
aria-label={t('cwd')}
|
|
141
|
+
spellCheck="false"
|
|
142
|
+
/>
|
|
143
|
+
<span className="dsh-docker-console-hint dsh-docker-truncate">{t('shellHint')}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="dsh-docker-chip-row" style={{ alignItems: 'center' }}>
|
|
146
|
+
<span className="dsh-docker-console-hint">{t('quickCommands')}</span>
|
|
147
|
+
{quick.map((command) => (
|
|
148
|
+
<button key={command} type="button" className="dsh-docker-quick" onClick={() => setDraft(`${commandPrefix} ${command}`)}>
|
|
149
|
+
{command}
|
|
150
|
+
</button>
|
|
151
|
+
))}
|
|
152
|
+
{history.length > 0 && (
|
|
153
|
+
<>
|
|
154
|
+
<span className="dsh-docker-console-hint" style={{ marginLeft: 4 }}>{t('history')}</span>
|
|
155
|
+
{history.slice(0, 4).map((command, index) => (
|
|
156
|
+
<button key={`${command}-${index}`} type="button" className="dsh-docker-quick" title={command} onClick={() => setDraft(command)}>
|
|
157
|
+
{command.length > 28 ? `${command.slice(0, 28)}…` : command}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
<Button size="sm" variant="ghost" onClick={() => { setHistory([]); writeHistory([]) }}>{t('resetHistory')}</Button>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</form>
|
|
165
|
+
|
|
166
|
+
<div className="dsh-docker-console-output">
|
|
167
|
+
<div className="dsh-docker-output-head">
|
|
168
|
+
<div className="dsh-docker-output-title">
|
|
169
|
+
<IconTerminal size={14} />
|
|
170
|
+
{run !== null ? <span className="dsh-docker-output-cmd" title={run.displayCommand}>{run.displayCommand}</span> : <span>{t('terminal')}</span>}
|
|
171
|
+
</div>
|
|
172
|
+
<div className="dsh-docker-output-actions">
|
|
173
|
+
{runState?.truncated === true && <span className="dsh-docker-chip">{t('truncation')}</span>}
|
|
174
|
+
<CopyButton text={combined} label={t('copy')} copiedLabel={t('copied')} className="dsh-docker-btn dsh-docker-sm" />
|
|
175
|
+
<Button size="sm" onClick={() => runs.reset()} disabled={run === null && !hasOutput}>{t('clear')}</Button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
{runState?.error !== undefined && runState?.error !== null && runState.error !== '' ? (
|
|
179
|
+
<pre className="dsh-docker-output-pre" style={{ color: 'var(--dsw-alias-label-error)' }}>{runState.error}</pre>
|
|
180
|
+
) : (
|
|
181
|
+
<pre
|
|
182
|
+
ref={preRef}
|
|
183
|
+
className={cx('dsh-docker-output-pre', !hasOutput && 'dsh-docker-no-output')}
|
|
184
|
+
onScroll={() => {
|
|
185
|
+
const element = preRef.current
|
|
186
|
+
if (element === null) return
|
|
187
|
+
setStick(element.scrollHeight - element.scrollTop - element.clientHeight < 28)
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{hasOutput ? combined : t('noOutput')}
|
|
191
|
+
</pre>
|
|
192
|
+
)}
|
|
193
|
+
<div className="dsh-docker-run-status" data-status={run?.status ?? 'idle'}>
|
|
194
|
+
<span className="dsh-docker-dot" />
|
|
195
|
+
{run === null ? <span>{t('noActiveRun')}</span> : (
|
|
196
|
+
<>
|
|
197
|
+
<span>{statusLabel(t, run.status)}</span>
|
|
198
|
+
{run.exitCode !== null && run.exitCode !== undefined && <span>{t('exitCode')}: <span className="dsh-docker-mono">{run.exitCode}</span></span>}
|
|
199
|
+
{elapsed !== undefined && <span>{t('elapsed')}: <span className="dsh-docker-mono">{formatDuration(Math.max(0, elapsed))}</span></span>}
|
|
200
|
+
<span>{isApple ? t('providerLabel') : t('contextLabel')}: <span className="dsh-docker-mono">{isApple ? t('providerApple') : run.context ?? context ?? '—'}</span></span>
|
|
201
|
+
<span className="dsh-docker-muted">{formatTime(run.startedAt)}</span>
|
|
202
|
+
{active && <Spinner size={12} />}
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
<span style={{ flex: 1 }} />
|
|
206
|
+
<span className="dsh-docker-muted dsh-docker-mono">{isApple ? status?.runtime?.details?.status ?? '' : status?.context?.endpoint ?? ''}</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Containers tab with search, state filter, and per-row actions. */
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Chips,
|
|
7
|
+
EmptyState,
|
|
8
|
+
ErrorState,
|
|
9
|
+
IconButton,
|
|
10
|
+
LoadingSkeleton,
|
|
11
|
+
SearchInput,
|
|
12
|
+
StateLabel,
|
|
13
|
+
} from '../components.jsx'
|
|
14
|
+
import { useDockerList } from '../hooks.js'
|
|
15
|
+
import { IconAlert, IconAttach, IconBox, IconExec, IconInfo, IconPlay, IconRestart, IconSparkles, IconStop, IconTerminal, IconTrash } from '../icons.jsx'
|
|
16
|
+
|
|
17
|
+
export default function ContainersTab({ t, api, provider, context, status, refreshToken, features, onAction, onAttach, onRunContainer, onAskAI, canAskAI, onInspect }) {
|
|
18
|
+
const [all, setAll] = useState(true)
|
|
19
|
+
const [search, setSearch] = useState('')
|
|
20
|
+
const enabled = status?.daemon?.state === 'running'
|
|
21
|
+
const list = useDockerList(api, provider, 'containers', context, { all, refreshToken, enabled })
|
|
22
|
+
const items = list.data?.items ?? []
|
|
23
|
+
const filtered = useMemo(() => {
|
|
24
|
+
const needle = search.trim().toLowerCase()
|
|
25
|
+
if (needle === '') return items
|
|
26
|
+
return items.filter((item) =>
|
|
27
|
+
[item.name, item.image, item.id, item.status, item.ports.join(' ')]
|
|
28
|
+
.join(' ')
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.includes(needle),
|
|
31
|
+
)
|
|
32
|
+
}, [items, search])
|
|
33
|
+
|
|
34
|
+
const act = (action, item, focusConsole = false, follow = false) => {
|
|
35
|
+
onAction({ kind: 'container', action, id: item.id, follow }, { focusConsole })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="dsh-docker-stack">
|
|
40
|
+
<div className="dsh-docker-toolbar">
|
|
41
|
+
<div className="dsh-docker-toolbar-left">
|
|
42
|
+
<SearchInput value={search} onChange={setSearch} placeholder={t('search')} label={t('search')} />
|
|
43
|
+
<div className="dsh-docker-seg" role="group" aria-label={t('status')}>
|
|
44
|
+
<button type="button" aria-pressed={all} onClick={() => setAll(true)}>{t('all')}</button>
|
|
45
|
+
<button type="button" aria-pressed={!all} onClick={() => setAll(false)}>{t('running')}</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="dsh-docker-toolbar-right">
|
|
49
|
+
<span className="dsh-docker-muted">{filtered.length} / {items.length}</span>
|
|
50
|
+
{features.includes('run') ? <Button size="sm" variant="primary" onClick={() => onRunContainer()}>{t('runContainer')}</Button> : <span className="dsh-docker-muted">{t('restartFeatures')}</span>}
|
|
51
|
+
<Button size="sm" onClick={list.reload} disabled={list.loading}>{t('refresh')}</Button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{!enabled && items.length === 0 ? (
|
|
56
|
+
<EmptyState icon={<IconAlert size={20} />} title={provider === 'apple-container' ? t('appleSystemStopped') : t('daemonStopped')} text={provider === 'apple-container' ? t('appleSystemStoppedBody') : t('daemonStoppedBody')} />
|
|
57
|
+
) : list.loading && list.data === null ? (
|
|
58
|
+
<LoadingSkeleton rows={6} />
|
|
59
|
+
) : list.error !== null && list.data === null ? (
|
|
60
|
+
<ErrorState title={t('listFailed')} text={list.error.message} retryLabel={t('retry')} onRetry={list.reload} />
|
|
61
|
+
) : filtered.length === 0 ? (
|
|
62
|
+
<EmptyState icon={<IconBox size={20} />} title={search === '' ? t('emptyContainers') : t('emptySearch')} text={search === '' ? undefined : search} />
|
|
63
|
+
) : (
|
|
64
|
+
<section className="dsh-docker-card">
|
|
65
|
+
<div className="dsh-docker-table-wrap">
|
|
66
|
+
<table className="dsh-docker-table">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>{t('name')}</th>
|
|
70
|
+
<th>{t('image')}</th>
|
|
71
|
+
<th>{t('status')}</th>
|
|
72
|
+
<th>{t('ports')}</th>
|
|
73
|
+
<th>{t('created')}</th>
|
|
74
|
+
<th className="dsh-docker-col-actions">{t('actions')}</th>
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
{filtered.map((item) => (
|
|
79
|
+
<tr key={item.id}>
|
|
80
|
+
<td>
|
|
81
|
+
<button type="button" className="dsh-docker-row-title" style={{ border: 0, padding: 0, background: 'none', cursor: 'pointer', textAlign: 'left' }} onClick={() => onInspect('container', item.id)}>
|
|
82
|
+
{item.name || item.id}
|
|
83
|
+
</button>
|
|
84
|
+
{item.id !== item.name && <div className="dsh-docker-mono dsh-docker-muted">{item.id}</div>}
|
|
85
|
+
</td>
|
|
86
|
+
<td className="dsh-docker-mono dsh-docker-truncate" title={item.image}>{item.image}</td>
|
|
87
|
+
<td><StateLabel state={item.state} t={t} /></td>
|
|
88
|
+
<td><Chips values={item.ports.slice(0, 2)} mono /></td>
|
|
89
|
+
<td className="dsh-docker-muted">{item.runningFor || item.createdAt || '—'}</td>
|
|
90
|
+
<td className="dsh-docker-col-actions">
|
|
91
|
+
<span className="dsh-docker-row-actions">
|
|
92
|
+
{item.state === 'running' ? (
|
|
93
|
+
<IconButton size="sm" label={t('stopContainer')} onClick={() => act('stop', item)}>
|
|
94
|
+
<IconStop size={14} />
|
|
95
|
+
</IconButton>
|
|
96
|
+
) : (
|
|
97
|
+
<IconButton size="sm" label={t('startContainer')} onClick={() => act('start', item)}>
|
|
98
|
+
<IconPlay size={14} />
|
|
99
|
+
</IconButton>
|
|
100
|
+
)}
|
|
101
|
+
{provider === 'docker' && (
|
|
102
|
+
<IconButton size="sm" label={t('restartContainer')} onClick={() => act('restart', item)}>
|
|
103
|
+
<IconRestart size={14} />
|
|
104
|
+
</IconButton>
|
|
105
|
+
)}
|
|
106
|
+
{canAskAI && (
|
|
107
|
+
<IconButton
|
|
108
|
+
size="sm"
|
|
109
|
+
label={item.state === 'running' ? t('askAi') : t('diagnose')}
|
|
110
|
+
onClick={() => onAskAI(item, item.state === 'running' ? 'ask' : 'diagnose')}
|
|
111
|
+
>
|
|
112
|
+
<IconSparkles size={14} />
|
|
113
|
+
</IconButton>
|
|
114
|
+
)}
|
|
115
|
+
{features.includes('exec') && item.state === 'running' && (
|
|
116
|
+
<IconButton size="sm" label={t('exec')} onClick={() => onAttach(item, 'exec')}>
|
|
117
|
+
<IconExec size={14} />
|
|
118
|
+
</IconButton>
|
|
119
|
+
)}
|
|
120
|
+
{features.includes('attach') && item.state === 'running' && (
|
|
121
|
+
<IconButton size="sm" label={t('attach')} onClick={() => onAttach(item)}>
|
|
122
|
+
<IconAttach size={14} />
|
|
123
|
+
</IconButton>
|
|
124
|
+
)}
|
|
125
|
+
<IconButton size="sm" label={t('logs')} onClick={() => act('logs', item, true)}>
|
|
126
|
+
<IconTerminal size={14} />
|
|
127
|
+
</IconButton>
|
|
128
|
+
<IconButton size="sm" label={t('followLogs')} onClick={() => act('logs', item, true, true)}>
|
|
129
|
+
<IconTerminal size={14} style={{ transform: 'scaleX(-1)' }} />
|
|
130
|
+
</IconButton>
|
|
131
|
+
<IconButton size="sm" label={t('inspect')} onClick={() => onInspect('container', item.id)}>
|
|
132
|
+
<IconInfo size={14} />
|
|
133
|
+
</IconButton>
|
|
134
|
+
<IconButton size="sm" label={t('remove')} onClick={() => act('delete', item)}>
|
|
135
|
+
<IconTrash size={14} />
|
|
136
|
+
</IconButton>
|
|
137
|
+
</span>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
140
|
+
))}
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Images tab with search, inspect/remove actions, and a pull form. */
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
EmptyState,
|
|
7
|
+
ErrorState,
|
|
8
|
+
IconButton,
|
|
9
|
+
LoadingSkeleton,
|
|
10
|
+
SearchInput,
|
|
11
|
+
displayValue,
|
|
12
|
+
shortId,
|
|
13
|
+
} from '../components.jsx'
|
|
14
|
+
import { useDockerList } from '../hooks.js'
|
|
15
|
+
import { IconAlert, IconImage, IconInfo, IconPlay, IconTrash, IconDownload } from '../icons.jsx'
|
|
16
|
+
|
|
17
|
+
const IMAGE_REF = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]*$/u
|
|
18
|
+
|
|
19
|
+
export default function ImagesTab({ t, api, provider, context, status, refreshToken, features = [], onAction, onRunContainer, onInspect, onNotify }) {
|
|
20
|
+
const showArchitecture = features.includes('image-architecture')
|
|
21
|
+
const [search, setSearch] = useState('')
|
|
22
|
+
const [pull, setPull] = useState('')
|
|
23
|
+
const enabled = status?.daemon?.state === 'running'
|
|
24
|
+
const list = useDockerList(api, provider, 'images', context, { refreshToken, enabled })
|
|
25
|
+
const items = list.data?.items ?? []
|
|
26
|
+
const filtered = useMemo(() => {
|
|
27
|
+
const needle = search.trim().toLowerCase()
|
|
28
|
+
if (needle === '') return items
|
|
29
|
+
return items.filter((item) => `${item.name} ${item.id} ${item.repository} ${item.tag}`.toLowerCase().includes(needle))
|
|
30
|
+
}, [items, search])
|
|
31
|
+
|
|
32
|
+
const submitPull = (event) => {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
const reference = pull.trim()
|
|
35
|
+
if (!IMAGE_REF.test(reference)) {
|
|
36
|
+
onNotify('error', t('pullInvalid'))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
setPull('')
|
|
40
|
+
onAction({ kind: 'image', action: 'pull', id: reference }, { focusConsole: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="dsh-docker-stack">
|
|
45
|
+
<div className="dsh-docker-toolbar">
|
|
46
|
+
<div className="dsh-docker-toolbar-left">
|
|
47
|
+
<SearchInput value={search} onChange={setSearch} placeholder={t('search')} label={t('search')} />
|
|
48
|
+
<form onSubmit={submitPull} className="dsh-docker-toolbar-left" style={{ gap: 6 }}>
|
|
49
|
+
<input
|
|
50
|
+
className="dsh-docker-input"
|
|
51
|
+
style={{ width: 200 }}
|
|
52
|
+
value={pull}
|
|
53
|
+
onChange={(event) => setPull(event.target.value)}
|
|
54
|
+
placeholder={t('pullPlaceholder')}
|
|
55
|
+
aria-label={t('pullPlaceholder')}
|
|
56
|
+
spellCheck="false"
|
|
57
|
+
/>
|
|
58
|
+
<Button size="sm" type="submit" disabled={pull.trim() === ''}><IconDownload size={13} /> {t('pull')}</Button>
|
|
59
|
+
</form>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="dsh-docker-toolbar-right">
|
|
62
|
+
<span className="dsh-docker-muted">{filtered.length} / {items.length}</span>
|
|
63
|
+
{!showArchitecture && <span className="dsh-docker-muted">{t('restartForArchitecture')}</span>}
|
|
64
|
+
<Button size="sm" onClick={list.reload} disabled={list.loading}>{t('refresh')}</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{!enabled && items.length === 0 ? (
|
|
69
|
+
<EmptyState icon={<IconAlert size={20} />} title={provider === 'apple-container' ? t('appleSystemStopped') : t('daemonStopped')} text={provider === 'apple-container' ? t('appleSystemStoppedBody') : t('daemonStoppedBody')} />
|
|
70
|
+
) : list.loading && list.data === null ? (
|
|
71
|
+
<LoadingSkeleton rows={6} />
|
|
72
|
+
) : list.error !== null && list.data === null ? (
|
|
73
|
+
<ErrorState title={t('listFailed')} text={list.error.message} retryLabel={t('retry')} onRetry={list.reload} />
|
|
74
|
+
) : filtered.length === 0 ? (
|
|
75
|
+
<EmptyState icon={<IconImage size={20} />} title={search === '' ? t('emptyImages') : t('emptySearch')} text={search === '' ? t('pullPlaceholder') : search} />
|
|
76
|
+
) : (
|
|
77
|
+
<section className="dsh-docker-card">
|
|
78
|
+
<div className="dsh-docker-table-wrap">
|
|
79
|
+
<table className="dsh-docker-table">
|
|
80
|
+
<thead>
|
|
81
|
+
<tr>
|
|
82
|
+
<th>{t('repository')}</th>
|
|
83
|
+
<th>{t('tag')}</th>
|
|
84
|
+
<th>{t('id')}</th>
|
|
85
|
+
{showArchitecture && <th>{t('architecture')}</th>}
|
|
86
|
+
<th className="dsh-docker-col-num">{t('size')}</th>
|
|
87
|
+
<th>{t('createdSince')}</th>
|
|
88
|
+
<th className="dsh-docker-col-actions">{t('actions')}</th>
|
|
89
|
+
</tr>
|
|
90
|
+
</thead>
|
|
91
|
+
<tbody>
|
|
92
|
+
{filtered.map((item) => {
|
|
93
|
+
const inspectId = provider === 'apple-container' ? item.name : item.id
|
|
94
|
+
return (
|
|
95
|
+
<tr key={`${item.id}-${item.name}`}>
|
|
96
|
+
<td className="dsh-docker-mono dsh-docker-truncate" title={item.repository}>{item.repository}</td>
|
|
97
|
+
<td className="dsh-docker-mono">{displayValue(item.tag)}</td>
|
|
98
|
+
<td className="dsh-docker-mono dsh-docker-muted" title={item.id}>{shortId(item.id)}</td>
|
|
99
|
+
{showArchitecture && <td className="dsh-docker-mono">{displayValue(item.architecture)}</td>}
|
|
100
|
+
<td className="dsh-docker-col-num">{displayValue(item.size)}</td>
|
|
101
|
+
<td className="dsh-docker-muted">{displayValue(item.createdSince)}</td>
|
|
102
|
+
<td className="dsh-docker-col-actions">
|
|
103
|
+
<span className="dsh-docker-row-actions">
|
|
104
|
+
{features.includes('run') && item.name !== '<none>' && (
|
|
105
|
+
<IconButton size="sm" label={t('runFromImage')} onClick={() => onRunContainer(item.name)}>
|
|
106
|
+
<IconPlay size={14} />
|
|
107
|
+
</IconButton>
|
|
108
|
+
)}
|
|
109
|
+
<IconButton size="sm" label={t('inspect')} onClick={() => onInspect('image', inspectId)}>
|
|
110
|
+
<IconInfo size={14} />
|
|
111
|
+
</IconButton>
|
|
112
|
+
<IconButton size="sm" label={t('removeImage')} onClick={() => onAction({ kind: 'image', action: 'delete', id: inspectId })}>
|
|
113
|
+
<IconTrash size={14} />
|
|
114
|
+
</IconButton>
|
|
115
|
+
</span>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Networks tab. */
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button, EmptyState, ErrorState, IconButton, LoadingSkeleton, SearchInput, displayValue } from '../components.jsx'
|
|
5
|
+
import { useDockerList } from '../hooks.js'
|
|
6
|
+
import { IconAlert, IconInfo, IconNetwork, IconTrash } from '../icons.jsx'
|
|
7
|
+
|
|
8
|
+
const BUILTIN = new Set(['bridge', 'host', 'none'])
|
|
9
|
+
|
|
10
|
+
export default function NetworksTab({ t, api, provider, context, status, refreshToken, onAction, onInspect }) {
|
|
11
|
+
const [search, setSearch] = useState('')
|
|
12
|
+
const enabled = status?.daemon?.state === 'running'
|
|
13
|
+
const list = useDockerList(api, provider, 'networks', context, { refreshToken, enabled })
|
|
14
|
+
const items = list.data?.items ?? []
|
|
15
|
+
const builtin = provider === 'apple-container' ? new Set(['default']) : BUILTIN
|
|
16
|
+
const filtered = useMemo(() => {
|
|
17
|
+
const needle = search.trim().toLowerCase()
|
|
18
|
+
return needle === '' ? items : items.filter((item) => `${item.name} ${item.driver} ${item.scope} ${item.id}`.toLowerCase().includes(needle))
|
|
19
|
+
}, [items, search])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="dsh-docker-stack">
|
|
23
|
+
<div className="dsh-docker-toolbar">
|
|
24
|
+
<div className="dsh-docker-toolbar-left">
|
|
25
|
+
<SearchInput value={search} onChange={setSearch} placeholder={t('search')} label={t('search')} />
|
|
26
|
+
</div>
|
|
27
|
+
<div className="dsh-docker-toolbar-right">
|
|
28
|
+
<span className="dsh-docker-muted">{filtered.length} / {items.length}</span>
|
|
29
|
+
<Button size="sm" onClick={list.reload} disabled={list.loading}>{t('refresh')}</Button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
{!enabled && items.length === 0 ? (
|
|
33
|
+
<EmptyState icon={<IconAlert size={20} />} title={provider === 'apple-container' ? t('appleSystemStopped') : t('daemonStopped')} text={provider === 'apple-container' ? t('appleSystemStoppedBody') : t('daemonStoppedBody')} />
|
|
34
|
+
) : list.loading && list.data === null ? (
|
|
35
|
+
<LoadingSkeleton rows={5} />
|
|
36
|
+
) : list.error !== null && list.data === null ? (
|
|
37
|
+
<ErrorState title={t('listFailed')} text={list.error.message} retryLabel={t('retry')} onRetry={list.reload} />
|
|
38
|
+
) : filtered.length === 0 ? (
|
|
39
|
+
<EmptyState icon={<IconNetwork size={20} />} title={search === '' ? t('emptyNetworks') : t('emptySearch')} text={search === '' ? undefined : search} />
|
|
40
|
+
) : (
|
|
41
|
+
<section className="dsh-docker-card">
|
|
42
|
+
<div className="dsh-docker-table-wrap">
|
|
43
|
+
<table className="dsh-docker-table">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>{t('name')}</th>
|
|
47
|
+
<th>{t('driver')}</th>
|
|
48
|
+
<th>{t('scope')}</th>
|
|
49
|
+
<th>IPv4</th>
|
|
50
|
+
<th>IPv6</th>
|
|
51
|
+
<th>{t('internal')}</th>
|
|
52
|
+
<th className="dsh-docker-col-actions">{t('actions')}</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{filtered.map((item) => (
|
|
57
|
+
<tr key={item.id}>
|
|
58
|
+
<td>
|
|
59
|
+
<div className="dsh-docker-row-title">{item.name}</div>
|
|
60
|
+
<div className="dsh-docker-mono dsh-docker-muted">{item.id}</div>
|
|
61
|
+
</td>
|
|
62
|
+
<td>{displayValue(item.driver)}</td>
|
|
63
|
+
<td>{displayValue(item.scope)}</td>
|
|
64
|
+
<td>{displayValue(item.ipv4)}</td>
|
|
65
|
+
<td>{displayValue(item.ipv6)}</td>
|
|
66
|
+
<td>{displayValue(item.internal)}</td>
|
|
67
|
+
<td className="dsh-docker-col-actions">
|
|
68
|
+
<span className="dsh-docker-row-actions">
|
|
69
|
+
<IconButton size="sm" label={t('inspect')} onClick={() => onInspect('network', item.id)}>
|
|
70
|
+
<IconInfo size={14} />
|
|
71
|
+
</IconButton>
|
|
72
|
+
<IconButton
|
|
73
|
+
size="sm"
|
|
74
|
+
label={t('removeNetwork')}
|
|
75
|
+
disabled={builtin.has(item.name)}
|
|
76
|
+
onClick={() => onAction({ kind: 'network', action: 'delete', id: item.name })}
|
|
77
|
+
>
|
|
78
|
+
<IconTrash size={14} />
|
|
79
|
+
</IconButton>
|
|
80
|
+
</span>
|
|
81
|
+
</td>
|
|
82
|
+
</tr>
|
|
83
|
+
))}
|
|
84
|
+
</tbody>
|
|
85
|
+
</table>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|