Projekte: Preview-Block ohne Kleinknoepfe, Statuspunkt als Badge, Log auf hellem Grund
Markup auf Bausteine (Button, Badge) und die Klassen pv-*, input, select, msg, link umgestellt; keine Inline-Stile, kein btn-sm, kein Farbliteral. Fehlertexte des Runners behalten ihren Zeilenumbruch ueber .pv-block .msg. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
92fef9d4a5
commit
154aea44a8
2 files changed
+46
-57
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { modeOptionsFor, previewStatus, modeHint, isDevUnavailableError, NO_DEV_HINT } from '../lib/previewView'
|
import { modeOptionsFor, previewStatus, modeHint, isDevUnavailableError, NO_DEV_HINT } from '../lib/previewView'
|
||||||
import { isValidEnvName, buildEnvPayload, isDevPreview, dotColor, envValueTooLong, MAX_ENV_VARS } from '../lib/previewEnv'
|
import { isValidEnvName, buildEnvPayload, isDevPreview, envValueTooLong, MAX_ENV_VARS } from '../lib/previewEnv'
|
||||||
import {
|
import {
|
||||||
fetchPreview,
|
fetchPreview,
|
||||||
setPreviewMode,
|
setPreviewMode,
|
||||||
@@ -10,8 +10,9 @@ import {
|
|||||||
stopPreviewDev,
|
stopPreviewDev,
|
||||||
fetchPreviewLog,
|
fetchPreviewLog,
|
||||||
} from '../lib/previewApi'
|
} from '../lib/previewApi'
|
||||||
|
import { Button, Badge } from './ui'
|
||||||
|
import { previewColorTone } from './projectTones'
|
||||||
|
|
||||||
const STATE_OF_COLOR = { green: 'online', yellow: 'building', red: 'failed', grey: 'off' }
|
|
||||||
const NOT_ACTIVE = 'Preview-Runner ist nicht aktiv'
|
const NOT_ACTIVE = 'Preview-Runner ist nicht aktiv'
|
||||||
|
|
||||||
const logDate = (at) => {
|
const logDate = (at) => {
|
||||||
@@ -171,7 +172,7 @@ export default function PreviewBlock({ projectId }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="text-grey" style={{ fontSize: 12 }}>Lädt …</div>
|
if (loading) return <p className="muted xs">Lädt …</p>
|
||||||
|
|
||||||
const view = data ? { ...data } : { state: 'off' }
|
const view = data ? { ...data } : { state: 'off' }
|
||||||
const status = previewStatus(view)
|
const status = previewStatus(view)
|
||||||
@@ -182,18 +183,15 @@ export default function PreviewBlock({ projectId }) {
|
|||||||
const dev = isDevPreview(data)
|
const dev = isDevPreview(data)
|
||||||
const logs = [...(data?.logs || [])].sort((a, b) => new Date(b.at) - new Date(a.at)).slice(0, 5)
|
const logs = [...(data?.logs || [])].sort((a, b) => new Date(b.at) - new Date(a.at)).slice(0, 5)
|
||||||
|
|
||||||
const small = { fontSize: 12 }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontSize: 13 }}>
|
<div className="pv-block">
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
<div className="pv-row">
|
||||||
<label className="text-grey" htmlFor={`pv-mode-${projectId}`} style={small}>Modus</label>
|
<label className="muted xs" htmlFor={`pv-mode-${projectId}`}>Modus</label>
|
||||||
<select
|
<select
|
||||||
id={`pv-mode-${projectId}`}
|
id={`pv-mode-${projectId}`}
|
||||||
className="form-control"
|
className="select pv-mode"
|
||||||
style={{ maxWidth: 200 }}
|
|
||||||
value={mode}
|
value={mode}
|
||||||
disabled={busy || inactive}
|
disabled={busy || inactive}
|
||||||
onChange={(e) => run(() => setPreviewMode(projectId, e.target.value))}
|
onChange={(e) => run(() => setPreviewMode(projectId, e.target.value))}
|
||||||
@@ -201,47 +199,45 @@ export default function PreviewBlock({ projectId }) {
|
|||||||
{modeOptionsFor(data.devAvailable).map((o) => <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>)}
|
{modeOptionsFor(data.devAvailable).map((o) => <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{data.devAvailable === false && <div className="text-grey" style={{ ...small, marginTop: 4 }}>{NO_DEV_HINT}</div>}
|
{data.devAvailable === false && <p className="muted xs">{NO_DEV_HINT}</p>}
|
||||||
{hint && <div className="text-grey" style={{ ...small, marginTop: 4 }}>{hint}</div>}
|
{hint && <p className="muted xs">{hint}</p>}
|
||||||
<div style={{ marginTop: 8, display: 'flex', gap: 6, alignItems: 'flex-start' }}>
|
<div className="pv-row">
|
||||||
<span style={{ width: 10, height: 10, borderRadius: '50%', background: dotColor(STATE_OF_COLOR[status.color]), display: 'inline-block', flexShrink: 0, marginTop: 4 }} />
|
<Badge tone={previewColorTone(status.color)} className="wrap">{status.text}</Badge>
|
||||||
<span style={{ overflowWrap: 'anywhere' }}>{status.text}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && (
|
{data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && (
|
||||||
<div style={{ color: 'var(--red-fg)', ...small, marginTop: 4, overflowWrap: 'anywhere' }}>{data.error}</div>
|
<p className="msg err">{data.error}</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inactive && <div className="text-grey" style={{ ...small, marginTop: 8 }}>{NOT_ACTIVE}.</div>}
|
{inactive && <p className="muted xs">{NOT_ACTIVE}.</p>}
|
||||||
{error && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 8, overflowWrap: 'anywhere' }}>{error}</div>}
|
{error && <p className="msg err" role="alert">{error}</p>}
|
||||||
|
|
||||||
{data && !inactive && (
|
{data && !inactive && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 10 }}>
|
<div className="pv-row">
|
||||||
<button type="button" className="btn btn-sm" disabled={busy || state === 'building'} onClick={() => run(() => rebuildPreview(projectId))}>
|
<Button type="button" disabled={busy || state === 'building'} onClick={() => run(() => rebuildPreview(projectId))}>
|
||||||
Neu bauen
|
Neu bauen
|
||||||
</button>
|
</Button>
|
||||||
<button type="button" className="btn btn-sm" disabled={busy || logs.length === 0} onClick={() => setShowLogs((v) => !v)}>
|
<Button type="button" disabled={busy || logs.length === 0} onClick={() => setShowLogs((v) => !v)}>
|
||||||
Protokoll ansehen
|
Protokoll ansehen
|
||||||
</button>
|
</Button>
|
||||||
{dev && (
|
{dev && (
|
||||||
<>
|
<>
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => run(() => startPreviewDev(projectId))}>
|
<Button type="button" disabled={busy} onClick={() => run(() => startPreviewDev(projectId))}>
|
||||||
Jetzt starten
|
Jetzt starten
|
||||||
</button>
|
</Button>
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => run(() => stopPreviewDev(projectId))}>
|
<Button type="button" disabled={busy} onClick={() => run(() => stopPreviewDev(projectId))}>
|
||||||
Stoppen
|
Stoppen
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showLogs && (
|
{showLogs && (
|
||||||
<div style={{ marginTop: 8 }}>
|
<>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="select"
|
||||||
style={{ maxWidth: '100%' }}
|
|
||||||
aria-label="Lauf wählen"
|
aria-label="Lauf wählen"
|
||||||
value={logFile}
|
value={logFile}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
@@ -250,47 +246,39 @@ export default function PreviewBlock({ projectId }) {
|
|||||||
<option value="">Lauf wählen …</option>
|
<option value="">Lauf wählen …</option>
|
||||||
{logs.map((l) => <option key={l.runFile} value={l.runFile}>{logDate(l.at)}</option>)}
|
{logs.map((l) => <option key={l.runFile} value={l.runFile}>{logDate(l.at)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
{logFile && (
|
{logFile && <pre className="pv-log">{logText || '…'}</pre>}
|
||||||
<pre style={{ marginTop: 6, maxHeight: 260, overflow: 'auto', background: 'var(--bg-subtle)', padding: 8, borderRadius: 6, fontFamily: 'monospace', fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
</>
|
||||||
{logText || '…'}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginTop: 14 }}>
|
<h4 className="pv-title">Variablen</h4>
|
||||||
<strong style={small}>Variablen</strong>
|
{names.length === 0 && <p className="muted xs">Keine Variablen.</p>}
|
||||||
<div style={{ marginTop: 6 }}>
|
|
||||||
{names.length === 0 && <div className="text-grey" style={small}>Keine Variablen.</div>}
|
|
||||||
{names.map((n) => (
|
{names.map((n) => (
|
||||||
<div key={n} style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: '4px 0', borderBottom: '1px solid var(--line)' }}>
|
<div key={n} className="pv-var">
|
||||||
<code style={{ flex: '1 1 140px', overflowWrap: 'anywhere' }}>{n}</code>
|
<code>{n}</code>
|
||||||
<span className="text-grey">•••</span>
|
<span className="muted">•••</span>
|
||||||
{editName === n ? (
|
{editName === n ? (
|
||||||
<>
|
<>
|
||||||
<input className="form-control" style={{ flex: '1 1 120px', minWidth: 0 }} type="password" autoComplete="off" placeholder="Neuer Wert" value={editValue} onKeyDown={enter(handleChange)} onChange={(e) => setEditValue(e.target.value)} />
|
<input className="input" type="password" autoComplete="off" placeholder="Neuer Wert" value={editValue} onKeyDown={enter(handleChange)} onChange={(e) => setEditValue(e.target.value)} />
|
||||||
<button type="button" className="btn btn-sm" onClick={handleChange}>Übernehmen</button>
|
<button type="button" className="link" onClick={handleChange}>Übernehmen</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => { setEditName(n); setEditValue(''); setEnvError('') }}>Ändern</button>
|
<button type="button" className="link" disabled={busy} onClick={() => { setEditName(n); setEditValue(''); setEnvError('') }}>Ändern</button>
|
||||||
)}
|
)}
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => handleRemove(n)}>Entfernen</button>
|
<button type="button" className="link" disabled={busy} onClick={() => handleRemove(n)}>Entfernen</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginTop: 8 }}>
|
<div className="pv-add">
|
||||||
<input type="text" className="form-control" style={{ flex: '1 1 140px', minWidth: 0 }} placeholder="NAME" onKeyDown={enter(handleAdd)} value={newName} onChange={(e) => setNewName(e.target.value)} />
|
<input type="text" className="input" placeholder="NAME" onKeyDown={enter(handleAdd)} value={newName} onChange={(e) => setNewName(e.target.value)} />
|
||||||
<input type="password" autoComplete="off" className="form-control" style={{ flex: '1 1 140px', minWidth: 0 }} placeholder="Wert" onKeyDown={enter(handleAdd)} value={newValue} onChange={(e) => setNewValue(e.target.value)} />
|
<input type="password" autoComplete="off" className="input" placeholder="Wert" onKeyDown={enter(handleAdd)} value={newValue} onChange={(e) => setNewValue(e.target.value)} />
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleAdd}>Hinzufügen</button>
|
<Button type="button" disabled={busy} onClick={handleAdd}>Hinzufügen</Button>
|
||||||
</div>
|
</div>
|
||||||
{envError && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 4 }}>{envError}</div>}
|
{envError && <p className="msg err">{envError}</p>}
|
||||||
{dirty && (
|
{dirty && (
|
||||||
<div style={{ marginTop: 8 }}>
|
<div className="pv-row">
|
||||||
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleSaveEnv}>Variablen speichern</button>
|
<Button type="button" disabled={busy} onClick={handleSaveEnv}>Variablen speichern</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-grey" style={{ ...small, marginTop: 6 }}>Alles mit VITE_ / NEXT_PUBLIC_ ist später im Browser sichtbar.</div>
|
<p className="muted xs">Alles mit VITE_ / NEXT_PUBLIC_ ist später im Browser sichtbar.</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1150,6 +1150,7 @@ button.link { text-align: left; } /* Reset und Gewicht von button.link stehen im
|
|||||||
.pv-var { padding-bottom: 8px; border-bottom: 1px solid var(--line); }
|
.pv-var { padding-bottom: 8px; border-bottom: 1px solid var(--line); }
|
||||||
.pv-mode { width: 200px; max-width: 100%; }
|
.pv-mode { width: 200px; max-width: 100%; }
|
||||||
.pv-title { font-size: 13px; font-weight: 600; }
|
.pv-title { font-size: 13px; font-weight: 600; }
|
||||||
|
.pv-block .msg { overflow-wrap: anywhere; } /* Fehlertexte des Runners enthalten lange Pfade ohne Leerzeichen */
|
||||||
.pv-log { background: var(--bg-subtle); max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all; padding: 8px; border-radius: var(--radius); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 16px; }
|
.pv-log { background: var(--bg-subtle); max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all; padding: 8px; border-radius: var(--radius); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 16px; }
|
||||||
.pv-var code, .pv-var .input, .pv-add .input { flex: 1 1 140px; min-width: 0; }
|
.pv-var code, .pv-var .input, .pv-add .input { flex: 1 1 140px; min-width: 0; }
|
||||||
.pv-var code { overflow-wrap: anywhere; }
|
.pv-var code { overflow-wrap: anywhere; }
|
||||||
|
|||||||
Reference in new issue
Block a user