Skript Integration + UI Anpassungen
This commit is contained in:
@@ -424,7 +424,8 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
if (startOptions.ensureConfirmed) {
|
||||
await api.confirmEncodeReview(normalizedJobId, {
|
||||
selectedEncodeTitleId: startOptions.selectedEncodeTitleId ?? null,
|
||||
selectedTrackSelection: startOptions.selectedTrackSelection ?? null
|
||||
selectedTrackSelection: startOptions.selectedTrackSelection ?? null,
|
||||
selectedPostEncodeScriptIds: startOptions.selectedPostEncodeScriptIds ?? []
|
||||
});
|
||||
}
|
||||
await api.startJob(normalizedJobId);
|
||||
@@ -438,12 +439,18 @@ export default function DashboardPage({ pipeline, lastDiscEvent, refreshPipeline
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmReview = async (jobId, selectedEncodeTitleId = null, selectedTrackSelection = null) => {
|
||||
const handleConfirmReview = async (
|
||||
jobId,
|
||||
selectedEncodeTitleId = null,
|
||||
selectedTrackSelection = null,
|
||||
selectedPostEncodeScriptIds = undefined
|
||||
) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.confirmEncodeReview(jobId, {
|
||||
selectedEncodeTitleId,
|
||||
selectedTrackSelection
|
||||
selectedTrackSelection,
|
||||
selectedPostEncodeScriptIds
|
||||
});
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Toast } from 'primereact/toast';
|
||||
import { api } from '../api/client';
|
||||
import JobDetailDialog from '../components/JobDetailDialog';
|
||||
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
|
||||
import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
||||
import discIndicatorIcon from '../assets/media-disc.svg';
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Alle', value: '' },
|
||||
@@ -34,6 +36,11 @@ function statusSeverity(status) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
function resolveMediaType(row) {
|
||||
const raw = String(row?.mediaType || row?.media_type || '').trim().toLowerCase();
|
||||
return raw === 'bluray' ? 'bluray' : 'disc';
|
||||
}
|
||||
|
||||
export default function DatabasePage() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [orphanRows, setOrphanRows] = useState([]);
|
||||
@@ -425,6 +432,19 @@ export default function DatabasePage() {
|
||||
);
|
||||
|
||||
const stateBody = (row) => <Tag value={row.status} severity={statusSeverity(row.status)} />;
|
||||
const mediaBody = (row) => {
|
||||
const mediaType = resolveMediaType(row);
|
||||
const src = mediaType === 'bluray' ? blurayIndicatorIcon : discIndicatorIcon;
|
||||
const alt = mediaType === 'bluray' ? 'Blu-ray' : 'Disc';
|
||||
const title = mediaType === 'bluray' ? 'Blu-ray' : 'Sonstiges Medium';
|
||||
const label = mediaType === 'bluray' ? 'Blu-ray' : 'Sonstiges';
|
||||
return (
|
||||
<span className="job-step-cell">
|
||||
<img src={src} alt={alt} title={title} className="media-indicator-icon" />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
const orphanTitleBody = (row) => (
|
||||
<div>
|
||||
<div><strong>{row.title || '-'}</strong></div>
|
||||
@@ -483,6 +503,7 @@ export default function DatabasePage() {
|
||||
>
|
||||
<Column field="id" header="ID" style={{ width: '6rem' }} />
|
||||
<Column header="Bild" body={posterBody} style={{ width: '7rem' }} />
|
||||
<Column header="Medium" body={mediaBody} style={{ width: '10rem' }} />
|
||||
<Column header="Titel" body={titleBody} style={{ minWidth: '18rem' }} />
|
||||
<Column header="Status" body={stateBody} style={{ width: '11rem' }} />
|
||||
<Column field="start_time" header="Start" style={{ width: '16rem' }} />
|
||||
|
||||
@@ -2,6 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TabView, TabPanel } from 'primereact/tabview';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { api } from '../api/client';
|
||||
import DynamicSettingsForm from '../components/DynamicSettingsForm';
|
||||
|
||||
@@ -22,26 +26,154 @@ function isSameValue(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function injectHandBrakePresetOptions(categories, presetPayload) {
|
||||
const list = Array.isArray(categories) ? categories : [];
|
||||
const sourceOptions = Array.isArray(presetPayload?.options) ? presetPayload.options : [];
|
||||
|
||||
return list.map((category) => ({
|
||||
...category,
|
||||
settings: (category?.settings || []).map((setting) => {
|
||||
if (setting?.key !== 'handbrake_preset') {
|
||||
return setting;
|
||||
}
|
||||
|
||||
const normalizedOptions = [];
|
||||
const seenValues = new Set();
|
||||
const seenGroupLabels = new Set();
|
||||
const addGroupOption = (option) => {
|
||||
const rawLabel = String(option?.label || '').trim();
|
||||
if (!rawLabel || seenGroupLabels.has(rawLabel)) {
|
||||
return;
|
||||
}
|
||||
seenGroupLabels.add(rawLabel);
|
||||
normalizedOptions.push({
|
||||
...option,
|
||||
label: rawLabel,
|
||||
value: String(option?.value || `__group__${rawLabel.toLowerCase().replace(/\s+/g, '_')}`),
|
||||
disabled: true
|
||||
});
|
||||
};
|
||||
const addSelectableOption = (optionValue, optionLabel = optionValue, option = null) => {
|
||||
const value = String(optionValue || '').trim();
|
||||
if (!value || seenValues.has(value)) {
|
||||
return;
|
||||
}
|
||||
seenValues.add(value);
|
||||
normalizedOptions.push({
|
||||
...(option && typeof option === 'object' ? option : {}),
|
||||
label: String(optionLabel ?? value),
|
||||
value,
|
||||
disabled: false
|
||||
});
|
||||
};
|
||||
|
||||
for (const option of sourceOptions) {
|
||||
if (option?.disabled) {
|
||||
addGroupOption(option);
|
||||
continue;
|
||||
}
|
||||
addSelectableOption(option?.value, option?.label, option);
|
||||
}
|
||||
addSelectableOption(setting?.value);
|
||||
addSelectableOption(setting?.defaultValue);
|
||||
|
||||
if (normalizedOptions.length === 0) {
|
||||
return setting;
|
||||
}
|
||||
|
||||
return {
|
||||
...setting,
|
||||
type: 'select',
|
||||
options: normalizedOptions
|
||||
};
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testingPushover, setTestingPushover] = useState(false);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const [initialValues, setInitialValues] = useState({});
|
||||
const [draftValues, setDraftValues] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [scripts, setScripts] = useState([]);
|
||||
const [scriptsLoading, setScriptsLoading] = useState(false);
|
||||
const [scriptSaving, setScriptSaving] = useState(false);
|
||||
const [scriptActionBusyId, setScriptActionBusyId] = useState(null);
|
||||
const [scriptEditor, setScriptEditor] = useState({
|
||||
mode: 'none',
|
||||
id: null,
|
||||
name: '',
|
||||
scriptBody: ''
|
||||
});
|
||||
const [scriptErrors, setScriptErrors] = useState({});
|
||||
const [lastScriptTestResult, setLastScriptTestResult] = useState(null);
|
||||
const toastRef = useRef(null);
|
||||
|
||||
const loadScripts = async ({ silent = false } = {}) => {
|
||||
if (!silent) {
|
||||
setScriptsLoading(true);
|
||||
}
|
||||
try {
|
||||
const response = await api.getScripts();
|
||||
const next = Array.isArray(response?.scripts) ? response.scripts : [];
|
||||
setScripts(next);
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Script-Liste', detail: error.message });
|
||||
}
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setScriptsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.getSettings();
|
||||
const nextCategories = response.categories || [];
|
||||
const [settingsResponse, presetsResponse, scriptsResponse] = await Promise.allSettled([
|
||||
api.getSettings(),
|
||||
api.getHandBrakePresets(),
|
||||
api.getScripts()
|
||||
]);
|
||||
if (settingsResponse.status !== 'fulfilled') {
|
||||
throw settingsResponse.reason;
|
||||
}
|
||||
let nextCategories = settingsResponse.value?.categories || [];
|
||||
const presetPayload = presetsResponse.status === 'fulfilled' ? presetsResponse.value : null;
|
||||
nextCategories = injectHandBrakePresetOptions(nextCategories, presetPayload);
|
||||
if (presetsResponse.status === 'fulfilled' && presetsResponse.value?.message) {
|
||||
toastRef.current?.show({
|
||||
severity: presetsResponse.value?.source === 'fallback' ? 'warn' : 'info',
|
||||
summary: 'HandBrake Presets',
|
||||
detail: presetsResponse.value.message
|
||||
});
|
||||
}
|
||||
if (presetsResponse.status === 'rejected') {
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'HandBrake Presets',
|
||||
detail: 'Preset-Liste konnte nicht geladen werden. Aktueller Wert bleibt auswählbar.'
|
||||
});
|
||||
}
|
||||
const values = buildValuesMap(nextCategories);
|
||||
setCategories(nextCategories);
|
||||
setInitialValues(values);
|
||||
setDraftValues(values);
|
||||
setErrors({});
|
||||
if (scriptsResponse.status === 'fulfilled') {
|
||||
setScripts(Array.isArray(scriptsResponse.value?.scripts) ? scriptsResponse.value.scripts : []);
|
||||
} else {
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Scripte',
|
||||
detail: 'Script-Liste konnte nicht geladen werden.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toastRef.current?.show({ severity: 'error', summary: 'Fehler', detail: error.message });
|
||||
} finally {
|
||||
@@ -148,56 +280,404 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleScriptEditorChange = (key, value) => {
|
||||
setScriptEditor((prev) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
setScriptErrors((prev) => ({
|
||||
...prev,
|
||||
[key]: null
|
||||
}));
|
||||
};
|
||||
|
||||
const clearScriptEditor = () => {
|
||||
setScriptEditor({
|
||||
mode: 'none',
|
||||
id: null,
|
||||
name: '',
|
||||
scriptBody: ''
|
||||
});
|
||||
setScriptErrors({});
|
||||
};
|
||||
|
||||
const startCreateScript = () => {
|
||||
setScriptEditor({
|
||||
mode: 'create',
|
||||
id: null,
|
||||
name: '',
|
||||
scriptBody: ''
|
||||
});
|
||||
setScriptErrors({});
|
||||
setLastScriptTestResult(null);
|
||||
};
|
||||
|
||||
const startEditScript = (script) => {
|
||||
setScriptEditor({
|
||||
mode: 'edit',
|
||||
id: script?.id || null,
|
||||
name: script?.name || '',
|
||||
scriptBody: script?.scriptBody || ''
|
||||
});
|
||||
setScriptErrors({});
|
||||
setLastScriptTestResult(null);
|
||||
};
|
||||
|
||||
const handleSaveScript = async () => {
|
||||
if (scriptEditor?.mode !== 'create' && scriptEditor?.mode !== 'edit') {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
name: String(scriptEditor?.name || '').trim(),
|
||||
scriptBody: String(scriptEditor?.scriptBody || '')
|
||||
};
|
||||
setScriptSaving(true);
|
||||
try {
|
||||
if (scriptEditor?.id) {
|
||||
await api.updateScript(scriptEditor.id, payload);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Scripte',
|
||||
detail: 'Script aktualisiert.'
|
||||
});
|
||||
} else {
|
||||
await api.createScript(payload);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Scripte',
|
||||
detail: 'Script angelegt.'
|
||||
});
|
||||
}
|
||||
await loadScripts({ silent: true });
|
||||
setScriptErrors({});
|
||||
clearScriptEditor();
|
||||
} catch (error) {
|
||||
const details = Array.isArray(error?.details) ? error.details : [];
|
||||
if (details.length > 0) {
|
||||
const nextErrors = {};
|
||||
for (const item of details) {
|
||||
if (item?.field) {
|
||||
nextErrors[item.field] = item.message || 'Ungültiger Wert';
|
||||
}
|
||||
}
|
||||
setScriptErrors(nextErrors);
|
||||
}
|
||||
toastRef.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Script speichern fehlgeschlagen',
|
||||
detail: error.message
|
||||
});
|
||||
} finally {
|
||||
setScriptSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteScript = async (script) => {
|
||||
const scriptId = Number(script?.id);
|
||||
if (!Number.isFinite(scriptId) || scriptId <= 0) {
|
||||
return;
|
||||
}
|
||||
const confirmed = window.confirm(`Script "${script?.name || scriptId}" wirklich löschen?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
setScriptActionBusyId(scriptId);
|
||||
try {
|
||||
await api.deleteScript(scriptId);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Scripte',
|
||||
detail: 'Script gelöscht.'
|
||||
});
|
||||
await loadScripts({ silent: true });
|
||||
if (scriptEditor?.mode === 'edit' && Number(scriptEditor?.id) === scriptId) {
|
||||
clearScriptEditor();
|
||||
}
|
||||
} catch (error) {
|
||||
toastRef.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Script löschen fehlgeschlagen',
|
||||
detail: error.message
|
||||
});
|
||||
} finally {
|
||||
setScriptActionBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestScript = async (script) => {
|
||||
const scriptId = Number(script?.id);
|
||||
if (!Number.isFinite(scriptId) || scriptId <= 0) {
|
||||
return;
|
||||
}
|
||||
setScriptActionBusyId(scriptId);
|
||||
try {
|
||||
const response = await api.testScript(scriptId);
|
||||
const result = response?.result || null;
|
||||
setLastScriptTestResult(result);
|
||||
if (result?.success) {
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Script-Test',
|
||||
detail: `"${script?.name || scriptId}" erfolgreich ausgeführt.`
|
||||
});
|
||||
} else {
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Script-Test',
|
||||
detail: `"${script?.name || scriptId}" fehlgeschlagen (exit=${result?.exitCode ?? 'n/a'}).`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toastRef.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Script-Test fehlgeschlagen',
|
||||
detail: error.message
|
||||
});
|
||||
} finally {
|
||||
setScriptActionBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-grid">
|
||||
<Toast ref={toastRef} />
|
||||
|
||||
<Card title="Einstellungen" subTitle="Änderungen werden erst beim Speichern in die Datenbank übernommen">
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Änderungen speichern"
|
||||
icon="pi pi-save"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!hasUnsavedChanges}
|
||||
/>
|
||||
<Button
|
||||
label="Änderungen verwerfen"
|
||||
icon="pi pi-undo"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={handleDiscard}
|
||||
disabled={!hasUnsavedChanges || saving}
|
||||
/>
|
||||
<Button
|
||||
label="Neu laden"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
onClick={load}
|
||||
loading={loading}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Button
|
||||
label="PushOver Test"
|
||||
icon="pi pi-send"
|
||||
severity="info"
|
||||
onClick={handlePushoverTest}
|
||||
loading={testingPushover}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<TabView
|
||||
className="settings-root-tabview"
|
||||
activeIndex={activeTabIndex}
|
||||
onTabChange={(event) => setActiveTabIndex(Number(event.index || 0))}
|
||||
>
|
||||
<TabPanel header="Konfiguration">
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Änderungen speichern"
|
||||
icon="pi pi-save"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!hasUnsavedChanges}
|
||||
/>
|
||||
<Button
|
||||
label="Änderungen verwerfen"
|
||||
icon="pi pi-undo"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={handleDiscard}
|
||||
disabled={!hasUnsavedChanges || saving}
|
||||
/>
|
||||
<Button
|
||||
label="Neu laden"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
onClick={load}
|
||||
loading={loading}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Button
|
||||
label="PushOver Test"
|
||||
icon="pi pi-send"
|
||||
severity="info"
|
||||
onClick={handlePushoverTest}
|
||||
loading={testingPushover}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p>Lade Settings ...</p>
|
||||
) : (
|
||||
<DynamicSettingsForm
|
||||
categories={categories}
|
||||
values={draftValues}
|
||||
errors={errors}
|
||||
dirtyKeys={dirtyKeys}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
)}
|
||||
{loading ? (
|
||||
<p>Lade Settings ...</p>
|
||||
) : (
|
||||
<DynamicSettingsForm
|
||||
categories={categories}
|
||||
values={draftValues}
|
||||
errors={errors}
|
||||
dirtyKeys={dirtyKeys}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel header="Scripte">
|
||||
<div className="script-manager-wrap">
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Neues Skript hinzufügen"
|
||||
icon="pi pi-plus"
|
||||
onClick={startCreateScript}
|
||||
severity="success"
|
||||
outlined
|
||||
disabled={scriptSaving || scriptEditor?.mode === 'create'}
|
||||
/>
|
||||
<Button
|
||||
label="Scripts neu laden"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
onClick={() => loadScripts()}
|
||||
loading={scriptsLoading}
|
||||
disabled={scriptSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
Die ausgewählten Scripts werden später pro Job nach erfolgreichem Encode in Reihenfolge ausgeführt.
|
||||
</small>
|
||||
|
||||
<div className="script-list-box">
|
||||
<h4>Verfügbare Scripts</h4>
|
||||
{scriptsLoading ? (
|
||||
<p>Lade Scripts ...</p>
|
||||
) : (
|
||||
<div className="script-list">
|
||||
{scriptEditor?.mode === 'create' ? (
|
||||
<div className="script-list-item script-list-item-editing">
|
||||
<div className="script-list-main">
|
||||
<div className="script-title-line">
|
||||
<strong className="script-id-title">NEU - Titel</strong>
|
||||
<InputText
|
||||
id="script-name-new"
|
||||
value={scriptEditor?.name || ''}
|
||||
onChange={(event) => handleScriptEditorChange('name', event.target.value)}
|
||||
placeholder="z.B. Library Refresh"
|
||||
className="script-title-input"
|
||||
/>
|
||||
</div>
|
||||
{scriptErrors?.name ? <small className="error-text">{scriptErrors.name}</small> : null}
|
||||
</div>
|
||||
<div className="script-editor-fields">
|
||||
<label htmlFor="script-body-new">Bash Script</label>
|
||||
<InputTextarea
|
||||
id="script-body-new"
|
||||
value={scriptEditor?.scriptBody || ''}
|
||||
onChange={(event) => handleScriptEditorChange('scriptBody', event.target.value)}
|
||||
rows={12}
|
||||
autoResize={false}
|
||||
placeholder={'#!/usr/bin/env bash\necho "Post-Encode Script"'}
|
||||
/>
|
||||
{scriptErrors?.scriptBody ? <small className="error-text">{scriptErrors.scriptBody}</small> : null}
|
||||
</div>
|
||||
<div className="script-list-actions">
|
||||
<Button
|
||||
label="Speichern"
|
||||
icon="pi pi-save"
|
||||
onClick={handleSaveScript}
|
||||
loading={scriptSaving}
|
||||
/>
|
||||
<Button
|
||||
label="Verwerfen"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={clearScriptEditor}
|
||||
disabled={scriptSaving}
|
||||
/>
|
||||
<span className="script-action-spacer" aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{scripts.length === 0 ? <p>Keine Scripts vorhanden.</p> : null}
|
||||
|
||||
{scripts.map((script) => {
|
||||
return (
|
||||
<div key={script.id} className="script-list-item">
|
||||
<div className="script-list-main">
|
||||
<strong className="script-id-title">{`ID #${script.id} - ${script.name}`}</strong>
|
||||
</div>
|
||||
|
||||
<div className="script-list-actions">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
label="Bearbeiten"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => startEditScript(script)}
|
||||
disabled={Boolean(scriptActionBusyId) || scriptSaving || scriptEditor?.mode === 'create'}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-play"
|
||||
label="Test"
|
||||
severity="info"
|
||||
onClick={() => handleTestScript(script)}
|
||||
loading={scriptActionBusyId === script.id}
|
||||
disabled={Boolean(scriptActionBusyId) && scriptActionBusyId !== script.id}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
label="Löschen"
|
||||
severity="danger"
|
||||
outlined
|
||||
onClick={() => handleDeleteScript(script)}
|
||||
loading={scriptActionBusyId === script.id}
|
||||
disabled={Boolean(scriptActionBusyId) && scriptActionBusyId !== script.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
header={scriptEditor?.id ? `Script bearbeiten (#${scriptEditor.id})` : 'Script bearbeiten'}
|
||||
visible={scriptEditor?.mode === 'edit'}
|
||||
onHide={clearScriptEditor}
|
||||
style={{ width: 'min(52rem, calc(100vw - 1.5rem))' }}
|
||||
className="script-edit-dialog"
|
||||
dismissableMask
|
||||
draggable={false}
|
||||
>
|
||||
<div className="script-editor-fields">
|
||||
<label htmlFor="script-edit-name">Name</label>
|
||||
<InputText
|
||||
id="script-edit-name"
|
||||
value={scriptEditor?.name || ''}
|
||||
onChange={(event) => handleScriptEditorChange('name', event.target.value)}
|
||||
placeholder="z.B. Library Refresh"
|
||||
/>
|
||||
{scriptErrors?.name ? <small className="error-text">{scriptErrors.name}</small> : null}
|
||||
<label htmlFor="script-edit-body">Bash Script</label>
|
||||
<InputTextarea
|
||||
id="script-edit-body"
|
||||
value={scriptEditor?.scriptBody || ''}
|
||||
onChange={(event) => handleScriptEditorChange('scriptBody', event.target.value)}
|
||||
rows={14}
|
||||
autoResize={false}
|
||||
placeholder={'#!/usr/bin/env bash\necho "Post-Encode Script"'}
|
||||
/>
|
||||
{scriptErrors?.scriptBody ? <small className="error-text">{scriptErrors.scriptBody}</small> : null}
|
||||
</div>
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
label="Script aktualisieren"
|
||||
icon="pi pi-save"
|
||||
onClick={handleSaveScript}
|
||||
loading={scriptSaving}
|
||||
/>
|
||||
<Button
|
||||
label="Abbrechen"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={clearScriptEditor}
|
||||
disabled={scriptSaving}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{lastScriptTestResult ? (
|
||||
<div className="script-test-result">
|
||||
<h4>Letzter Script-Test: {lastScriptTestResult.scriptName}</h4>
|
||||
<small>
|
||||
Status: {lastScriptTestResult.success ? 'SUCCESS' : 'ERROR'}
|
||||
{' | '}exit={lastScriptTestResult.exitCode ?? 'n/a'}
|
||||
{' | '}timeout={lastScriptTestResult.timedOut ? 'ja' : 'nein'}
|
||||
{' | '}dauer={Number(lastScriptTestResult.durationMs || 0)}ms
|
||||
</small>
|
||||
<pre>{`${lastScriptTestResult.stdout || ''}${lastScriptTestResult.stderr ? `\n${lastScriptTestResult.stderr}` : ''}`.trim() || 'Keine Ausgabe.'}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user