feat: connect workspace access lifecycle to admin api

This commit is contained in:
a.tolmachev
2026-03-31 09:12:42 +03:00
parent 3065b3100b
commit cb23f1eb96
12 changed files with 551 additions and 31 deletions
+1 -1
View File
@@ -304,7 +304,7 @@
<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.</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>
+12
View File
@@ -177,6 +177,12 @@
listMemberships: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/members');
},
updateMembership: function(workspaceId, userId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId), payload);
},
deleteMembership: function(workspaceId, userId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId));
},
listInvitations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations');
},
@@ -186,6 +192,12 @@
deleteInvitation: function(workspaceId, invitationId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations/' + encodeURIComponent(invitationId));
},
exportWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/export');
},
deleteWorkspace: function(workspaceId) {
return del('/workspaces/' + encodeURIComponent(workspaceId));
},
listOperations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations');
},
+104 -13
View File
@@ -7,6 +7,7 @@ var workspaceFormState = {
workspaceRecord: null,
memberships: [],
invitations: [],
currentUserId: null,
};
function normalizeSlug(value) {
@@ -101,16 +102,32 @@ function applyWorkspaceRecord(record) {
function addMemberRow(container, membership) {
var row = document.createElement('div');
row.className = 'member-row';
var isCurrentUser = membership.user.id === workspaceFormState.currentUserId;
var canManage = membership.role !== 'owner';
row.innerHTML =
'<div class="member-avatar" style="background:linear-gradient(135deg,' + workspaceFormState.selectedColor + ',#6366f1);">' +
membership.user.display_name.slice(0, 2).toUpperCase() +
'</div>' +
'<div class="member-info">' +
'<div class="member-name">' + membership.user.display_name + '</div>' +
'<div class="member-name">' + membership.user.display_name +
(isCurrentUser ? ' <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(you)</span>' : '') +
'</div>' +
'<div class="member-email">' + membership.user.email + '</div>' +
'</div>' +
'<div class="member-last-active">—</div>' +
'<span class="member-role-badge ' + roleClass(membership.role) + '">' + roleLabel(membership.role) + '</span>';
(
canManage
? '<select class="member-role-select" data-previous="' + membership.role + '" onchange="updateRole(this, \'' + membership.user.id + '\')">' +
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>Owner</option>' +
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>Admin</option>' +
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>Developer</option>' +
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>Viewer</option>' +
'</select>' +
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" 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>'
: '<span class="member-role-badge ' + roleClass(membership.role) + '">' + roleLabel(membership.role) + '</span>'
);
container.appendChild(row);
}
@@ -175,6 +192,13 @@ async function loadWorkspaceAccessData() {
return;
}
if (window.CrankAuth && typeof window.CrankAuth.fetchSession === 'function') {
try {
var session = await window.CrankAuth.fetchSession(false);
workspaceFormState.currentUserId = session && session.user ? session.user.id : null;
} catch (_error) {}
}
var membershipsResponse = await window.CrankApi.listMemberships(workspaceFormState.workspaceId);
var invitationsResponse = await window.CrankApi.listInvitations(workspaceFormState.workspaceId);
workspaceFormState.memberships = membershipsResponse && membershipsResponse.items ? membershipsResponse.items : [];
@@ -384,12 +408,42 @@ async function sendInvite() {
}
}
function updateRole() {
alert('Role management is not available yet in the backend.');
async function updateRole(select, userId) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
var previous = select.dataset.previous || select.value;
select.disabled = true;
try {
var response = await window.CrankApi.updateMembership(workspaceFormState.workspaceId, userId, {
role: select.value,
});
workspaceFormState.memberships = response && response.items ? response.items : workspaceFormState.memberships;
renderMembers();
} catch (error) {
select.value = previous;
alert(error.message || 'Failed to update role');
} finally {
select.disabled = false;
}
}
function removeMember() {
alert('Member removal is not available yet in the backend.');
async function removeMember(userId, name) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
if (!confirm('Remove ' + name + ' from this workspace?')) {
return;
}
try {
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
await loadWorkspaceAccessData();
} catch (error) {
alert(error.message || 'Failed to remove member');
}
}
async function revokeInviteById(invitationId) {
@@ -413,6 +467,48 @@ function revokeInvite(button) {
row.remove();
}
function downloadJsonFile(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);
}
async function exportWorkspaceSnapshot() {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
try {
var snapshot = await window.CrankApi.exportWorkspace(workspaceFormState.workspaceId);
var slug = workspaceFormState.workspaceRecord && workspaceFormState.workspaceRecord.workspace
? workspaceFormState.workspaceRecord.workspace.slug
: 'workspace';
downloadJsonFile(slug + '-snapshot.json', snapshot);
} catch (error) {
alert(error.message || 'Failed to export workspace');
}
}
async function deleteWorkspaceAction() {
if (!workspaceFormState.workspaceId || !window.CrankApi || !window.CrankAuth) {
return;
}
if (!confirm('Delete this workspace? This cannot be undone.')) {
return;
}
try {
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
await window.CrankAuth.logout();
} catch (error) {
alert(error.message || 'Failed to delete workspace');
}
}
async function initPage() {
updatePageMode();
@@ -421,13 +517,8 @@ async function initPage() {
button.classList.toggle('active', button.dataset.lang === language);
});
document.getElementById('export-workspace-btn').addEventListener('click', function () {
alert('Workspace export is not available yet in the backend.');
});
document.getElementById('delete-workspace-btn').addEventListener('click', function () {
alert('Workspace deletion is not available yet in the backend.');
});
document.getElementById('export-workspace-btn').addEventListener('click', exportWorkspaceSnapshot);
document.getElementById('delete-workspace-btn').addEventListener('click', deleteWorkspaceAction);
if (workspaceFormState.isCreateMode) {
document.getElementById('ws-name').focus();