feat: add retention policy and stability verification
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
(() => {
|
||||
const state = { token: '', authenticated: false, queue: '', base: location.origin, consoleConfig: null, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null };
|
||||
const state = { token: '', authenticated: false, queue: '', base: location.origin, consoleConfig: null, retention: null, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const text = (id, value) => { const el = $(id); if (el) el.textContent = value; };
|
||||
const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅' };
|
||||
const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅', retention: '保留策略' };
|
||||
|
||||
function showToast(message) {
|
||||
const toast = $('toast'); toast.textContent = message; toast.classList.add('show');
|
||||
@@ -28,6 +28,7 @@
|
||||
text('page-title', pageNames[next]);
|
||||
if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
if (next === 'retention' && state.authenticated) loadRetention();
|
||||
}
|
||||
function redirectToLogin() {
|
||||
const returnTo = `${location.pathname}${location.search}${location.hash}`;
|
||||
@@ -62,6 +63,80 @@
|
||||
// 配置只是已登录会话的默认队列,失败时业务 API 会返回明确错误。
|
||||
}
|
||||
}
|
||||
function renderRetention(policy) {
|
||||
if (!policy) return;
|
||||
state.retention = policy;
|
||||
$('retention-enabled').checked = Boolean(policy.enabled);
|
||||
$('retention-idempotency').value = policy.idempotency_days ?? '';
|
||||
$('retention-task').value = policy.task_history_days ?? '';
|
||||
$('retention-workflow').value = policy.workflow_history_days ?? '';
|
||||
$('retention-event').value = policy.event_log_days ?? '';
|
||||
$('retention-delivery').value = policy.delivery_history_days ?? '';
|
||||
$('retention-inbox').value = policy.inbox_history_days ?? '';
|
||||
$('retention-audit').value = policy.audit_log_days ?? '';
|
||||
text('retention-source', policy.source === 'database' ? '已保存配置' : '默认策略');
|
||||
text('retention-revision', policy.revision ? String(policy.revision) : '默认');
|
||||
text('retention-updated', policy.updated_at ? formatTime(policy.updated_at) : '尚未保存');
|
||||
}
|
||||
async function loadRetention() {
|
||||
try {
|
||||
const response = await request('/console/retention');
|
||||
renderRetention(extractData(response));
|
||||
} catch (error) {
|
||||
$('retention-error').textContent = error.message || '保留策略读取失败';
|
||||
}
|
||||
}
|
||||
function retentionNumber(id) {
|
||||
const value = Number($(id).value);
|
||||
return Number.isSafeInteger(value) && value >= 1 && value <= 3650 ? value : 0;
|
||||
}
|
||||
async function submitRetention(event) {
|
||||
event.preventDefault();
|
||||
const fields = { idempotency: 'retention-idempotency', task: 'retention-task', workflow: 'retention-workflow', event: 'retention-event', delivery: 'retention-delivery', inbox: 'retention-inbox', audit: 'retention-audit' };
|
||||
const values = Object.fromEntries(Object.entries(fields).map(([key, id]) => [key, retentionNumber(id)]));
|
||||
if (Object.values(values).some(value => !value)) { $('retention-error').textContent = '所有保留天数必须是 1 到 3650 的整数。'; return; }
|
||||
const reason = $('retention-reason').value.trim();
|
||||
if (!reason) { $('retention-error').textContent = '请填写变更原因。'; return; }
|
||||
const button = $('retention-submit'); button.disabled = true; $('retention-error').textContent = '';
|
||||
const current = state.retention || {};
|
||||
try {
|
||||
const response = await request('/console/retention', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': operationKey('retention-policy') }, body: JSON.stringify({ expected_revision: String(current.revision || 0), enabled: $('retention-enabled').checked, idempotency_days: values.idempotency, task_history_days: values.task, workflow_history_days: values.workflow, event_log_days: values.event, delivery_history_days: values.delivery, inbox_history_days: values.inbox, audit_log_days: values.audit, reason }) });
|
||||
renderRetention(extractData(response)); $('retention-reason').value = ''; showToast('保留策略已保存,已写入管理审计');
|
||||
} catch (error) { $('retention-error').textContent = error.message || '保留策略保存失败'; }
|
||||
finally { button.disabled = false; }
|
||||
}
|
||||
function renderRetentionPreview(preview) {
|
||||
const el = $('retention-preview');
|
||||
const items = Array.isArray(preview?.items) ? preview.items : [];
|
||||
if (!items.length) { el.innerHTML = '<span>当前策略没有可预览的候选数据。</span>'; return; }
|
||||
const labels = { tasks: '任务历史', workflows: '流程历史', events: '事件日志', audit: '管理审计', deliveries: '交付历史', inbox: 'Inbox 历史', idempotency: '幂等记录' };
|
||||
el.innerHTML = `<div class="retention-preview-head"><b>候选预览</b><small>仅展示样本,不会删除数据</small></div>${items.map(item => `<div class="retention-preview-row"><span>${escapeHTML(labels[item.kind] || item.kind)}</span><b>${escapeHTML(String(item.count || 0))} 条</b><small>截止 ${escapeHTML(formatTime(item.cutoff))}${item.more ? ' · 还有更多' : ''}</small></div>`).join('')}`;
|
||||
}
|
||||
async function previewRetention() {
|
||||
const button = $('retention-preview-button'); button.disabled = true; $('retention-error').textContent = '';
|
||||
try {
|
||||
const response = await request('/console/retention/preview');
|
||||
renderRetentionPreview(extractData(response));
|
||||
} catch (error) { $('retention-error').textContent = error.message || '候选预览失败'; }
|
||||
finally { button.disabled = false; }
|
||||
}
|
||||
async function exportRetention() {
|
||||
const button = $('retention-export-button'); button.disabled = true; $('retention-error').textContent = '';
|
||||
try {
|
||||
const kind = $('retention-kind').value;
|
||||
const current = state.retention || {};
|
||||
const response = await request('/console/retention/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind, limit: 20, expected_revision: String(current.revision || 0) }) });
|
||||
const snapshot = extractData(response);
|
||||
const content = String(snapshot.content || '');
|
||||
if (!content) throw new Error('服务端返回了空的归档快照');
|
||||
const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob); const anchor = document.createElement('a');
|
||||
anchor.href = url; anchor.download = `orbit-retention-${kind}-${new Date().toISOString().replace(/[:.]/g, '-')}.json`; anchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
showToast(`快照已下载 · SHA256 ${String(snapshot.sha256 || '').slice(0, 12)}…`);
|
||||
} catch (error) { $('retention-error').textContent = error.message || '归档快照导出失败'; }
|
||||
finally { button.disabled = false; }
|
||||
}
|
||||
function renderConnectionSummary() {
|
||||
text('summary-api', state.base || location.origin);
|
||||
text('summary-api-hint', state.connected ? '接口响应正常,按 10 秒周期刷新' : 'API 地址自动跟随当前页面');
|
||||
@@ -473,13 +548,13 @@
|
||||
finally { state.loading = false; $('refresh-button').classList.remove('spinning'); }
|
||||
}
|
||||
async function logout() { await fetch('/console/auth/logout', { method: 'POST', credentials: 'same-origin' }); redirectToLogin(); }
|
||||
$('logout-hero').addEventListener('click', logout); $('logout-button').addEventListener('click', logout); $('logout-inline').addEventListener('click', logout); $('refresh-button').addEventListener('click', refresh);
|
||||
$('logout-hero').addEventListener('click', logout); $('logout-button').addEventListener('click', logout); $('logout-inline').addEventListener('click', logout); $('refresh-button').addEventListener('click', refresh); $('retention-form').addEventListener('submit', submitRetention); $('retention-preview-button').addEventListener('click', previewRetention); $('retention-export-button').addEventListener('click', exportRetention);
|
||||
$('close-task-drawer').addEventListener('click', closeTaskDrawer); $('task-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'task-drawer-backdrop') closeTaskDrawer(); }); $('action-cancel').addEventListener('click', closeActionForm); $('task-action-form').addEventListener('submit', submitAction); $('task-cancel-button').addEventListener('click', () => openActionForm('cancel')); $('task-retry-button').addEventListener('click', () => openActionForm('retry')); $('close-schedule-drawer').addEventListener('click', closeScheduleDrawer); $('schedule-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'schedule-drawer-backdrop') closeScheduleDrawer(); }); $('schedule-control-button').addEventListener('click', openScheduleAction); $('schedule-action-cancel').addEventListener('click', closeScheduleAction); $('schedule-action-form').addEventListener('submit', submitScheduleAction); $('close-workflow-drawer').addEventListener('click', closeWorkflowDrawer); $('workflow-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'workflow-drawer-backdrop') closeWorkflowDrawer(); }); $('workflow-control-button').addEventListener('click', () => openWorkflowAction(state.selectedWorkflow?.paused ? 'RESUME' : 'PAUSE')); $('workflow-cancel-button').addEventListener('click', () => openWorkflowAction('CANCEL')); $('workflow-action-cancel').addEventListener('click', closeWorkflowAction); $('workflow-action-form').addEventListener('submit', submitWorkflowAction); $('close-subscription-drawer').addEventListener('click', closeSubscriptionDrawer); $('subscription-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'subscription-drawer-backdrop') closeSubscriptionDrawer(); }); $('subscription-control-button').addEventListener('click', openSubscriptionAction); $('subscription-action-cancel').addEventListener('click', closeSubscriptionAction); $('subscription-action-form').addEventListener('submit', submitSubscriptionAction); $('subscription-retry-cancel').addEventListener('click', closeSubscriptionRetry); $('subscription-retry-form').addEventListener('submit', submitSubscriptionRetry); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('workflow-action-form').hidden) closeWorkflowAction(); else if (!$('subscription-action-form').hidden) closeSubscriptionAction(); else if (!$('subscription-retry-form').hidden) closeSubscriptionRetry(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); else if (!$('workflow-drawer-backdrop').hidden) closeWorkflowDrawer(); else if (!$('subscription-drawer-backdrop').hidden) closeSubscriptionDrawer(); } });
|
||||
$('workflow-recovery-actions').addEventListener('click', event => { const button = event.target.closest('[data-recovery-action]'); if (!button) return; let context = {}; try { context = JSON.parse(button.dataset.recoveryContext || '{}'); } catch { showToast('恢复按钮上下文无效,请重新读取流程'); return; } openWorkflowRecovery(button.dataset.recoveryAction, context); }); $('workflow-recovery-cancel').addEventListener('click', closeWorkflowRecovery); $('workflow-recovery-form').addEventListener('submit', submitWorkflowRecovery);
|
||||
document.addEventListener('keydown', event => { if (event.key === 'Escape' && !$('workflow-recovery-form').hidden) closeWorkflowRecovery(); });
|
||||
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', event => { event.preventDefault(); setView(button.dataset.view); }));
|
||||
window.addEventListener('hashchange', () => setView(currentView(), false));
|
||||
setView(currentView(), false);
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(() => refresh()); }).catch(() => redirectToLogin());
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(() => Promise.all([loadRetention(), refresh()])); }).catch(() => redirectToLogin());
|
||||
state.timer = setInterval(() => { if (state.authenticated) refresh(); }, 10000);
|
||||
})();
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<button class="nav-item" data-view="schedules"><span class="nav-icon">◷</span><span>定时计划</span><em>03</em></button>
|
||||
<button class="nav-item" data-view="workflows"><span class="nav-icon">⌘</span><span>流程实例</span><em>04</em></button>
|
||||
<button class="nav-item" data-view="subscriptions"><span class="nav-icon">⌁</span><span>结果订阅</span><em>05</em></button>
|
||||
<button class="nav-item" data-view="retention"><span class="nav-icon">⌛</span><span>保留策略</span><em>06</em></button>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="rail-card">
|
||||
@@ -129,6 +130,37 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="page-view" id="view-retention" data-page="retention">
|
||||
<section class="content-grid single-column">
|
||||
<article class="panel wide-panel retention-panel">
|
||||
<div class="panel-heading"><div><span class="panel-kicker">DATA / RETENTION</span><h2>历史数据保留策略</h2></div><span class="connection-state ready" id="retention-source">默认策略</span></div>
|
||||
<p class="panel-description">这里配置各类历史记录的最短保留时间。保存策略只记录配置,不会立即删除数据;归档执行器还会核对终态、未确认结果、备份和审计要求。</p>
|
||||
<form id="retention-form" class="retention-form">
|
||||
<div class="retention-switch"><div><b>允许归档执行器使用策略</b><small>默认关闭。打开后仍需独立归档任务执行,未确认任务、交付和 Inbox 不会按窗口清理。</small></div><label class="switch"><input id="retention-enabled" type="checkbox"><span></span></label></div>
|
||||
<div class="retention-grid">
|
||||
<label class="retention-field"><span>幂等记录</span><small>天</small><input id="retention-idempotency" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field"><span>任务历史</span><small>天</small><input id="retention-task" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field"><span>流程历史</span><small>天</small><input id="retention-workflow" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field"><span>事件日志</span><small>天</small><input id="retention-event" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field"><span>交付历史</span><small>天</small><input id="retention-delivery" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field"><span>Inbox 历史</span><small>天</small><input id="retention-inbox" type="number" min="1" max="3650" required></label>
|
||||
<label class="retention-field retention-field-wide"><span>管理审计</span><small>天</small><input id="retention-audit" type="number" min="1" max="3650" required></label>
|
||||
</div>
|
||||
<div class="retention-meta"><span>当前版本 <b id="retention-revision">默认</b></span><span>最后修改 <b id="retention-updated">—</b></span></div>
|
||||
<label class="field-label" for="retention-reason">变更原因</label>
|
||||
<textarea id="retention-reason" rows="3" maxlength="1024" placeholder="说明为什么调整历史保留窗口"></textarea>
|
||||
<div class="action-form-buttons"><button class="primary-button compact" id="retention-submit" type="submit">保存保留策略 <span>→</span></button></div>
|
||||
<div class="retention-tools">
|
||||
<label class="retention-select"><span>历史类型</span><select id="retention-kind"><option value="tasks">任务历史</option><option value="workflows">流程历史</option><option value="events">事件日志</option><option value="audit">管理审计</option><option value="deliveries">交付历史</option><option value="inbox">Inbox 历史</option><option value="idempotency">幂等记录</option></select></label>
|
||||
<div class="retention-tool-buttons"><button class="secondary-button compact" id="retention-preview-button" type="button">预览候选</button><button class="secondary-button compact" id="retention-export-button" type="button">导出只读快照 <span>↓</span></button></div>
|
||||
</div>
|
||||
<div class="retention-preview" id="retention-preview" aria-live="polite"><span>保存策略后可预览当前窗口下的候选数据;预览不会删除任何记录。</span></div>
|
||||
<div class="action-error" id="retention-error" role="alert"></div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="page-footer"><span><span class="footer-orbit">◉</span> Orbit Console · 星轨控制台</span><span>数据每 10 秒刷新 · <a href="/version" target="_blank" rel="noreferrer">查看服务版本</a></span></footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
.detail-row-action{border:1px solid rgba(255,179,107,.28);border-radius:8px;padding:6px 9px;color:var(--orange);background:rgba(255,179,107,.07);font:10px inherit;cursor:pointer;white-space:nowrap}.detail-row-action:hover{border-color:rgba(255,179,107,.65);color:#ffe0bb;background:rgba(255,179,107,.13)}
|
||||
.recovery-block{border-color:rgba(255,179,107,.2);background:linear-gradient(135deg,rgba(255,179,107,.045),rgba(98,230,212,.025))}.recovery-intro{margin:0 0 12px;color:#8399b0;font-size:10px;line-height:1.6}.recovery-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.recovery-action-button{display:flex;flex-direction:column;align-items:flex-start;gap:4px;border:1px solid rgba(255,179,107,.2);border-radius:10px;padding:10px 11px;color:#e6f0ff;background:rgba(4,12,23,.4);text-align:left;cursor:pointer;transition:border-color .2s,background .2s,transform .2s}.recovery-action-button:hover,.recovery-action-button:focus{outline:0;border-color:rgba(98,230,212,.6);background:rgba(98,230,212,.08);transform:translateY(-1px)}.recovery-action-button b{font-size:10px;font-weight:650}.recovery-action-button small{color:#7890a8;font-size:9px;line-height:1.4}.recovery-form{border-color:rgba(255,179,107,.22)}.recovery-snapshot{margin:0 0 14px;padding:8px 10px;border-radius:8px;background:rgba(98,230,212,.06);color:#8db4b6;font:10px ui-monospace,SFMono-Regular,Menlo,monospace}.recovery-inline-fields{display:grid;grid-template-columns:150px minmax(0,1fr);gap:10px}.recovery-inline-fields select{width:100%;border:1px solid rgba(156,183,218,.15);outline:0;border-radius:9px;background:rgba(4,12,23,.45);color:#dceafb;font:inherit;font-size:10px;padding:10px}.recovery-inline-fields select:focus{border-color:rgba(98,230,212,.58);box-shadow:0 0 0 3px rgba(98,230,212,.07)}@media(max-width:780px){.recovery-action-grid{grid-template-columns:1fr}.recovery-inline-fields{grid-template-columns:1fr}}
|
||||
.overview-grid{grid-template-columns:minmax(0,1.45fr) minmax(300px,.7fr)}.stats-panel,.connection-summary-panel{min-height:326px}.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.stat-tile{padding:14px 13px;border:1px solid rgba(156,183,218,.1);border-radius:13px;background:rgba(4,13,24,.28)}.stat-tile span{display:block;color:#7188a3;font-size:9px}.stat-tile strong{display:block;margin-top:10px;color:#e4f1fc;font-size:23px;letter-spacing:-.04em}.stat-tile small{display:block;margin-top:5px;color:#607791;font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.distribution-block{margin-top:22px;padding-top:18px;border-top:1px solid rgba(156,183,218,.1)}.distribution-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;color:#afc2d6;font-size:11px}.distribution-heading small{color:#687f99;font-size:9px}.distribution-list{display:grid;gap:10px}.distribution-row{display:grid;grid-template-columns:42px minmax(0,1fr) 28px;gap:10px;align-items:center;color:#7187a1;font-size:9px}.distribution-row b{text-align:right;color:#c1d2e2;font-size:10px;font-weight:500}.status-track{height:5px;border-radius:5px;background:#1c3048;overflow:hidden}.status-track i{display:block;width:0;height:100%;border-radius:5px;transition:width .45s ease}.distribution-row:nth-child(1) .status-track i{background:var(--cyan)}.distribution-row:nth-child(2) .status-track i{background:var(--blue)}.distribution-row:nth-child(3) .status-track i{background:var(--purple)}.distribution-row:nth-child(4) .status-track i{background:var(--orange)}.connection-summary-copy{display:flex;align-items:center;gap:12px;padding:14px;border:1px solid rgba(98,230,212,.12);border-radius:14px;background:rgba(4,15,26,.3)}.connection-summary-icon{display:grid;place-items:center;width:33px;height:33px;border-radius:11px;background:rgba(98,230,212,.1);color:var(--cyan);font-size:17px}.connection-summary-copy b{display:block;color:#d5e5f4;font-size:12px;max-width:230px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.connection-summary-copy small{display:block;margin-top:5px;color:#68809a;font-size:9px}.connection-summary-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;margin-top:12px;border:1px solid rgba(156,183,218,.1);border-radius:13px;overflow:hidden}.connection-summary-grid>div{padding:12px;background:rgba(4,13,24,.25)}.connection-summary-grid span{display:block;color:#647b96;font-size:9px;margin-bottom:5px}.connection-summary-grid b{display:block;color:#b9cde0;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.connection-summary-note{min-height:31px;margin:15px 0 14px;color:#6f86a0;font-size:10px;line-height:1.6}.connection-summary-panel>.quiet-button{width:100%;justify-content:center}.connection-settings-intro{margin:0 0 18px}.connection-drawer{width:min(510px,100%)}@media(max-width:1200px){.stat-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:780px){.overview-grid{grid-template-columns:1fr}.stat-grid{grid-template-columns:repeat(2,1fr)}.connection-drawer{width:100%}}
|
||||
.retention-panel{max-width:980px}.retention-panel .panel-description{max-width:760px}.retention-form{max-width:820px}.retention-switch{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:16px 17px;border:1px solid rgba(98,230,212,.16);border-radius:14px;background:rgba(4,15,26,.34)}.retention-switch b{display:block;color:#dceafb;font-size:11px;font-weight:550}.retention-switch small{display:block;margin-top:6px;color:#7086a0;font-size:9px;line-height:1.5}.switch{position:relative;display:block;width:43px;height:24px;flex:0 0 43px}.switch input{position:absolute;opacity:0;width:0;height:0}.switch span{position:absolute;inset:0;border-radius:999px;background:#243852;cursor:pointer;transition:.2s}.switch span:before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;border-radius:50%;background:#8ca3ba;transition:.2s}.switch input:checked+span{background:rgba(98,230,212,.55);box-shadow:0 0 18px rgba(98,230,212,.18)}.switch input:checked+span:before{transform:translateX(19px);background:#ecfffb}.retention-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:11px;margin-top:15px}.retention-field{display:block;padding:14px;border:1px solid rgba(156,183,218,.1);border-radius:13px;background:rgba(4,13,24,.28)}.retention-field span{color:#afc2d6;font-size:10px}.retention-field small{float:right;color:#637b96;font-size:9px}.retention-field input{display:block;width:100%;margin-top:12px;border:1px solid rgba(156,183,218,.15);outline:0;border-radius:9px;background:rgba(4,12,23,.45);color:#dceafb;font:inherit;font-size:13px;padding:10px}.retention-field input:focus{border-color:rgba(98,230,212,.58);box-shadow:0 0 0 3px rgba(98,230,212,.07)}.retention-field-wide{grid-column:span 3}.retention-meta{display:flex;gap:25px;margin-top:16px;color:#637b96;font-size:9px}.retention-meta b{margin-left:5px;color:#afc2d6;font-weight:500}.retention-form>.field-label{margin-top:22px}.retention-form>textarea{width:100%;border:1px solid rgba(156,183,218,.15);outline:0;border-radius:9px;background:rgba(4,12,23,.45);color:#dceafb;font:inherit;font-size:10px;padding:10px;resize:vertical}.retention-form>textarea:focus{border-color:rgba(98,230,212,.58);box-shadow:0 0 0 3px rgba(98,230,212,.07)}.retention-form .action-form-buttons{justify-content:flex-start}.retention-form .action-error{min-height:18px}@media(max-width:780px){.retention-grid{grid-template-columns:1fr 1fr}.retention-field-wide{grid-column:span 2}.retention-meta{flex-direction:column;gap:7px}}
|
||||
.retention-tools{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-top:22px;padding-top:18px;border-top:1px solid rgba(156,183,218,.1)}.retention-select{display:flex;align-items:center;gap:10px;color:#9eb4cb;font-size:10px}.retention-select select{min-width:150px;border:1px solid rgba(156,183,218,.15);border-radius:9px;background:#0a1a2e;color:#dceafb;font:inherit;padding:9px 11px;outline:0}.retention-select select:focus{border-color:rgba(98,230,212,.58)}.retention-tool-buttons{display:flex;gap:9px}.secondary-button.compact{min-height:37px;padding:0 14px}.retention-preview{margin-top:13px;padding:13px 14px;border:1px solid rgba(156,183,218,.1);border-radius:11px;background:rgba(4,13,24,.28);color:#7189a3;font-size:10px;line-height:1.5}.retention-preview-head{display:flex;justify-content:space-between;gap:10px;padding-bottom:8px;color:#c9d8e8}.retention-preview-head small{color:#7189a3}.retention-preview-row{display:grid;grid-template-columns:1fr auto 1.6fr;gap:12px;padding:7px 0;border-top:1px solid rgba(156,183,218,.08)}.retention-preview-row b{color:#9beade;font-weight:600}.retention-preview-row small{color:#7189a3;text-align:right}@media(max-width:780px){.retention-tools{align-items:stretch;flex-direction:column}.retention-tool-buttons{justify-content:flex-start}.retention-preview-row{grid-template-columns:1fr auto}.retention-preview-row small{grid-column:span 2;text-align:left}}
|
||||
|
||||
Reference in New Issue
Block a user