feat: polish alpine settings and wizard feedback
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/platform-key-usage`
|
||||
### `feat/alpine-polish`
|
||||
|
||||
Status: completed
|
||||
Status: in_progress
|
||||
|
||||
DoD:
|
||||
- platform API keys authenticate real machine access
|
||||
- `last_used_at` updates from successful platform key usage
|
||||
- UI API keys page reflects honest runtime usage rather than seed-only metadata
|
||||
- Alpine UI pages have consistent feedback and shell state
|
||||
- Settings page exposes only honest live functionality and clearly marks deferred capabilities
|
||||
- Wizard live actions have stable loading and feedback behavior on real backend requests
|
||||
|
||||
## Next
|
||||
|
||||
|
||||
@@ -654,6 +654,21 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-callout.info-callout-subtle {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border-color: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.info-callout.is-success {
|
||||
background: rgba(13, 148, 136, 0.07);
|
||||
border-color: rgba(13, 148, 136, 0.28);
|
||||
}
|
||||
|
||||
.info-callout.is-error {
|
||||
background: rgba(248, 113, 113, 0.08);
|
||||
border-color: rgba(248, 113, 113, 0.28);
|
||||
}
|
||||
|
||||
.info-callout-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@@ -662,6 +677,19 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.info-callout.info-callout-subtle .info-callout-icon {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-callout.is-error .info-callout-icon,
|
||||
.info-callout.is-error .info-callout-title {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.info-callout.is-success .info-callout-title {
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.info-callout-body { flex: 1; }
|
||||
|
||||
.info-callout-title {
|
||||
@@ -1404,6 +1432,14 @@
|
||||
}
|
||||
.btn-ghost-sm:hover { border-color: var(--text-secondary); color: var(--text-primary); }
|
||||
|
||||
.btn-primary-sm:disabled,
|
||||
.btn-ghost-sm:disabled,
|
||||
.btn-primary-sm.is-busy,
|
||||
.btn-ghost-sm.is-busy {
|
||||
opacity: 0.68;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
HTTP method picker (Step 4 REST)
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
+30
-258
@@ -96,7 +96,7 @@
|
||||
<div class="page-header" style="margin-bottom: 24px;">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title">Account settings</h1>
|
||||
<p class="page-subtitle">Manage your personal profile, security, and notification preferences.</p>
|
||||
<p class="page-subtitle">Manage the live profile, password and workspace settings available in this build. Planned capabilities stay visible, but they do not pretend to be wired yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,10 +215,10 @@
|
||||
<div class="avatar-upload-actions">
|
||||
<div class="avatar-name" id="profile-display-name">Operator</div>
|
||||
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
|
||||
<div class="field-hint">Profile photo upload is not available yet.</div>
|
||||
<div class="field-hint">Your avatar is generated from your display name and email.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-bottom:16px;">Your name and email are stored on the backend and used across the console.</div>
|
||||
<div class="field-hint" style="margin-bottom:16px;">Your name and email are stored on the backend and used across the console and session identity.</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
@@ -245,196 +245,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ■ MEMBERS — moved to workspace-setup.html ■ -->
|
||||
<div id="section-members" style="display:none;">
|
||||
|
||||
<!-- Members card -->
|
||||
<div class="section-card" style="margin-bottom: 16px;">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title">Team members</div>
|
||||
<div class="section-card-subtitle" id="members-count-label">4 members</div>
|
||||
</div>
|
||||
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="toggleInviteForm()">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" style="margin-right:4px;"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
Invite member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Invite form -->
|
||||
<div id="invite-form" style="display:none; padding: 0 20px 16px; border-bottom: 1px solid var(--border-subtle);">
|
||||
<div style="display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
|
||||
<div class="field-group" style="flex:1; min-width:200px; margin-bottom:0;">
|
||||
<label class="field-label">Email address</label>
|
||||
<input class="field-input" id="invite-email" type="email" placeholder="colleague@company.com" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group" style="width:150px; margin-bottom:0;">
|
||||
<label class="field-label">Role</label>
|
||||
<select class="field-input" id="invite-role">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="developer" selected>Developer</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;white-space:nowrap;" onclick="sendInvite()">Send invite</button>
|
||||
<button class="btn-secondary" type="button" style="padding:7px 12px;font-size:13px;" onclick="toggleInviteForm()">Cancel</button>
|
||||
</div>
|
||||
<div id="invite-success" style="display:none; margin-top:10px; font-size:13px; color: #3fb950;">
|
||||
✓ Invite sent successfully.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members list -->
|
||||
<div class="section-card-body" style="padding: 4px 20px 8px;" id="members-list">
|
||||
<!-- Owner row (you) -->
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#0d9488,#6366f1);">AT</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Operator <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(you)</span></div>
|
||||
<div class="member-email">operator@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">Today</div>
|
||||
<span class="member-role-badge role-owner">Owner</span>
|
||||
</div>
|
||||
<!-- Admin -->
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#7c3aed,#4f46e5);">BS</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Blake Smith</div>
|
||||
<div class="member-email">blake@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">2 days ago</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'blake@acme.com')">
|
||||
<option value="admin" selected>Admin</option>
|
||||
<option value="developer">Developer</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Developer -->
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#d29922,#f59e0b);">CL</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Carol Lane</div>
|
||||
<div class="member-email">carol@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">1 week ago</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'carol@acme.com')">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="developer" selected>Developer</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Viewer -->
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#0891b2,#06b6d4);">DM</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Dan Morgan</div>
|
||||
<div class="member-email">dan@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">Never</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'dan@acme.com')">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="developer">Developer</option>
|
||||
<option value="viewer" selected>Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending invites -->
|
||||
<div id="pending-invites" style="border-top: 1px solid var(--border-subtle); padding: 12px 20px 16px;">
|
||||
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">Pending invites <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">1</span></div>
|
||||
<div class="member-row" id="invite-john">
|
||||
<div class="member-avatar" style="background:var(--bg-canvas);border:1.5px dashed var(--border);color:var(--text-muted);font-size:16px;">?</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">john@startup.com</div>
|
||||
<div class="member-email">Invited 3 days ago · not yet accepted</div>
|
||||
</div>
|
||||
<div class="member-last-active">—</div>
|
||||
<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">Developer</span>
|
||||
<button class="member-remove-btn" onclick="revokeInvite(this)" title="Revoke invite">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles & permissions reference -->
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title">Roles & permissions</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="padding: 4px 0 8px; gap: 0;">
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;">Owner</span>
|
||||
<div class="role-def-text">Full workspace control, billing access, can transfer or delete the workspace. Only one owner per workspace.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;">Admin</span>
|
||||
<div class="role-def-text">Manage members and roles, create and edit all operations and agents, manage API keys.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-developer" style="min-width:80px;text-align:center;">Developer</span>
|
||||
<div class="role-def-text">Create and edit operations and agents. Cannot manage members or workspace settings.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-viewer" style="min-width:80px;text-align:center;">Viewer</span>
|
||||
<div class="role-def-text">Read-only access to operations, agents, and logs. Cannot create or modify anything.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleInviteForm() {
|
||||
var f = document.getElementById('invite-form');
|
||||
var s = document.getElementById('invite-success');
|
||||
if (f.style.display === 'none') {
|
||||
f.style.display = '';
|
||||
s.style.display = 'none';
|
||||
document.getElementById('invite-email').focus();
|
||||
} else {
|
||||
f.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function sendInvite() {
|
||||
var email = document.getElementById('invite-email').value.trim();
|
||||
if (!email || !email.includes('@')) return;
|
||||
document.getElementById('invite-success').style.display = '';
|
||||
document.getElementById('invite-email').value = '';
|
||||
}
|
||||
function updateRole(select, email) {
|
||||
// In production: API call to update role
|
||||
console.log('Role updated:', email, '->', select.value);
|
||||
}
|
||||
function removeMember(btn, name) {
|
||||
if (!confirm('Remove ' + name + ' from this workspace?')) return;
|
||||
btn.closest('.member-row').remove();
|
||||
updateMembersCount();
|
||||
}
|
||||
function revokeInvite(btn) {
|
||||
btn.closest('.member-row').remove();
|
||||
var pending = document.getElementById('pending-invites');
|
||||
if (pending && !pending.querySelector('.member-row')) {
|
||||
pending.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function updateMembersCount() {
|
||||
var rows = document.querySelectorAll('#members-list .member-row');
|
||||
var label = document.getElementById('members-count-label');
|
||||
if (label) label.textContent = rows.length + ' member' + (rows.length !== 1 ? 's' : '');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ■ SECURITY ■ -->
|
||||
<div id="section-security" class="section-anchor">
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
@@ -464,8 +274,15 @@
|
||||
|
||||
<div style="padding: 4px 20px 16px;">
|
||||
<div class="settings-section-divider" style="margin-top:0;"></div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">Advanced security</div>
|
||||
<div class="field-hint" style="margin-bottom:12px;">Two-factor authentication, passkeys and session management are not implemented yet. The current live security flow is password-based login with HttpOnly session cookies.</div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">Planned security capabilities</div>
|
||||
<div class="callout warning" style="margin-bottom:0;">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Not available in this build.</strong> Two-factor authentication, passkeys and multi-session inventory are planned, but the current live flow is password-based login with one HttpOnly browser session.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -477,7 +294,7 @@
|
||||
<div class="member-row">
|
||||
<div class="member-info">
|
||||
<div class="member-name">Browser session — <span style="color:var(--green);font-size:12px;">current</span></div>
|
||||
<div class="member-email">Session expires according to server-side auth settings. Use Log out to revoke the current browser session.</div>
|
||||
<div class="member-email" id="settings-session-summary">Loading current session details…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,73 +308,28 @@
|
||||
<div class="section-card-title">Notification preferences</div>
|
||||
</div>
|
||||
<div style="padding: 8px 20px 16px;">
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">Operation error spike</div>
|
||||
<div class="toggle-setting-desc">Alert when error rate exceeds 5% over a 5-minute window.</div>
|
||||
<div class="callout info" style="margin-bottom:14px;">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Notifications are not wired yet.</strong> Alert routing and per-user preferences are planned, but this build does not persist or deliver notification settings.
|
||||
</div>
|
||||
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">Upstream latency degradation</div>
|
||||
<div class="toggle-setting-desc">Alert when p99 latency exceeds the baseline by 3×.</div>
|
||||
<div class="planned-list">
|
||||
<div class="planned-item">
|
||||
<div class="planned-item-title">Operation error spike</div>
|
||||
<div class="planned-item-desc">Alert when error rate exceeds a rolling threshold for a published tool.</div>
|
||||
</div>
|
||||
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">Quota approaching limit</div>
|
||||
<div class="toggle-setting-desc">Email when any operation reaches 80% of its monthly quota.</div>
|
||||
<div class="planned-item">
|
||||
<div class="planned-item-title">Upstream latency degradation</div>
|
||||
<div class="planned-item-desc">Notify operators when p99 latency rises sharply above the baseline.</div>
|
||||
</div>
|
||||
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">New team member joined</div>
|
||||
<div class="toggle-setting-desc">Notify workspace admins when someone accepts an invite.</div>
|
||||
<div class="planned-item">
|
||||
<div class="planned-item-title">Quota and usage digests</div>
|
||||
<div class="planned-item-desc">Deliver periodic summaries for quota consumption, errors and workspace activity.</div>
|
||||
</div>
|
||||
<div class="toggle-sm" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="toggle-setting-row">
|
||||
<div class="toggle-setting-text">
|
||||
<div class="toggle-setting-label">Weekly usage digest</div>
|
||||
<div class="toggle-setting-desc">Email summary of invocations, errors and latency each Monday.</div>
|
||||
</div>
|
||||
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-top:12px;">Notification preferences are local UI placeholders for now and are not synced to the backend yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ■ DANGER ZONE — moved to workspace-setup.html ■ -->
|
||||
<div id="section-danger" style="display:none;">
|
||||
<div class="section-card" style="border-color: rgba(248,81,73,0.25);">
|
||||
<div class="section-card-header" style="border-bottom-color: rgba(248,81,73,0.15);">
|
||||
<div class="section-card-title" style="color: var(--red);">Danger zone</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="danger-zone-action">
|
||||
<div class="danger-zone-text">
|
||||
<div class="danger-zone-title">Rename workspace</div>
|
||||
<div class="danger-zone-desc">Changes the workspace slug used in all API endpoints. Existing API keys will continue to work. All existing tool call references will break.</div>
|
||||
</div>
|
||||
<button class="btn-danger" id="settings-ws-rename-btn" type="button">Rename</button>
|
||||
</div>
|
||||
<div class="danger-zone-action">
|
||||
<div class="danger-zone-text">
|
||||
<div class="danger-zone-title">Export all data</div>
|
||||
<div class="danger-zone-desc">Download a ZIP archive of all operation configs, schemas, mappings and invocation history as JSON. May take a few minutes.</div>
|
||||
</div>
|
||||
<button class="btn-danger" id="settings-ws-export-btn" type="button">Export</button>
|
||||
</div>
|
||||
<div class="danger-zone-action">
|
||||
<div class="danger-zone-text">
|
||||
<div class="danger-zone-title">Delete workspace</div>
|
||||
<div class="danger-zone-desc">Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.</div>
|
||||
</div>
|
||||
<button class="btn-danger" id="settings-ws-delete-btn" type="button">Delete workspace</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,17 @@ tls:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout info-callout-subtle" style="margin-bottom: 20px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title">Live actions save the draft first</div>
|
||||
<div class="info-callout-text">Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
|
||||
+33
-64
@@ -5,16 +5,6 @@ function settingsWorkspaceId() {
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function downloadSettingsJson(fileName, value) {
|
||||
var blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
return source
|
||||
@@ -34,6 +24,37 @@ function setStatus(id, text, isError) {
|
||||
: 'var(--text-muted)';
|
||||
}
|
||||
|
||||
function titleCaseRole(role) {
|
||||
return String(role || 'viewer').replace(/^\w/, function(character) {
|
||||
return character.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function populateCurrentSession(session) {
|
||||
var summary = document.getElementById('settings-session-summary');
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(session && session.user)) {
|
||||
summary.textContent = 'Session details are unavailable. Use Log out to revoke the current browser session.';
|
||||
return;
|
||||
}
|
||||
|
||||
var membership = null;
|
||||
if (Array.isArray(session.memberships)) {
|
||||
membership = session.memberships.find(function(item) {
|
||||
return item.workspace && item.workspace.id === session.current_workspace_id;
|
||||
}) || session.memberships[0] || null;
|
||||
}
|
||||
|
||||
var workspaceLabel = membership && membership.workspace
|
||||
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
|
||||
: 'current workspace';
|
||||
var roleLabel = membership ? titleCaseRole(membership.role) : 'Viewer';
|
||||
summary.textContent = session.user.email + ' · ' + roleLabel + ' in ' + workspaceLabel + '. Use Log out to revoke the current browser session.';
|
||||
}
|
||||
|
||||
function populateProfile(session) {
|
||||
if (!session || !session.user) {
|
||||
return;
|
||||
@@ -52,6 +73,8 @@ function populateProfile(session) {
|
||||
if (email) {
|
||||
email.value = user.email || '';
|
||||
}
|
||||
|
||||
populateCurrentSession(session);
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
@@ -285,65 +308,11 @@ async function loadWorkspaceSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
function bindDangerZoneActions() {
|
||||
var renameButton = document.getElementById('settings-ws-rename-btn');
|
||||
var exportButton = document.getElementById('settings-ws-export-btn');
|
||||
var deleteButton = document.getElementById('settings-ws-delete-btn');
|
||||
|
||||
if (renameButton && renameButton.dataset.bound !== 'true') {
|
||||
renameButton.dataset.bound = 'true';
|
||||
renameButton.addEventListener('click', function() {
|
||||
document.getElementById('settings-ws-slug').focus();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info('Update the workspace slug above and save changes to rename it.', 'Rename workspace');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (exportButton && exportButton.dataset.bound !== 'true') {
|
||||
exportButton.dataset.bound = 'true';
|
||||
exportButton.addEventListener('click', async function() {
|
||||
var workspaceId = settingsWorkspaceId();
|
||||
if (!workspaceId || !window.CrankApi) return;
|
||||
try {
|
||||
var snapshot = await window.CrankApi.exportWorkspace(workspaceId);
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
downloadSettingsJson((workspace ? workspace.slug : 'workspace') + '-snapshot.json', snapshot);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success('Workspace export downloaded.', 'Export complete');
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || 'Failed to export workspace', 'Export failed');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteButton && deleteButton.dataset.bound !== 'true') {
|
||||
deleteButton.dataset.bound = 'true';
|
||||
deleteButton.addEventListener('click', async function() {
|
||||
var workspaceId = settingsWorkspaceId();
|
||||
if (!workspaceId || !window.CrankApi || !window.CrankAuth) return;
|
||||
if (!window.confirm('Delete workspace? This cannot be undone.')) return;
|
||||
try {
|
||||
await window.CrankApi.deleteWorkspace(workspaceId);
|
||||
await window.CrankAuth.logout();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || 'Failed to delete workspace', 'Delete failed');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
injectLanguageSwitcher();
|
||||
bindSectionNavigation();
|
||||
loadProfile();
|
||||
bindProfileSave();
|
||||
bindPasswordSave();
|
||||
bindDangerZoneActions();
|
||||
loadWorkspaceSettings();
|
||||
});
|
||||
|
||||
+52
-9
@@ -1352,19 +1352,19 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
}
|
||||
|
||||
function bindWizardLiveActions() {
|
||||
bindClick('wizard-upload-input-sample', uploadInputSampleFromWizard);
|
||||
bindClick('wizard-upload-output-sample', uploadOutputSampleFromWizard);
|
||||
bindClick('wizard-generate-draft', generateDraftFromWizard);
|
||||
bindClick('wizard-run-test', runWizardTest);
|
||||
bindLiveAction('wizard-upload-input-sample', 'Saving input sample…', uploadInputSampleFromWizard);
|
||||
bindLiveAction('wizard-upload-output-sample', 'Saving output sample…', uploadOutputSampleFromWizard);
|
||||
bindLiveAction('wizard-generate-draft', 'Generating draft…', generateDraftFromWizard);
|
||||
bindLiveAction('wizard-run-test', 'Running test…', runWizardTest);
|
||||
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
|
||||
bindClick('wizard-export-yaml', exportWizardYaml);
|
||||
bindClick('wizard-import-yaml', importWizardYaml);
|
||||
bindClick('wizard-publish-operation', publishWizardOperation);
|
||||
bindLiveAction('wizard-export-yaml', 'Exporting YAML…', exportWizardYaml);
|
||||
bindLiveAction('wizard-import-yaml', 'Importing YAML…', importWizardYaml);
|
||||
bindLiveAction('wizard-publish-operation', 'Publishing operation…', publishWizardOperation);
|
||||
bindClick('wizard-select-descriptor-set', function() {
|
||||
var input = document.getElementById('wizard-descriptor-set-input');
|
||||
if (input) input.click();
|
||||
});
|
||||
bindClick('wizard-upload-descriptor-set', uploadDescriptorSetAndDiscover);
|
||||
bindLiveAction('wizard-upload-descriptor-set', 'Uploading descriptor set…', uploadDescriptorSetAndDiscover);
|
||||
bindClick('wizard-import-yaml-file-trigger', function() {
|
||||
var input = document.getElementById('wizard-import-yaml-file');
|
||||
if (input) input.click();
|
||||
@@ -1386,6 +1386,48 @@ function bindClick(id, handler) {
|
||||
});
|
||||
}
|
||||
|
||||
function bindLiveAction(id, busyLabel, handler) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
element.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
runWizardLiveAction(element, busyLabel, handler);
|
||||
});
|
||||
}
|
||||
|
||||
async function runWizardLiveAction(button, busyLabel, handler) {
|
||||
if (!button || button.dataset.busy === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
var originalLabel = button.textContent;
|
||||
button.dataset.busy = 'true';
|
||||
button.disabled = true;
|
||||
button.classList.add('is-busy');
|
||||
button.textContent = busyLabel;
|
||||
|
||||
try {
|
||||
await handler();
|
||||
} catch (error) {
|
||||
showWizardLiveStatus(
|
||||
'Live action failed',
|
||||
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
|
||||
true
|
||||
);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
|
||||
'Wizard action failed'
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
button.dataset.busy = 'false';
|
||||
button.disabled = false;
|
||||
button.classList.remove('is-busy');
|
||||
button.textContent = originalLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWizardProtocolVisibility() {
|
||||
var grpcTools = document.getElementById('wizard-grpc-live-tools');
|
||||
if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none';
|
||||
@@ -1399,7 +1441,8 @@ function showWizardLiveStatus(title, text, isError) {
|
||||
titleEl.textContent = title;
|
||||
textEl.textContent = text;
|
||||
root.style.display = '';
|
||||
root.style.borderColor = isError ? 'rgba(248, 113, 113, 0.35)' : '';
|
||||
root.classList.toggle('is-error', !!isError);
|
||||
root.classList.toggle('is-success', !isError);
|
||||
}
|
||||
|
||||
function currentDraftVersion() {
|
||||
|
||||
Reference in New Issue
Block a user