331 lines
20 KiB
HTML
331 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<base href="../">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Crank — Workspace</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/variables.css">
|
|
<link rel="stylesheet" href="css/layout.css">
|
|
<link rel="stylesheet" href="css/pages.css">
|
|
<link rel="stylesheet" href="css/wizard.css">
|
|
<link rel="stylesheet" href="css/workspace-setup.css">
|
|
<script src="js/config.js"></script>
|
|
<script src="js/i18n.js"></script>
|
|
<script src="js/api.js"></script>
|
|
<script src="js/ui-feedback.js"></script>
|
|
<script src="js/auth.js"></script>
|
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
|
</head>
|
|
<body>
|
|
<div class="ws-setup-page">
|
|
|
|
<!-- Top bar -->
|
|
<div class="ws-setup-topbar">
|
|
<a href="index.html" class="ws-setup-logo">
|
|
<div class="ws-setup-logo-mark">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="18" height="18" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/></svg>
|
|
</div>
|
|
Crank
|
|
</a>
|
|
<a href="javascript:history.back()" class="ws-setup-back">
|
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4l-4 4 4 4"/></svg>
|
|
Back
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="ws-setup-body">
|
|
<div class="ws-setup-container">
|
|
|
|
<div class="ws-setup-header">
|
|
<div class="ws-setup-title" id="page-title">Workspace settings</div>
|
|
<div class="ws-setup-subtitle" id="page-subtitle">Configure your workspace identity, default settings, and team members.</div>
|
|
</div>
|
|
|
|
<!-- ── Identity ── -->
|
|
<div class="ws-form-section">
|
|
<div class="ws-form-section-title">Workspace identity</div>
|
|
|
|
<div class="ws-avatar-row">
|
|
<div class="ws-avatar-preview" id="ws-avatar-preview">A</div>
|
|
<div>
|
|
<div class="ws-avatar-hint">Avatar is derived from your workspace name.</div>
|
|
<div class="ws-color-swatches">
|
|
<div class="ws-color-swatch active" style="background:#0d9488;" data-color="#0d9488" onclick="pickColor(this)" title="Teal"></div>
|
|
<div class="ws-color-swatch" style="background:#7c3aed;" data-color="#7c3aed" onclick="pickColor(this)" title="Purple"></div>
|
|
<div class="ws-color-swatch" style="background:#0891b2;" data-color="#0891b2" onclick="pickColor(this)" title="Cyan"></div>
|
|
<div class="ws-color-swatch" style="background:#d29922;" data-color="#d29922" onclick="pickColor(this)" title="Amber"></div>
|
|
<div class="ws-color-swatch" style="background:#1a7f37;" data-color="#1a7f37" onclick="pickColor(this)" title="Green"></div>
|
|
<div class="ws-color-swatch" style="background:#cf222e;" data-color="#cf222e" onclick="pickColor(this)" title="Red"></div>
|
|
<div class="ws-color-swatch" style="background:#5e6ad2;" data-color="#5e6ad2" onclick="pickColor(this)" title="Indigo"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-bottom:14px;">
|
|
<label class="form-label">Workspace name <span class="form-label-required">required</span></label>
|
|
<input class="form-input" id="ws-name" type="text" placeholder="Acme Inc" autocomplete="off" oninput="onWsNameInput(this.value)">
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-bottom:14px;">
|
|
<label class="form-label">Slug <span class="form-label-required">required</span></label>
|
|
<div style="position:relative;">
|
|
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:13px;color:var(--text-muted);font-family:monospace;pointer-events:none;">mcp.crank.io/</span>
|
|
<input class="form-input input-mono" id="ws-slug" type="text" placeholder="acme-inc" autocomplete="off" style="padding-left: 112px;" oninput="onWsSlugInput(this.value)">
|
|
</div>
|
|
<div class="form-hint">Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Description <span class="form-label-optional">(optional)</span></label>
|
|
<textarea class="form-textarea" id="ws-desc" rows="2" placeholder="What does this workspace do?"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Default settings ── -->
|
|
<div class="ws-form-section">
|
|
<div class="ws-form-section-title">Default settings</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Default protocol</label>
|
|
<select class="form-input" id="ws-protocol">
|
|
<option value="rest" selected>REST</option>
|
|
<option value="graphql">GraphQL</option>
|
|
<option value="grpc">gRPC</option>
|
|
</select>
|
|
<div class="form-hint">Pre-selected in the operation wizard.</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Timezone</label>
|
|
<select class="form-input" id="ws-timezone">
|
|
<option value="UTC" selected>UTC</option>
|
|
<option value="America/New_York">Eastern Time</option>
|
|
<option value="America/Chicago">Central Time</option>
|
|
<option value="America/Los_Angeles">Pacific Time</option>
|
|
<option value="Europe/London">London</option>
|
|
<option value="Europe/Berlin">Berlin / Paris</option>
|
|
<option value="Asia/Tokyo">Tokyo</option>
|
|
<option value="Asia/Shanghai">Shanghai</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-top:4px;margin-bottom:0;">
|
|
<label class="form-label">Interface language</label>
|
|
<div class="lang-switcher">
|
|
<button class="lang-btn active" data-lang="en" onclick="setLang('en')">
|
|
<span class="lang-flag">🇬🇧</span>
|
|
<span data-i18n="settings.lang.en">English</span>
|
|
</button>
|
|
<button class="lang-btn" data-lang="ru" onclick="setLang('ru')">
|
|
<span class="lang-flag">🇷🇺</span>
|
|
<span data-i18n="settings.lang.ru">Русский</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Members (edit mode only) ── -->
|
|
<div id="section-members" class="ws-form-section" style="display:none; padding:0; overflow:hidden;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;">
|
|
<div>
|
|
<div class="ws-form-section-title" style="margin-bottom:2px;">Team members</div>
|
|
<div style="font-size:12px;color:var(--text-muted);" 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 24px 16px; border-bottom: 1px solid var(--border-subtle);">
|
|
<div style="display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
|
|
<div class="form-group" style="flex:1; min-width:200px; margin-bottom:0;">
|
|
<label class="form-label">Email address</label>
|
|
<input class="form-input" id="invite-email" type="email" placeholder="colleague@company.com" autocomplete="off">
|
|
</div>
|
|
<div class="form-group" style="width:140px; margin-bottom:0;">
|
|
<label class="form-label">Role</label>
|
|
<select class="form-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;" onclick="sendInvite()">Send invite</button>
|
|
<button class="btn-ghost-sm" type="button" style="padding:7px 12px;" onclick="toggleInviteForm()">Cancel</button>
|
|
</div>
|
|
<div id="invite-success" style="display:none; margin-top:10px; font-size:13px; color: #3fb950;">✓ Invite sent.</div>
|
|
</div>
|
|
|
|
<!-- Members list -->
|
|
<div style="padding: 4px 24px 8px;" id="members-list">
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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 24px 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>
|
|
|
|
<!-- Roles reference -->
|
|
<div style="border-top:1px solid var(--border-subtle);padding:16px 24px 20px;">
|
|
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:14px;">Roles & permissions</div>
|
|
<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.</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.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Invite team members (create mode only) ── -->
|
|
<div id="section-invite" class="ws-form-section" style="display:none;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
|
|
<div class="ws-form-section-title" style="margin-bottom:0;">Invite team members <span style="font-size:12px;font-weight:400;color:var(--text-muted);">(optional)</span></div>
|
|
<button class="btn-ghost-sm" type="button" onclick="addInviteRow()" style="font-size:12px;padding:4px 10px;">+ Add person</button>
|
|
</div>
|
|
<div class="form-hint" style="margin-bottom:14px;">You can invite more people later from Workspace settings.</div>
|
|
<div class="invite-rows" id="invite-rows">
|
|
<div class="invite-row">
|
|
<input class="form-input" type="email" placeholder="colleague@company.com" autocomplete="off" style="flex:1;margin-bottom:0;">
|
|
<select class="form-input" style="width:130px;margin-bottom:0;">
|
|
<option value="admin">Admin</option>
|
|
<option value="developer" selected>Developer</option>
|
|
<option value="viewer">Viewer</option>
|
|
</select>
|
|
<button class="invite-row-remove" onclick="removeInviteRow(this)" title="Remove">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Actions ── -->
|
|
<div class="ws-setup-actions">
|
|
<a href="javascript:history.back()" class="btn-ghost-sm" style="padding:9px 18px;font-size:13px;text-decoration:none;color:var(--text-secondary);">Cancel</a>
|
|
<button class="btn-primary" id="submit-btn" type="button" onclick="submitForm()" style="padding:9px 22px;font-size:13px;">Save changes</button>
|
|
</div>
|
|
|
|
<div class="ws-setup-footer-note" id="footer-note" style="display:none;">
|
|
You'll be the Owner of this workspace. You can invite additional members after creation.
|
|
</div>
|
|
|
|
<!-- ── Danger zone (edit mode only) ── -->
|
|
<div id="section-danger" style="display:none; margin-top:32px;">
|
|
<div class="ws-form-section" style="border-color: rgba(248,81,73,0.25); padding:0; overflow:hidden;">
|
|
<div style="padding:16px 24px; border-bottom: 1px solid rgba(248,81,73,0.15);">
|
|
<div style="font-size:14px;font-weight:600;color:var(--red);">Danger zone</div>
|
|
</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 JSON snapshot of workspace settings, memberships, invitations, operations, agents and platform API keys.</div>
|
|
</div>
|
|
<button class="btn-danger" id="export-workspace-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="delete-workspace-btn" type="button">Delete workspace</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script src="js/workspace.js"></script>
|
|
<script src="js/workspace-setup.js"></script>
|
|
</body>
|
|
</html>
|