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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/cordis.patch.yml +3 -0
  4. package/docs/ai-chat.png +0 -0
  5. package/docs/ai-new-session.png +0 -0
  6. package/docs/apple-container.png +0 -0
  7. package/docs/attach.png +0 -0
  8. package/docs/docker-console.png +0 -0
  9. package/docs/docker-overview.png +0 -0
  10. package/docs/docker-volumes.png +0 -0
  11. package/docs/image-architecture.png +0 -0
  12. package/docs/package-install.png +0 -0
  13. package/docs/provider-switch.png +0 -0
  14. package/docs/run-container.png +0 -0
  15. package/index.js +138 -0
  16. package/lib/client.js +3560 -0
  17. package/package.json +62 -4
  18. package/src/client/api.js +98 -0
  19. package/src/client/attach.jsx +134 -0
  20. package/src/client/components.jsx +320 -0
  21. package/src/client/hooks.js +161 -0
  22. package/src/client/i18n.js +502 -0
  23. package/src/client/icons.jsx +227 -0
  24. package/src/client/index.jsx +76 -0
  25. package/src/client/page.jsx +782 -0
  26. package/src/client/run-container.jsx +187 -0
  27. package/src/client/runs.js +182 -0
  28. package/src/client/store.js +91 -0
  29. package/src/client/styles.js +371 -0
  30. package/src/client/tabs/compose.jsx +94 -0
  31. package/src/client/tabs/console.jsx +211 -0
  32. package/src/client/tabs/containers.jsx +148 -0
  33. package/src/client/tabs/images.jsx +127 -0
  34. package/src/client/tabs/networks.jsx +91 -0
  35. package/src/client/tabs/overview.jsx +266 -0
  36. package/src/client/tabs/volumes.jsx +76 -0
  37. package/src/host/apple-cli.js +307 -0
  38. package/src/host/apple.js +471 -0
  39. package/src/host/attach.js +246 -0
  40. package/src/host/commands.js +235 -0
  41. package/src/host/docker-cli.js +405 -0
  42. package/src/host/jobs.js +245 -0
  43. package/src/host/parsers.js +484 -0
  44. package/src/host/runtime.js +285 -0
  45. package/src/host/service.js +836 -0
  46. package/src/host/tools.js +198 -0
  47. 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
+ }