feat: add workflow orchestration and media governance
This commit is contained in:
@@ -44,7 +44,8 @@ import {
|
||||
Webhook,
|
||||
Workflow,
|
||||
XCircle,
|
||||
Zap
|
||||
Zap,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import {
|
||||
createApiClient,
|
||||
@@ -90,6 +91,8 @@ import {
|
||||
testNotification,
|
||||
fetchNotificationDeliveries,
|
||||
fetchStorageUsage,
|
||||
fetchStorageCleanupPreview,
|
||||
reclaimStorage,
|
||||
fetchSsoProviders,
|
||||
fetchIdentityCenter,
|
||||
fetchOrganizationCommercial,
|
||||
@@ -2460,6 +2463,7 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
|
||||
const [health, setHealth] = useState({ services: [], summary: {} });
|
||||
const [readiness, setReadiness] = useState({ checks: [], summary: {}, backups: { backups: [], count: 0 } });
|
||||
const [storage, setStorage] = useState(null);
|
||||
const [cleanup, setCleanup] = useState(null);
|
||||
const [deliveries, setDeliveries] = useState([]);
|
||||
const [originalSettings, setOriginalSettings] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -2468,17 +2472,19 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
|
||||
const [clientName, setClientName] = useState("");
|
||||
const [apiKeyNotice, setApiKeyNotice] = useState("");
|
||||
const [backupBusy, setBackupBusy] = useState(false);
|
||||
const [cleanupBusy, setCleanupBusy] = useState(false);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [nextConfig, nextHealth, nextReadiness, nextStorage, nextDeliveries] = await Promise.all([fetchSystemConfig(contextOverrides), fetchSystemHealth(contextOverrides), fetchSystemReadiness(contextOverrides), fetchStorageUsage(contextOverrides), fetchNotificationDeliveries(contextOverrides)]);
|
||||
const [nextConfig, nextHealth, nextReadiness, nextStorage, nextDeliveries, nextCleanup] = await Promise.all([fetchSystemConfig(contextOverrides), fetchSystemHealth(contextOverrides), fetchSystemReadiness(contextOverrides), fetchStorageUsage(contextOverrides), fetchNotificationDeliveries(contextOverrides), fetchStorageCleanupPreview("", contextOverrides)]);
|
||||
setConfig(nextConfig);
|
||||
const values = Object.fromEntries(nextConfig.settings.map((setting) => [setting.key, setting.value]));
|
||||
setOriginalSettings(values);
|
||||
setHealth(nextHealth);
|
||||
setReadiness(nextReadiness);
|
||||
setStorage(nextStorage.storage || null);
|
||||
setCleanup(nextCleanup.cleanup || null);
|
||||
setDeliveries(nextDeliveries.deliveries || []);
|
||||
setNotice("");
|
||||
} catch (error) {
|
||||
@@ -2572,6 +2578,36 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function previewCleanup() {
|
||||
setCleanupBusy(true);
|
||||
try {
|
||||
const retentionDays = config.settings.find((setting) => setting.key === "storage.retention_days")?.value || "";
|
||||
const result = await fetchStorageCleanupPreview(retentionDays, contextOverrides);
|
||||
setCleanup(result.cleanup || null);
|
||||
setNotice(`清理预览完成:${result.cleanup?.candidates?.length || 0} 个无引用临时文件,${((result.cleanup?.totalBytes || 0) / 1024 ** 2).toFixed(2)} MB。`);
|
||||
} catch (error) {
|
||||
setNotice(error.message);
|
||||
} finally {
|
||||
setCleanupBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function reclaimCleanup() {
|
||||
if (!cleanup?.candidates?.length) return;
|
||||
if (typeof window !== "undefined" && !window.confirm(`确认回收 ${cleanup.candidates.length} 个无引用临时文件吗?角色资产、交付物和 QA 证据不会被处理。`)) return;
|
||||
setCleanupBusy(true);
|
||||
try {
|
||||
const result = await reclaimStorage({ paths: cleanup.candidates.map((item) => item.relativePath) }, contextOverrides);
|
||||
setCleanup(result.cleanup || null);
|
||||
setStorage(result.storage || storage);
|
||||
setNotice(`已回收 ${result.cleanup?.deleted?.length || 0} 个临时文件,释放 ${((result.cleanup?.deletedBytes || 0) / 1024 ** 2).toFixed(2)} MB,并写入审计日志。`);
|
||||
} catch (error) {
|
||||
setNotice(error.message);
|
||||
} finally {
|
||||
setCleanupBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="enterprise-page">
|
||||
<AdminHeader eyebrow="SYSTEM SETTINGS / CONTROL PLANE" title={title} description={description} action={<div className="header-actions"><button className="subtle" onClick={load}><RefreshCw size={15} />刷新</button>{section !== "overview" && <button className="primary" onClick={save} disabled={saving || !dirtySettings.length}><Save size={15} />{saving ? "保存中…" : dirtySettings.length ? `保存 ${dirtySettings.length} 项` : "保存配置"}</button>}</div>} />
|
||||
@@ -2583,6 +2619,7 @@ export function SystemSettingsPage({ section = "overview", contextOverrides }) {
|
||||
{(section !== "notifications" && section !== "overview" && section !== "features") && <section className="studio-card wide-card"><SectionBar title="系统配置" detail={dirtySettings.length ? `${dirtySettings.length} 项未保存` : "已同步"} action={<span className="setting-scope"><Database size={14} />SQLite / system_settings</span>} /><div className="settings-list">{visibleSettings.map((setting) => <div className="system-setting-row" key={setting.key}><div><strong>{settingLabels[setting.category] || setting.category} · {setting.key}</strong><span>{setting.description}</span></div><SettingInput setting={setting} value={setting.value} onChange={(value) => setSetting(setting.key, value)} /></div>)}</div></section>}
|
||||
{(section === "overview" || section === "generation" || section === "deployment" || section === "features") && <section className="studio-card wide-card"><SectionBar title="功能开关" detail="系统范围,变更立即生效" /><div className="feature-flag-grid">{config.featureFlags.map((flag) => <div key={flag.key}><div><strong>{flag.label}</strong><span>{flag.description}</span></div><button type="button" className={`switch-control ${flag.enabled ? "on" : ""}`} onClick={() => toggleFlag(flag)}><span />{flag.enabled ? "启用" : "关闭"}</button></div>)}</div></section>}
|
||||
{(section === "storage" || section === "overview") && <section className="studio-card wide-card"><SectionBar title="真实存储用量" detail={storage ? `当前项目 ${storage.percent}%` : "读取中"} /><div className="storage-admin-grid"><div className="storage-admin-kpi"><HardDrive size={20} /><strong>{storage ? `${storage.usedGb} GB` : "—"}</strong><span>已使用 / {storage?.limitGb || 0} GB</span></div><div className="storage-admin-track"><div><span>文件系统测量</span><b>{storage ? `${storage.usedBytes.toLocaleString()} / ${storage.limitBytes.toLocaleString()} bytes` : "—"}</b></div><div className="quota-track"><span style={{ width: `${storage?.percent || 0}%` }} /></div><small>资产、任务结果和本地合成输出均计入存储配额。</small></div><div className="storage-admin-files"><span>最大文件</span>{storage?.largestFiles?.slice(0, 4).map((file) => <div key={file.path}><strong>{file.relativePath}</strong><em>{(file.bytes / 1024 ** 2).toFixed(2)} MB</em></div>) || <small>暂无文件</small>}</div></div></section>}
|
||||
{(section === "storage" || section === "overview") && <section className="studio-card wide-card"><SectionBar title="临时媒体治理" detail={cleanup ? `保留 ${cleanup.policy.retentionDays} 天 · ${cleanup.candidates.length} 个候选` : "只处理无引用临时文件"} action={<div className="header-actions"><button className="subtle" onClick={previewCleanup} disabled={cleanupBusy}><RefreshCw size={14} />{cleanupBusy ? "检查中…" : "生成清理预览"}</button>{cleanup?.candidates?.length > 0 && <button className="danger-button" onClick={reclaimCleanup} disabled={cleanupBusy}><Trash2 size={14} />回收候选文件</button>}</div>} /><div className="storage-cleanup-summary"><div><strong>{cleanup?.candidates?.length || 0}</strong><span>无引用临时文件</span></div><div><strong>{((cleanup?.totalBytes || 0) / 1024 ** 2).toFixed(2)} MB</strong><span>预计可释放</span></div><div><strong>{cleanup?.policy?.provider || "local-filesystem"}</strong><span>存储提供方</span></div><div><strong>{cleanup?.cutoff ? cleanup.cutoff.slice(0, 10) : "—"}</strong><span>本次截止日期</span></div></div>{cleanup?.candidates?.length > 0 ? <div className="storage-cleanup-list">{cleanup.candidates.slice(0, 8).map((item) => <div key={item.relativePath}><span className="mono">{item.relativePath}</span><strong>{(item.bytes / 1024 ** 2).toFixed(2)} MB</strong><small>{item.reason} · {item.updatedAt?.replace("T", " ").slice(0, 19)}</small></div>)}{cleanup.candidates.length > 8 && <small className="muted-copy">还有 {cleanup.candidates.length - 8} 个候选文件,执行时会按服务端预览结果处理。</small>}</div> : <div className="empty-table">没有满足保留周期且未被生产证据引用的临时文件。先调整“存储 · storage.retention_days”后重新生成预览。</div>}</section>}
|
||||
{(section === "overview" || section === "deployment" || section === "storage") && <section className="studio-card wide-card"><SectionBar title="数据库快照备份" detail={`${readiness.backups?.count || 0} 个本地快照`} action={<button className="subtle" onClick={createBackup} disabled={backupBusy}><Database size={14} />{backupBusy ? "创建中…" : "立即创建快照"}</button>} /><div className="backup-summary-row"><div><Database size={18} /><strong>当前业务真源:SQLite</strong><span>快照写入 `data/backups/`,创建动作进入审计日志。</span></div><StatusBadge status={readiness.backups?.count ? "ready" : "needs-evidence"} label={readiness.backups?.count ? "已有备份" : "待创建"} /></div><div className="backup-list">{(readiness.backups?.backups || []).slice(0, 6).map((backup) => <div className="backup-row" key={backup.relativePath}><div><strong>{backup.relativePath}</strong><span>{formatAdminDate(backup.modifiedAt)} · {(Number(backup.bytes || 0) / 1024 ** 2).toFixed(2)} MB</span></div><StatusBadge status="active" label="可用" /></div>)}{!(readiness.backups?.backups || []).length && <div className="empty-table">还没有数据库快照。</div>}</div></section>}
|
||||
{(section === "overview" || section === "notifications") && <div className="enterprise-grid enterprise-grid-main"><section className="studio-card"><SectionBar title="通知渠道" detail="事件通知与 Webhook" action={<button className="subtle" onClick={sendNotificationTest}><Bell size={14} />发送测试</button>} /><div className="notification-list">{config.notifications.map((channel) => <div key={channel.id}><div className="notification-icon"><Bell size={16} /></div><div><strong>{channel.name}</strong><span>{channel.kind} · {channel.endpoint || "本地日志"}</span></div><button type="button" className={`switch-control small ${channel.enabled ? "on" : ""}`} onClick={() => toggleNotification(channel)}><span />{channel.enabled ? "启用" : "关闭"}</button></div>)}</div><div className="notification-delivery-list"><div className="subsection-heading"><strong>最近投递</strong><span>{deliveries.length} 条</span></div>{deliveries.slice(0, 5).map((delivery) => <div key={delivery.id}><span>{delivery.event_key}</span><StatusBadge status={delivery.status} label={delivery.status === "delivered" ? "已送达" : delivery.status === "blocked" ? "已阻断" : "失败"} /><small>{delivery.channel_name} · {delivery.created_at?.replace("T", " ").slice(0, 19)}</small></div>)}{!deliveries.length && <small className="muted-copy">还没有通知投递记录。</small>}</div></section><section className="studio-card"><SectionBar title="API 客户端" detail="Runner 与企业集成" /><div className="api-client-list">{config.apiClients.map((client) => <div key={client.id}><FileKey2 size={16} /><div><strong>{client.name}</strong><span>{client.client_key_preview || client.client_key}</span><div className="api-client-scope-list">{(config.apiClientScopes?.length ? config.apiClientScopes : fallbackApiClientScopes).map((scope) => <button key={scope.key} type="button" className={`scope-toggle ${client.scopes.includes(scope.key) ? "active" : ""}`} aria-pressed={client.scopes.includes(scope.key)} title={scope.description} onClick={() => toggleApiClientScope(client, scope.key)}>{scope.label}</button>)}</div></div><StatusBadge status={client.status} label={client.status === "active" ? "有效" : client.status === "revoked" ? "已撤销" : client.status} /><button className="icon-text-button" onClick={() => rotateClientKey(client)}><RefreshCw size={13} />轮换密钥</button><button className="icon-text-button" onClick={() => toggleApiClient(client)}><XCircle size={13} />{client.status === "active" ? "撤销" : "恢复"}</button></div>)}</div><form className="client-create-form" onSubmit={createClient}><input value={clientName} onChange={(event) => setClientName(event.target.value)} placeholder="新 API 客户端名称" /><button className="subtle" type="submit"><Plus size={15} />创建</button></form>{apiKeyNotice && <p className="form-notice"><CheckCircle2 size={14} />{apiKeyNotice}</p>}</section></div>}
|
||||
</>}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -730,6 +730,11 @@ export async function fetchMediaArtifacts(params = {}, overrides = {}) {
|
||||
return apiFetch(`/api/production/media-artifacts${query.toString() ? `?${query.toString()}` : ""}`, {}, overrides);
|
||||
}
|
||||
|
||||
export async function fetchMediaArtifactContent(artifactId, frame = "", overrides = {}) {
|
||||
const query = frame ? `?frame=${encodeURIComponent(frame)}` : "";
|
||||
return apiBlob(`/api/production/media-artifacts/${encodeURIComponent(artifactId)}/content${query}`, {}, overrides);
|
||||
}
|
||||
|
||||
export async function decideProductionReview(reviewId, body, overrides = {}) {
|
||||
return apiFetch(`/api/production/reviews/${encodeURIComponent(reviewId)}/decision`, { method: "POST", body: JSON.stringify(body) }, overrides);
|
||||
}
|
||||
@@ -905,6 +910,39 @@ export async function fetchStorageUsage(overrides = {}) {
|
||||
return apiFetch("/api/usage/storage", {}, overrides);
|
||||
}
|
||||
|
||||
export async function fetchStorageCleanupPreview(olderThanDays = "", overrides = {}) {
|
||||
const query = olderThanDays ? `?olderThanDays=${encodeURIComponent(olderThanDays)}` : "";
|
||||
return apiFetch(`/api/system/storage/cleanup-preview${query}`, {}, overrides);
|
||||
}
|
||||
|
||||
export async function reclaimStorage(body = {}, overrides = {}) {
|
||||
return apiFetch("/api/system/storage/reclaim", { method: "POST", body: JSON.stringify(body) }, overrides);
|
||||
}
|
||||
|
||||
export async function fetchWorkflowTemplates(overrides = {}, options = {}) {
|
||||
const query = options.includeArchived ? "?includeArchived=1" : "";
|
||||
return apiFetch(`/api/workflows/templates${query}`, {}, overrides);
|
||||
}
|
||||
|
||||
export async function createWorkflowTemplate(body, overrides = {}) {
|
||||
return apiFetch("/api/workflows/templates", { method: "POST", body: JSON.stringify(body) }, overrides);
|
||||
}
|
||||
|
||||
export async function updateWorkflowTemplate(templateId, body, overrides = {}) {
|
||||
return apiFetch(`/api/workflows/templates/${encodeURIComponent(templateId)}`, { method: "PATCH", body: JSON.stringify(body) }, overrides);
|
||||
}
|
||||
|
||||
export async function instantiateWorkflow(templateId, body = {}, overrides = {}) {
|
||||
return apiFetch(`/api/workflows/templates/${encodeURIComponent(templateId)}/instantiate`, { method: "POST", body: JSON.stringify(body) }, overrides);
|
||||
}
|
||||
|
||||
export async function fetchWorkflowRuns(overrides = {}, options = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (options.status) query.set("status", options.status);
|
||||
if (options.limit) query.set("limit", String(options.limit));
|
||||
return apiFetch(`/api/workflows/runs${query.toString() ? `?${query.toString()}` : ""}`, {}, overrides);
|
||||
}
|
||||
|
||||
export async function fetchCompositions(overrides = {}) {
|
||||
return apiFetch("/api/production/compositions", {}, overrides);
|
||||
}
|
||||
|
||||
+34
-2
@@ -5645,6 +5645,20 @@ body {
|
||||
.qa-review-summary span { color: var(--faint); font-size: 10px; }
|
||||
.qa-review-summary .ok strong { color: var(--accent); }
|
||||
.qa-review-summary .warn strong { color: #b16629; }
|
||||
.workflow-runner-panel { display: grid; gap: 12px; }
|
||||
.workflow-runner-form { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(180px, .7fr) auto; gap: 10px; align-items: end; }
|
||||
.workflow-runner-form label { display: grid; gap: 5px; color: var(--muted); font-size: 10px; font-weight: 800; }
|
||||
.workflow-runner-form select { min-height: 36px; padding: 0 9px; color: var(--ink); background: #fbfcfb; border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 12px; }
|
||||
.workflow-step-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 7px; }
|
||||
.workflow-step-strip > div { display: grid; gap: 4px; min-width: 0; padding: 9px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.workflow-step-strip span { color: var(--accent); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; font-weight: 800; }
|
||||
.workflow-step-strip strong { overflow: hidden; color: var(--ink); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workflow-step-strip small { overflow: hidden; color: var(--faint); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workflow-run-list { display: grid; gap: 7px; }
|
||||
.workflow-run-list > div { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 10px; align-items: center; padding: 8px 10px; border-top: 1px solid #e7ece8; }
|
||||
.workflow-run-list > div > div { display: grid; gap: 3px; min-width: 0; }
|
||||
.workflow-run-list strong { overflow: hidden; color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workflow-run-list span, .workflow-run-list small { color: var(--faint); font-size: 9px; }
|
||||
.qa-review-layout { display: grid; grid-template-columns: minmax(280px, .75fr) minmax(0, 1.25fr); gap: 14px; }
|
||||
.review-list { display: grid; gap: 6px; }
|
||||
.review-list > button { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto 15px; gap: 8px; align-items: center; width: 100%; min-height: 58px; padding: 8px; color: var(--muted); text-align: left; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
@@ -5660,9 +5674,18 @@ body {
|
||||
.review-evidence strong { color: var(--ink); font-size: 11px; }
|
||||
.review-evidence pre { max-height: 160px; margin: 0; overflow: auto; color: var(--blue); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; line-height: 1.5; white-space: pre-wrap; }
|
||||
.review-artifact-list { display: grid; gap: 7px; margin-top: 10px; }
|
||||
.review-artifact-list > div:not(.subsection-heading) { display: grid; gap: 3px; padding: 9px 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.review-artifact-list > div:not(.subsection-heading), .review-artifact-row { display: grid; gap: 3px; padding: 9px 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.review-artifact-row { width: 100%; color: inherit; text-align: left; cursor: pointer; }
|
||||
.review-artifact-row:hover, .review-artifact-row.selected { border-color: var(--accent); background: #eef7f0; }
|
||||
.review-artifact-list strong { overflow: hidden; color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.review-artifact-list small { color: var(--faint); font-size: 9px; line-height: 1.4; }
|
||||
.review-media-proof { display: grid; gap: 10px; margin-top: 12px; padding: 12px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.review-media-proof > img, .review-media-proof > video { display: block; width: 100%; max-height: 440px; object-fit: contain; background: #18221d; border-radius: 6px; }
|
||||
.review-media-proof > audio { width: 100%; }
|
||||
.frame-proof-strip { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.frame-proof-strip figure { display: grid; gap: 5px; margin: 0; }
|
||||
.frame-proof-strip img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: contain; background: #18221d; border-radius: 5px; }
|
||||
.frame-proof-strip figcaption { color: var(--faint); font-size: 9px; }
|
||||
.review-decision-actions { display: flex; gap: 7px; margin: 12px 0; flex-wrap: wrap; }
|
||||
.danger-button { display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 11px; color: #9b5b22; background: #fff3e5; border: 1px solid #efcfaa; border-radius: 7px; font-size: 11px; font-weight: 800; }
|
||||
.review-comments { display: grid; gap: 8px; padding-top: 12px; border-top: 1px solid var(--line); }
|
||||
@@ -5980,6 +6003,15 @@ body {
|
||||
.storage-admin-files > div { display: flex; justify-content: space-between; gap: 8px; color: var(--muted); font-size: 10px; }
|
||||
.storage-admin-files strong { overflow: hidden; color: var(--ink); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.storage-admin-files em { flex: 0 0 auto; color: var(--faint); font-style: normal; }
|
||||
.storage-cleanup-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 9px; margin-top: 12px; }
|
||||
.storage-cleanup-summary > div { display: grid; gap: 4px; min-width: 0; padding: 11px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.storage-cleanup-summary strong { overflow: hidden; color: var(--ink); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.storage-cleanup-summary span { color: var(--faint); font-size: 10px; }
|
||||
.storage-cleanup-list { display: grid; gap: 7px; margin-top: 12px; }
|
||||
.storage-cleanup-list > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 10px; padding: 9px 10px; background: #f7f9f7; border: 1px solid #e2e9e3; border-radius: 7px; }
|
||||
.storage-cleanup-list .mono { overflow: hidden; color: var(--ink); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.storage-cleanup-list strong { color: var(--ink); font-size: 10px; }
|
||||
.storage-cleanup-list small { grid-column: 1 / -1; color: var(--faint); font-size: 9px; }
|
||||
.notification-delivery-list { display: grid; gap: 7px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #e7ece8; }
|
||||
.notification-delivery-list > div:not(.subsection-heading) { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid #edf0ed; }
|
||||
.notification-delivery-list > div:last-child { border-bottom: 0; }
|
||||
@@ -6084,7 +6116,7 @@ body {
|
||||
.attempt-log-list > div { grid-template-columns: 1fr auto; }
|
||||
.attempt-log-list span { grid-column: 1 / -1; }
|
||||
.attempt-log-list small { grid-column: 1 / -1; }
|
||||
.dependency-selector, .storage-admin-grid { grid-template-columns: 1fr; }
|
||||
.dependency-selector, .storage-admin-grid, .storage-cleanup-summary, .workflow-runner-form { grid-template-columns: 1fr; }
|
||||
.compose-form { grid-template-columns: 1fr; }
|
||||
.composition-list > div { grid-template-columns: minmax(0, 1fr) auto; }
|
||||
.composition-list small { grid-column: 1 / -1; }
|
||||
|
||||
Reference in New Issue
Block a user