chore: publish clean community baseline
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
function agentUiStatus(status) {
|
||||
if (status === 'published') return 'published';
|
||||
if (status === 'archived') return 'archived';
|
||||
return status || 'draft';
|
||||
}
|
||||
|
||||
function mapAgent(agent) {
|
||||
var mapped = {
|
||||
id: agent.id,
|
||||
slug: agent.slug,
|
||||
display_name: agent.display_name,
|
||||
description: agent.description || '',
|
||||
status: agentUiStatus(agent.status),
|
||||
raw_status: agent.status,
|
||||
operation_count: agent.operation_count || 0,
|
||||
operation_ids: agent.operation_ids || [],
|
||||
key_count: agent.key_count || 0,
|
||||
calls_today: agent.calls_today || 0,
|
||||
created_at: agent.created_at,
|
||||
current_draft_version: agent.current_draft_version || 1,
|
||||
latest_published_version: agent.latest_published_version,
|
||||
mcp_endpoint: agent.mcp_endpoint || '',
|
||||
};
|
||||
|
||||
return window.localizeDemoAgent ? window.localizeDemoAgent(mapped) : mapped;
|
||||
}
|
||||
|
||||
function mapOperation(operation) {
|
||||
var mapped = {
|
||||
id: operation.id,
|
||||
name: operation.name,
|
||||
display_name: operation.display_name,
|
||||
protocol: operation.protocol,
|
||||
status: operation.status,
|
||||
current_draft_version: operation.current_draft_version || 1,
|
||||
latest_published_version: operation.latest_published_version,
|
||||
};
|
||||
|
||||
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', function() {
|
||||
Alpine.data('agents', function() {
|
||||
return {
|
||||
agents: [],
|
||||
operations: [],
|
||||
capabilities: null,
|
||||
loading: true,
|
||||
loadError: '',
|
||||
workspaceId: null,
|
||||
|
||||
agentSearch: '',
|
||||
openDropdown: null,
|
||||
drawerOpen: false,
|
||||
drawerMode: 'create',
|
||||
editingId: null,
|
||||
saving: false,
|
||||
|
||||
form: {
|
||||
display_name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
status: 'published',
|
||||
selectedOps: [],
|
||||
},
|
||||
|
||||
opSearch: '',
|
||||
slugManuallyEdited: false,
|
||||
|
||||
async init() {
|
||||
var self = this;
|
||||
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
this.workspaceId = workspace ? workspace.id : null;
|
||||
await this.loadCapabilities();
|
||||
await this.reload();
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape' && self.drawerOpen) {
|
||||
self.closeDrawer();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function() {
|
||||
self.openDropdown = null;
|
||||
});
|
||||
|
||||
window.addEventListener('crank:workspacechange', async function(event) {
|
||||
self.workspaceId = event.detail ? event.detail.id : null;
|
||||
await self.loadCapabilities();
|
||||
await self.reload();
|
||||
});
|
||||
},
|
||||
|
||||
async loadCapabilities() {
|
||||
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||
this.capabilities = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.capabilities = await window.CrankApi.getCapabilities();
|
||||
} catch (_error) {
|
||||
this.capabilities = null;
|
||||
}
|
||||
},
|
||||
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
|
||||
if (!this.workspaceId || !window.CrankApi) {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loading = false;
|
||||
this.loadError = this.tKey('agents.error.api');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var responses = await Promise.all([
|
||||
window.CrankApi.listAgents(this.workspaceId),
|
||||
window.CrankApi.listOperations(this.workspaceId),
|
||||
]);
|
||||
this.agents = ((responses[0] && responses[0].items) || []).map(mapAgent);
|
||||
this.operations = ((responses[1] && responses[1].items) || []).map(mapOperation);
|
||||
} catch (error) {
|
||||
this.agents = [];
|
||||
this.operations = [];
|
||||
this.loadError = error.message || this.tKey('agents.error.load');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
get filteredAgents() {
|
||||
var query = this.agentSearch.toLowerCase().trim();
|
||||
if (!query) return this.agents;
|
||||
return this.agents.filter(function(agent) {
|
||||
return agent.display_name.toLowerCase().includes(query)
|
||||
|| agent.slug.toLowerCase().includes(query)
|
||||
|| (agent.description || '').toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
|
||||
get totalCalls() {
|
||||
return this.agents.reduce(function(sum, agent) {
|
||||
return sum + (agent.calls_today || 0);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
get activeCount() {
|
||||
return this.agents.filter(function(agent) {
|
||||
return agent.status === 'published';
|
||||
}).length;
|
||||
},
|
||||
|
||||
get totalOpsExposed() {
|
||||
return this.agents.reduce(function(sum, agent) {
|
||||
return sum + (agent.operation_count || 0);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
get isCommunityBuild() {
|
||||
return !!(this.capabilities && this.capabilities.edition === 'community');
|
||||
},
|
||||
|
||||
agentCalloutText() {
|
||||
if (this.isCommunityBuild) {
|
||||
return this.tfKey('agents.callout.community', { count: this.operations.length });
|
||||
}
|
||||
return this.tfKey('agents.callout.default', { count: this.operations.length });
|
||||
},
|
||||
|
||||
subtitleText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.subtitle.community' : 'agents.subtitle');
|
||||
},
|
||||
|
||||
emptyStateText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.empty.initial.sub_community' : 'agents.empty.initial.sub');
|
||||
},
|
||||
|
||||
drawerSubText() {
|
||||
if (this.drawerMode === 'edit') {
|
||||
return this.tKey('agents.drawer.edit_sub');
|
||||
}
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.drawer.new_sub_community' : 'agents.drawer.new_sub');
|
||||
},
|
||||
|
||||
operationsSubText() {
|
||||
return this.tKey(this.isCommunityBuild ? 'agents.drawer.operations_sub_community' : 'agents.drawer.operations_sub');
|
||||
},
|
||||
|
||||
get filteredOps() {
|
||||
var query = this.opSearch.toLowerCase().trim();
|
||||
if (!query) return this.operations;
|
||||
return this.operations.filter(function(operation) {
|
||||
return operation.name.toLowerCase().includes(query)
|
||||
|| operation.display_name.toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
|
||||
openCreate() {
|
||||
this.drawerMode = 'create';
|
||||
this.editingId = null;
|
||||
this.form = {
|
||||
display_name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
status: 'published',
|
||||
selectedOps: [],
|
||||
};
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = false;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
openEdit(agent) {
|
||||
this.drawerMode = 'edit';
|
||||
this.editingId = agent.id;
|
||||
this.form = {
|
||||
display_name: agent.display_name,
|
||||
slug: agent.slug,
|
||||
description: agent.description,
|
||||
status: agent.raw_status || agent.status || 'draft',
|
||||
selectedOps: [].concat(agent.operation_ids || []),
|
||||
};
|
||||
this.opSearch = '';
|
||||
this.slugManuallyEdited = true;
|
||||
this.drawerOpen = true;
|
||||
},
|
||||
|
||||
closeDrawer() {
|
||||
this.drawerOpen = false;
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
onNameInput(value) {
|
||||
this.form.display_name = value;
|
||||
if (!this.slugManuallyEdited) {
|
||||
this.form.slug = value
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
},
|
||||
|
||||
onSlugInput(value) {
|
||||
this.form.slug = value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||
this.slugManuallyEdited = true;
|
||||
},
|
||||
|
||||
toggleOp(operationId) {
|
||||
var index = this.form.selectedOps.indexOf(operationId);
|
||||
if (index === -1) {
|
||||
this.form.selectedOps.push(operationId);
|
||||
} else {
|
||||
this.form.selectedOps.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
isOpSelected(operationId) {
|
||||
return this.form.selectedOps.includes(operationId);
|
||||
},
|
||||
|
||||
async saveAgent() {
|
||||
var self = this;
|
||||
if (
|
||||
this.saving
|
||||
|| !this.workspaceId
|
||||
|| !this.form.display_name.trim()
|
||||
|| !this.form.slug.trim()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
var agentId = this.editingId;
|
||||
var currentVersion = 1;
|
||||
var existingAgent = this.drawerMode === 'edit'
|
||||
? this.agents.find(function(item) { return item.id === agentId; })
|
||||
: null;
|
||||
var previousStatus = existingAgent ? (existingAgent.raw_status || 'draft') : 'draft';
|
||||
|
||||
if (this.drawerMode === 'create') {
|
||||
var created = await window.CrankApi.createAgent(this.workspaceId, {
|
||||
slug: this.form.slug,
|
||||
display_name: this.form.display_name,
|
||||
description: this.form.description,
|
||||
instructions: {},
|
||||
tool_selection_policy: {},
|
||||
});
|
||||
agentId = created.agent_id;
|
||||
currentVersion = created.version || 1;
|
||||
} else {
|
||||
await window.CrankApi.updateAgent(this.workspaceId, this.editingId, {
|
||||
slug: this.form.slug,
|
||||
display_name: this.form.display_name,
|
||||
description: this.form.description,
|
||||
});
|
||||
var agent = await window.CrankApi.getAgent(this.workspaceId, this.editingId);
|
||||
currentVersion = agent.current_draft_version || 1;
|
||||
}
|
||||
|
||||
await window.CrankApi.saveAgentBindings(
|
||||
this.workspaceId,
|
||||
agentId,
|
||||
this.form.selectedOps.map(function(operationId) {
|
||||
var operation = self.operations.find(function(item) {
|
||||
return item.id === operationId;
|
||||
});
|
||||
return {
|
||||
operation_id: operationId,
|
||||
operation_version: operation && operation.latest_published_version
|
||||
? operation.latest_published_version
|
||||
: operation && operation.current_draft_version
|
||||
? operation.current_draft_version
|
||||
: 1,
|
||||
tool_name: operation ? operation.name : operationId,
|
||||
tool_title: operation ? (operation.display_name || operation.name) : operationId,
|
||||
tool_description_override: null,
|
||||
enabled: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.form.status === 'published') {
|
||||
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
|
||||
version: currentVersion,
|
||||
});
|
||||
} else if (this.form.status === 'archived') {
|
||||
await window.CrankApi.archiveAgent(this.workspaceId, agentId);
|
||||
} else if (this.drawerMode === 'edit' && previousStatus !== 'draft') {
|
||||
await window.CrankApi.unpublishAgent(this.workspaceId, agentId);
|
||||
}
|
||||
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
this.tfKey('agents.toast.saved_message', {
|
||||
name: this.form.display_name,
|
||||
count: this.form.selectedOps.length
|
||||
}),
|
||||
this.drawerMode === 'create'
|
||||
? this.tKey('agents.toast.saved_title_create')
|
||||
: this.tKey('agents.toast.saved_title_update')
|
||||
);
|
||||
}
|
||||
this.closeDrawer();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || this.tKey('agents.toast.save_error_message'),
|
||||
this.tKey('agents.toast.save_error_title')
|
||||
);
|
||||
}
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAgent(id) {
|
||||
if (!confirm(this.tKey('agents.toast.delete_confirm'))) return;
|
||||
|
||||
try {
|
||||
var agent = this.agents.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.deleteAgent(this.workspaceId, id);
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
this.tfKey('agents.toast.delete_message', { name: agent ? agent.display_name : '' }),
|
||||
this.tKey('agents.toast.delete_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || this.tKey('agents.toast.delete_error_message'),
|
||||
this.tKey('agents.toast.delete_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async applyLifecycle(agent, action) {
|
||||
if (!this.workspaceId || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'publish') {
|
||||
await window.CrankApi.publishAgent(this.workspaceId, agent.id, {
|
||||
version: agent.current_draft_version || 1,
|
||||
});
|
||||
} else if (action === 'unpublish') {
|
||||
await window.CrankApi.unpublishAgent(this.workspaceId, agent.id);
|
||||
} else if (action === 'archive') {
|
||||
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
|
||||
}
|
||||
await this.reload();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
action === 'publish'
|
||||
? this.tfKey('agents.toast.lifecycle_publish', { name: agent.display_name })
|
||||
: action === 'unpublish'
|
||||
? this.tfKey('agents.toast.lifecycle_unpublish', { name: agent.display_name })
|
||||
: this.tfKey('agents.toast.lifecycle_archive', { name: agent.display_name }),
|
||||
this.tKey('agents.toast.lifecycle_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || this.tKey('agents.toast.lifecycle_error_message'),
|
||||
this.tKey('agents.toast.lifecycle_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lifecycleAction(agent) {
|
||||
if (agent.raw_status === 'published') {
|
||||
return { key: 'unpublish', label: this.tKey('agents.lifecycle.unpublish') };
|
||||
}
|
||||
if (agent.raw_status === 'archived') {
|
||||
return { key: 'unpublish', label: this.tKey('agents.lifecycle.restore_draft') };
|
||||
}
|
||||
return { key: 'publish', label: this.tKey('agents.lifecycle.publish') };
|
||||
},
|
||||
|
||||
lifecycleLabel(agent) {
|
||||
if (agent.raw_status === 'published') return this.tKey('agents.lifecycle.published');
|
||||
if (agent.raw_status === 'archived') return this.tKey('agents.lifecycle.archived');
|
||||
return this.tKey('agents.lifecycle.draft');
|
||||
},
|
||||
|
||||
mcpEndpoint(agent) {
|
||||
if (agent.mcp_endpoint) return agent.mcp_endpoint;
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
var workspaceSlug = workspace ? workspace.slug : 'default';
|
||||
return '/mcp/v1/' + workspaceSlug + '/' + agent.slug;
|
||||
},
|
||||
|
||||
endpointHelpText(agent) {
|
||||
return this.tfKey(
|
||||
this.isCommunityBuild ? 'agents.card.endpoint_help_community' : 'agents.card.endpoint_help',
|
||||
{ count: agent.key_count || 0 }
|
||||
);
|
||||
},
|
||||
|
||||
copyEndpoint(agent) {
|
||||
var endpoint = this.mcpEndpoint(agent);
|
||||
navigator.clipboard.writeText(endpoint).catch(function() {});
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(endpoint, this.tKey('agents.toast.endpoint_title'));
|
||||
}
|
||||
},
|
||||
|
||||
statusClass(status) {
|
||||
return 'agent-status-badge agent-status-' + status;
|
||||
},
|
||||
|
||||
protocolBadge(operation) {
|
||||
return 'badge badge-rest';
|
||||
},
|
||||
|
||||
protocolLabel(operation) {
|
||||
return 'REST';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString(currentLocale(), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
},
|
||||
|
||||
formatCalls(value) {
|
||||
if (!value) return '0';
|
||||
return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : String(value);
|
||||
},
|
||||
|
||||
tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
},
|
||||
|
||||
tfKey(key, vars) {
|
||||
return window.tf ? tf(key, vars) : this.tKey(key);
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,620 @@
|
||||
var KEYS = [];
|
||||
var AGENTS = [];
|
||||
var capabilities = null;
|
||||
var currentWorkspaceId = null;
|
||||
var currentAgentId = null;
|
||||
var selectedScopes = new Set(['read']);
|
||||
var search = '';
|
||||
|
||||
var SCOPES = ['read', 'write', 'deploy'];
|
||||
var SCOPE_DESC = {
|
||||
read: 'apikeys.scope.read',
|
||||
write: 'apikeys.scope.write',
|
||||
deploy: 'apikeys.scope.deploy',
|
||||
};
|
||||
|
||||
function tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
}
|
||||
|
||||
function tfKey(key, vars) {
|
||||
return window.tf ? tf(key, vars) : tKey(key);
|
||||
}
|
||||
|
||||
function capabilityLabel(key) {
|
||||
return tKey('settings.capability.' + key);
|
||||
}
|
||||
|
||||
function buildIconSvg(href, width, height) {
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', String(width));
|
||||
svg.setAttribute('height', String(height));
|
||||
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('href', href);
|
||||
svg.appendChild(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function mapKeyRecord(record) {
|
||||
var apiKey = record.api_key || {};
|
||||
return {
|
||||
id: apiKey.id,
|
||||
agentId: apiKey.agent_id || null,
|
||||
name: apiKey.name,
|
||||
prefix: apiKey.prefix || '',
|
||||
scopes: apiKey.scopes || [],
|
||||
created: apiKey.created_at ? apiKey.created_at.slice(0, 10) : '—',
|
||||
lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : tKey('apikeys.last_used.never'),
|
||||
status: apiKey.status || 'active',
|
||||
};
|
||||
}
|
||||
|
||||
function mapAgentRecord(record) {
|
||||
return {
|
||||
id: record.id,
|
||||
slug: record.slug,
|
||||
displayName: record.display_name || record.slug || record.id,
|
||||
mcpEndpoint: record.mcp_endpoint || '',
|
||||
};
|
||||
}
|
||||
|
||||
function currentWorkspace() {
|
||||
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
}
|
||||
|
||||
async function loadKeys() {
|
||||
var workspace = currentWorkspace();
|
||||
currentWorkspaceId = workspace ? workspace.id : null;
|
||||
|
||||
setTableLoading(true);
|
||||
|
||||
if (!currentWorkspaceId || !window.CrankApi) {
|
||||
AGENTS = [];
|
||||
KEYS = [];
|
||||
currentAgentId = null;
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable(tKey('apikeys.error.api'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listAgents(currentWorkspaceId);
|
||||
AGENTS = ((response && response.items) || []).map(mapAgentRecord);
|
||||
if (!AGENTS.length) {
|
||||
currentAgentId = null;
|
||||
KEYS = [];
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable();
|
||||
return;
|
||||
}
|
||||
if (!currentAgentId || !AGENTS.some(function(agent) { return agent.id === currentAgentId; })) {
|
||||
currentAgentId = AGENTS[0].id;
|
||||
}
|
||||
renderAgentPicker();
|
||||
var keysResponse = await window.CrankApi.listAgentPlatformApiKeys(
|
||||
currentWorkspaceId,
|
||||
currentAgentId
|
||||
);
|
||||
KEYS = ((keysResponse && keysResponse.items) || []).map(mapKeyRecord);
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable();
|
||||
} catch (error) {
|
||||
AGENTS = [];
|
||||
KEYS = [];
|
||||
currentAgentId = null;
|
||||
renderAgentPicker();
|
||||
setCreateButtonState();
|
||||
setTableLoading(false);
|
||||
renderTable(error.message || tKey('apikeys.error.load'));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCapabilities() {
|
||||
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
capabilities = await window.CrankApi.getCapabilities();
|
||||
} catch (_error) {
|
||||
capabilities = null;
|
||||
}
|
||||
|
||||
renderMachineAccessSummary();
|
||||
}
|
||||
|
||||
function renderMachineAccessSummary() {
|
||||
var title = document.getElementById('machine-access-title');
|
||||
var subtitle = document.getElementById('machine-access-subtitle');
|
||||
var summary = document.getElementById('machine-access-summary');
|
||||
var note = document.getElementById('machine-access-note');
|
||||
if (!title || !subtitle || !summary || !note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!capabilities) {
|
||||
summary.textContent = tKey('apikeys.machine_access.summary_default');
|
||||
note.textContent = tKey('apikeys.machine_access.note');
|
||||
return;
|
||||
}
|
||||
|
||||
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
|
||||
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
|
||||
|
||||
title.textContent = tKey('apikeys.machine_access.title_' + capabilities.edition);
|
||||
subtitle.textContent = tKey('apikeys.machine_access.subtitle_' + capabilities.edition);
|
||||
summary.textContent = tfKey('apikeys.machine_access.summary', {
|
||||
access_modes: accessModes || capabilityLabel('none'),
|
||||
security_levels: securityLevels || capabilityLabel('none'),
|
||||
});
|
||||
note.textContent = tKey('apikeys.machine_access.note_' + capabilities.edition);
|
||||
}
|
||||
|
||||
function setTableLoading(on) {
|
||||
var tbody = document.getElementById('keys-tbody');
|
||||
if (!on) return;
|
||||
tbody.innerHTML = '';
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = 7;
|
||||
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
||||
td.textContent = tKey('apikeys.loading');
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
async function revokeKey(id) {
|
||||
if (!confirm(tKey('apikeys.confirm.revoke'))) return;
|
||||
if (!currentAgentId) return;
|
||||
|
||||
try {
|
||||
var key = KEYS.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.revokeAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
||||
await loadKeys();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }),
|
||||
tKey('apikeys.toast.revoke_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('apikeys.toast.revoke_error_message'),
|
||||
tKey('apikeys.toast.revoke_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKey(id) {
|
||||
if (!confirm(tKey('apikeys.confirm.delete'))) return;
|
||||
if (!currentAgentId) return;
|
||||
|
||||
try {
|
||||
var key = KEYS.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.deleteAgentPlatformApiKey(currentWorkspaceId, currentAgentId, id);
|
||||
await loadKeys();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }),
|
||||
tKey('apikeys.toast.delete_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('apikeys.toast.delete_error_message'),
|
||||
tKey('apikeys.toast.delete_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createKey(name, scopes) {
|
||||
var created = await window.CrankApi.createAgentPlatformApiKey(currentWorkspaceId, currentAgentId, {
|
||||
name: name,
|
||||
scopes: scopes,
|
||||
});
|
||||
return {
|
||||
rawKey: created.secret,
|
||||
record: mapKeyRecord(created.api_key),
|
||||
};
|
||||
}
|
||||
|
||||
function currentAgent() {
|
||||
return AGENTS.find(function(agent) { return agent.id === currentAgentId; }) || null;
|
||||
}
|
||||
|
||||
function renderAgentPicker() {
|
||||
var select = document.getElementById('agent-select');
|
||||
var hint = document.getElementById('agent-endpoint-hint');
|
||||
if (!select || !hint) return;
|
||||
|
||||
select.innerHTML = '';
|
||||
|
||||
if (!AGENTS.length) {
|
||||
var emptyOption = document.createElement('option');
|
||||
emptyOption.value = '';
|
||||
emptyOption.textContent = tKey('apikeys.agent.empty');
|
||||
select.appendChild(emptyOption);
|
||||
select.disabled = true;
|
||||
hint.textContent = tKey('apikeys.agent.empty_hint');
|
||||
return;
|
||||
}
|
||||
|
||||
AGENTS.forEach(function(agent) {
|
||||
var option = document.createElement('option');
|
||||
option.value = agent.id;
|
||||
option.textContent = agent.displayName;
|
||||
if (agent.id === currentAgentId) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.disabled = false;
|
||||
|
||||
var agent = currentAgent();
|
||||
hint.textContent = agent && agent.mcpEndpoint
|
||||
? tfKey('apikeys.agent.endpoint', { endpoint: agent.mcpEndpoint })
|
||||
: tKey('apikeys.agent.endpoint_missing');
|
||||
}
|
||||
|
||||
function setCreateButtonState() {
|
||||
var button = document.getElementById('btn-create-key');
|
||||
if (!button) return;
|
||||
button.disabled = !currentAgentId;
|
||||
}
|
||||
|
||||
function renderScopes() {
|
||||
var el = document.getElementById('scope-checkboxes');
|
||||
var tmpl = document.getElementById('tmpl-scope-checkbox');
|
||||
el.innerHTML = '';
|
||||
SCOPES.forEach(function(scope) {
|
||||
var node = tmpl.content.cloneNode(true);
|
||||
var input = node.querySelector('input');
|
||||
input.dataset.scope = scope;
|
||||
if (selectedScopes.has(scope)) input.checked = true;
|
||||
node.querySelector('.scope-checkbox-name').textContent = scope;
|
||||
node.querySelector('.scope-checkbox-desc').textContent = tKey(SCOPE_DESC[scope]);
|
||||
input.addEventListener('change', function() {
|
||||
if (this.checked) selectedScopes.add(this.dataset.scope);
|
||||
else selectedScopes.delete(this.dataset.scope);
|
||||
});
|
||||
el.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(errorMessage) {
|
||||
var q = search.toLowerCase();
|
||||
var tbody = document.getElementById('keys-tbody');
|
||||
var cardList = document.getElementById('keys-card-list');
|
||||
var rows = KEYS.filter(function(key) {
|
||||
return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q);
|
||||
});
|
||||
var subtitle = document.getElementById('keys-summary-subtitle');
|
||||
|
||||
tbody.innerHTML = '';
|
||||
if (cardList) {
|
||||
cardList.innerHTML = '';
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
var active = KEYS.filter(function(key) { return key.status === 'active'; }).length;
|
||||
var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length;
|
||||
subtitle.textContent = currentAgentId
|
||||
? tfKey('apikeys.active.subtitle', { active: active, revoked: revoked })
|
||||
: tKey('apikeys.agent.empty_hint');
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
var errorRow = document.createElement('tr');
|
||||
var errorCell = document.createElement('td');
|
||||
errorCell.colSpan = 7;
|
||||
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
|
||||
errorCell.textContent = errorMessage;
|
||||
errorRow.appendChild(errorCell);
|
||||
tbody.appendChild(errorRow);
|
||||
renderKeyCards([], errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
var empty = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = 7;
|
||||
td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
||||
td.textContent = currentAgentId
|
||||
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
|
||||
: tKey('apikeys.agent.empty_hint');
|
||||
empty.appendChild(td);
|
||||
tbody.appendChild(empty);
|
||||
renderKeyCards(rows);
|
||||
return;
|
||||
}
|
||||
|
||||
var tmpl = document.getElementById('tmpl-key-row');
|
||||
rows.forEach(function(key) {
|
||||
var node = tmpl.content.cloneNode(true);
|
||||
node.querySelector('.col-name').textContent = key.name;
|
||||
node.querySelector('.col-mono').textContent = key.prefix;
|
||||
node.querySelector('.col-created').textContent = key.created;
|
||||
node.querySelector('.col-last-used').textContent = key.lastUsed;
|
||||
|
||||
var scopesEl = node.querySelector('.col-scopes');
|
||||
key.scopes.forEach(function(scope) {
|
||||
var span = document.createElement('span');
|
||||
span.className = 'badge badge-scope';
|
||||
span.textContent = scope;
|
||||
scopesEl.appendChild(span);
|
||||
});
|
||||
|
||||
var badge = document.createElement('span');
|
||||
badge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
|
||||
badge.textContent = key.status === 'active' ? tKey('apikeys.status.active') : tKey('apikeys.status.revoked');
|
||||
node.querySelector('.col-status').appendChild(badge);
|
||||
|
||||
var actionsActive = node.querySelector('.actions-active');
|
||||
var actionsRevoked = node.querySelector('.actions-revoked');
|
||||
if (key.status === 'active') {
|
||||
actionsActive.querySelector('[title=\"Copy key prefix\"]').addEventListener('click', function() {
|
||||
copyPrefix(key.prefix);
|
||||
});
|
||||
actionsActive.querySelector('[title=\"Revoke key\"]').addEventListener('click', function() {
|
||||
revokeKey(key.id);
|
||||
});
|
||||
} else {
|
||||
actionsActive.hidden = true;
|
||||
actionsRevoked.hidden = false;
|
||||
actionsRevoked.querySelector('[title=\"Delete\"]').addEventListener('click', function() {
|
||||
deleteKey(key.id);
|
||||
});
|
||||
}
|
||||
|
||||
tbody.appendChild(node);
|
||||
});
|
||||
|
||||
renderKeyCards(rows);
|
||||
}
|
||||
|
||||
function renderKeyCards(rows, errorMessage) {
|
||||
var cardList = document.getElementById('keys-card-list');
|
||||
if (!cardList) return;
|
||||
|
||||
cardList.innerHTML = '';
|
||||
|
||||
if (errorMessage) {
|
||||
cardList.appendChild(buildKeyCardMessage(errorMessage, true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
cardList.appendChild(
|
||||
buildKeyCardMessage(
|
||||
currentAgentId
|
||||
? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'))
|
||||
: tKey('apikeys.agent.empty_hint'),
|
||||
false
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach(function(key) {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'resource-card';
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'resource-card-header';
|
||||
var headerMain = document.createElement('div');
|
||||
var title = document.createElement('div');
|
||||
title.className = 'resource-card-title';
|
||||
title.textContent = key.name;
|
||||
var subtitle = document.createElement('div');
|
||||
subtitle.className = 'resource-card-subtitle';
|
||||
subtitle.textContent = key.prefix;
|
||||
headerMain.appendChild(title);
|
||||
headerMain.appendChild(subtitle);
|
||||
|
||||
var actions = document.createElement('div');
|
||||
actions.className = 'resource-card-actions';
|
||||
|
||||
var statusBadge = document.createElement('span');
|
||||
statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked';
|
||||
statusBadge.textContent = key.status === 'active'
|
||||
? tKey('apikeys.status.active')
|
||||
: tKey('apikeys.status.revoked');
|
||||
actions.appendChild(statusBadge);
|
||||
header.appendChild(headerMain);
|
||||
header.appendChild(actions);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = document.createElement('div');
|
||||
metaGrid.className = 'resource-meta-grid';
|
||||
metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—'));
|
||||
metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created));
|
||||
metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed));
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
var actionRow = document.createElement('div');
|
||||
actionRow.className = 'resource-card-actions';
|
||||
if (key.status === 'active') {
|
||||
actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() {
|
||||
copyPrefix(key.prefix);
|
||||
}));
|
||||
actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() {
|
||||
revokeKey(key.id);
|
||||
}, true));
|
||||
} else {
|
||||
actionRow.appendChild(buildCardAction('apikeys.action.delete', function() {
|
||||
deleteKey(key.id);
|
||||
}, true));
|
||||
}
|
||||
card.appendChild(actionRow);
|
||||
|
||||
cardList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function buildKeyCardMessage(text, isError) {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'resource-card';
|
||||
var value = document.createElement('div');
|
||||
value.className = 'resource-meta-value';
|
||||
value.textContent = text;
|
||||
value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)';
|
||||
card.appendChild(value);
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildMetaItem(labelKey, valueText) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'resource-meta-item';
|
||||
var label = document.createElement('div');
|
||||
label.className = 'resource-meta-label';
|
||||
label.textContent = tKey(labelKey);
|
||||
var value = document.createElement('div');
|
||||
value.className = 'resource-meta-value';
|
||||
value.textContent = valueText;
|
||||
item.appendChild(label);
|
||||
item.appendChild(value);
|
||||
return item;
|
||||
}
|
||||
|
||||
function buildCardAction(labelKey, onClick, isDanger) {
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = isDanger ? 'btn-secondary' : 'btn-secondary';
|
||||
button.textContent = tKey(labelKey);
|
||||
button.addEventListener('click', onClick);
|
||||
return button;
|
||||
}
|
||||
|
||||
var modal = document.getElementById('modal-create');
|
||||
|
||||
function openModal() {
|
||||
if (!currentAgentId) return;
|
||||
document.getElementById('modal-form-body').hidden = false;
|
||||
document.getElementById('modal-reveal-body').hidden = true;
|
||||
document.getElementById('modal-footer-create').hidden = false;
|
||||
document.getElementById('modal-footer-done').hidden = true;
|
||||
document.getElementById('new-key-name').value = '';
|
||||
selectedScopes = new Set(['read']);
|
||||
renderScopes();
|
||||
modal.classList.add('open');
|
||||
setTimeout(function() {
|
||||
document.getElementById('new-key-name').focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove('open');
|
||||
}
|
||||
|
||||
document.getElementById('btn-create-key').addEventListener('click', openModal);
|
||||
document.getElementById('modal-close-btn').addEventListener('click', closeModal);
|
||||
document.getElementById('modal-cancel-btn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function(event) {
|
||||
if (event.target === modal) closeModal();
|
||||
});
|
||||
|
||||
document.getElementById('modal-confirm-btn').addEventListener('click', async function() {
|
||||
var button = this;
|
||||
var name = document.getElementById('new-key-name').value.trim();
|
||||
if (!name) {
|
||||
document.getElementById('new-key-name').focus();
|
||||
return;
|
||||
}
|
||||
if (!selectedScopes.size) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(
|
||||
tKey('apikeys.toast.scope_missing_message'),
|
||||
tKey('apikeys.toast.scope_missing_title')
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
button.disabled = true;
|
||||
button.textContent = tKey('apikeys.creating');
|
||||
var created = await createKey(name, Array.from(selectedScopes));
|
||||
KEYS.unshift(created.record);
|
||||
document.getElementById('reveal-key-value').textContent = created.rawKey;
|
||||
document.getElementById('modal-form-body').hidden = true;
|
||||
document.getElementById('modal-reveal-body').hidden = false;
|
||||
document.getElementById('modal-footer-create').hidden = true;
|
||||
document.getElementById('modal-footer-done').hidden = false;
|
||||
renderTable();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('apikeys.toast.create_message'),
|
||||
tKey('apikeys.toast.create_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('apikeys.toast.create_error_message'),
|
||||
tKey('apikeys.toast.create_error_title')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = tKey('apikeys.modal.create');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('modal-done-btn').addEventListener('click', closeModal);
|
||||
|
||||
document.getElementById('copy-key-btn').addEventListener('click', function() {
|
||||
var value = document.getElementById('reveal-key-value').textContent;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(value).catch(function() {});
|
||||
}
|
||||
this.replaceChildren(
|
||||
buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
|
||||
13,
|
||||
13
|
||||
)
|
||||
);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(
|
||||
tKey('apikeys.toast.copy_message'),
|
||||
tKey('apikeys.toast.copy_title')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('key-search').addEventListener('input', function() {
|
||||
search = this.value;
|
||||
renderTable();
|
||||
});
|
||||
|
||||
document.getElementById('agent-select').addEventListener('change', async function() {
|
||||
currentAgentId = this.value || null;
|
||||
await loadKeys();
|
||||
});
|
||||
|
||||
function copyPrefix(prefix) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(prefix).catch(function() {});
|
||||
}
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(prefix, tKey('apikeys.toast.prefix_title'));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
await loadCapabilities();
|
||||
await loadKeys();
|
||||
window.addEventListener('crank:workspacechange', function() {
|
||||
loadCapabilities();
|
||||
loadKeys();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
(function() {
|
||||
var API_BASE = '/api/admin';
|
||||
var AUTH_BASE = '/api/auth';
|
||||
|
||||
function headers(extra) {
|
||||
return Object.assign({
|
||||
'Accept': 'application/json',
|
||||
}, extra || {});
|
||||
}
|
||||
|
||||
async function request(path, options) {
|
||||
var response = await fetch(path, Object.assign({
|
||||
credentials: 'same-origin',
|
||||
headers: headers(),
|
||||
}, options || {}));
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = null;
|
||||
var text = await response.text();
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
||||
window.CrankAuth.handleUnauthorized();
|
||||
}
|
||||
var message = payload && payload.error
|
||||
? payload.error.message
|
||||
: payload && payload.message
|
||||
? payload.message
|
||||
: text
|
||||
? text
|
||||
: ('HTTP ' + response.status);
|
||||
var error = new Error(message);
|
||||
error.status = response.status;
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function requestText(path, options) {
|
||||
var response = await fetch(path, Object.assign({
|
||||
credentials: 'same-origin',
|
||||
headers: headers(),
|
||||
}, options || {}));
|
||||
|
||||
var text = await response.text();
|
||||
var payload = null;
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && window.CrankAuth && typeof window.CrankAuth.handleUnauthorized === 'function') {
|
||||
window.CrankAuth.handleUnauthorized();
|
||||
}
|
||||
var message = payload && payload.error
|
||||
? payload.error.message
|
||||
: payload && payload.message
|
||||
? payload.message
|
||||
: text
|
||||
? text
|
||||
: ('HTTP ' + response.status);
|
||||
var error = new Error(message);
|
||||
error.status = response.status;
|
||||
error.payload = payload;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function get(path) {
|
||||
return request(API_BASE + path);
|
||||
}
|
||||
|
||||
function post(path, body) {
|
||||
return request(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
function patch(path, body) {
|
||||
return request(API_BASE + path, {
|
||||
method: 'PATCH',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
function del(path) {
|
||||
return request(API_BASE + path, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
function query(params) {
|
||||
var search = new URLSearchParams();
|
||||
Object.keys(params || {}).forEach(function(key) {
|
||||
var value = params[key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
search.set(key, value);
|
||||
});
|
||||
var encoded = search.toString();
|
||||
return encoded ? ('?' + encoded) : '';
|
||||
}
|
||||
|
||||
function postBytes(path, bytes, fileName) {
|
||||
return request(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: headers(fileName ? { 'X-File-Name': fileName } : {}),
|
||||
body: bytes,
|
||||
});
|
||||
}
|
||||
|
||||
window.CrankApi = {
|
||||
login: function(payload) {
|
||||
return request(AUTH_BASE + '/login', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
logout: function() {
|
||||
return request(AUTH_BASE + '/logout', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
},
|
||||
getSession: function() {
|
||||
return request(AUTH_BASE + '/session');
|
||||
},
|
||||
getProfile: function() {
|
||||
return request(AUTH_BASE + '/profile');
|
||||
},
|
||||
updateProfile: function(payload) {
|
||||
return request(AUTH_BASE + '/profile', {
|
||||
method: 'PATCH',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
changePassword: function(payload) {
|
||||
return request(AUTH_BASE + '/password', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
setCurrentWorkspace: function(workspaceId) {
|
||||
return request(AUTH_BASE + '/current-workspace', {
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ workspace_id: workspaceId }),
|
||||
});
|
||||
},
|
||||
listWorkspaces: function() {
|
||||
return get('/workspaces');
|
||||
},
|
||||
getCapabilities: function() {
|
||||
return get('/capabilities');
|
||||
},
|
||||
createWorkspace: function(payload) {
|
||||
return post('/workspaces', payload);
|
||||
},
|
||||
getWorkspace: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId));
|
||||
},
|
||||
updateWorkspace: function(workspaceId, payload) {
|
||||
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
|
||||
},
|
||||
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');
|
||||
},
|
||||
createInvitation: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations', payload);
|
||||
},
|
||||
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');
|
||||
},
|
||||
getOperation: function(workspaceId, operationId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId));
|
||||
},
|
||||
getOperationVersion: function(workspaceId, operationId, version) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/versions/' + encodeURIComponent(version));
|
||||
},
|
||||
createOperation: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations', payload);
|
||||
},
|
||||
updateOperation: function(workspaceId, operationId, payload) {
|
||||
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId), payload);
|
||||
},
|
||||
deleteOperation: function(workspaceId, operationId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId));
|
||||
},
|
||||
archiveOperation: function(workspaceId, operationId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/archive', {});
|
||||
},
|
||||
publishOperation: function(workspaceId, operationId, version) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/publish', {
|
||||
version: version,
|
||||
});
|
||||
},
|
||||
createOperationVersion: function(workspaceId, operationId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/versions', payload);
|
||||
},
|
||||
runOperationTest: function(workspaceId, operationId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/test-runs', payload);
|
||||
},
|
||||
uploadInputSample: function(workspaceId, operationId, sample) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/samples/input-json', sample);
|
||||
},
|
||||
uploadOutputSample: function(workspaceId, operationId, sample) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/samples/output-json', sample);
|
||||
},
|
||||
generateDraft: function(workspaceId, operationId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/drafts/generate', payload || {});
|
||||
},
|
||||
exportOperation: function(workspaceId, operationId, params) {
|
||||
return requestText(
|
||||
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/export' + query(params),
|
||||
{
|
||||
headers: headers({ 'Accept': 'application/yaml' }),
|
||||
}
|
||||
);
|
||||
},
|
||||
importOperation: function(workspaceId, yamlDocument, mode) {
|
||||
return request(
|
||||
API_BASE + '/workspaces/' + encodeURIComponent(workspaceId) + '/operations/import' + query({ mode: mode }),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers({ 'Content-Type': 'application/yaml' }),
|
||||
body: yamlDocument,
|
||||
}
|
||||
);
|
||||
},
|
||||
listAgents: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents');
|
||||
},
|
||||
createAgent: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents', payload);
|
||||
},
|
||||
getAgent: function(workspaceId, agentId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
|
||||
},
|
||||
updateAgent: function(workspaceId, agentId, payload) {
|
||||
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId), payload);
|
||||
},
|
||||
deleteAgent: function(workspaceId, agentId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId));
|
||||
},
|
||||
saveAgentBindings: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/bindings', payload);
|
||||
},
|
||||
publishAgent: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
|
||||
},
|
||||
unpublishAgent: function(workspaceId, agentId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/unpublish', {});
|
||||
},
|
||||
archiveAgent: function(workspaceId, agentId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/archive', {});
|
||||
},
|
||||
listAgentPlatformApiKeys: function(workspaceId, agentId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys');
|
||||
},
|
||||
createAgentPlatformApiKey: function(workspaceId, agentId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys', payload);
|
||||
},
|
||||
revokeAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId) + '/revoke', {});
|
||||
},
|
||||
deleteAgentPlatformApiKey: function(workspaceId, agentId, keyId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/platform-api-keys/' + encodeURIComponent(keyId));
|
||||
},
|
||||
listSecrets: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets');
|
||||
},
|
||||
createSecret: function(workspaceId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets', payload);
|
||||
},
|
||||
getSecret: function(workspaceId, secretId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
|
||||
},
|
||||
rotateSecret: function(workspaceId, secretId, payload) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId) + '/rotate', payload);
|
||||
},
|
||||
deleteSecret: function(workspaceId, secretId) {
|
||||
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
|
||||
},
|
||||
listAuthProfiles: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/auth-profiles');
|
||||
},
|
||||
listLogs: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
|
||||
},
|
||||
getLog: function(workspaceId, logId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
|
||||
},
|
||||
getUsageOverview: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||
},
|
||||
getProtocolCapabilities: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
|
||||
},
|
||||
getOperationUsage: function(workspaceId, operationId, params) {
|
||||
return get(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
|
||||
);
|
||||
},
|
||||
getAgentUsage: function(workspaceId, agentId, params) {
|
||||
return get(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/agents/' + encodeURIComponent(agentId) + query(params)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,253 @@
|
||||
(function() {
|
||||
var STORAGE_KEY = 'crank_user';
|
||||
var sessionCache = null;
|
||||
var sessionPromise = null;
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
function roleLabel(role) {
|
||||
var normalized = String(role || 'viewer').toLowerCase();
|
||||
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
|
||||
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return normalized.replace(/^\w/, function(char) { return char.toUpperCase(); });
|
||||
}
|
||||
|
||||
function currentWorkspaceLabel() {
|
||||
if (window.getCurrentWorkspace) {
|
||||
var workspace = window.getCurrentWorkspace();
|
||||
if (workspace && workspace.name) {
|
||||
return workspace.name;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return localStorage.getItem('crank_workspace_slug') || tKey('settings.nav.workspace');
|
||||
} catch (_error) {
|
||||
return tKey('settings.nav.workspace');
|
||||
}
|
||||
}
|
||||
|
||||
function isLoginPage() {
|
||||
return /\/(?:html\/login\.html|login)\/?$/.test(window.location.pathname);
|
||||
}
|
||||
|
||||
function loginUrl() {
|
||||
return (window.CrankRoutes && window.CrankRoutes.login) || '/login';
|
||||
}
|
||||
|
||||
function homeUrl() {
|
||||
return (window.CrankRoutes && window.CrankRoutes.home) || '/';
|
||||
}
|
||||
|
||||
function clearUserMirror() {
|
||||
sessionCache = null;
|
||||
sessionPromise = null;
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (_error) {}
|
||||
renderShellIdentity(null);
|
||||
}
|
||||
|
||||
function primaryMembership(session) {
|
||||
if (!(session && session.memberships && session.memberships.length)) {
|
||||
return null;
|
||||
}
|
||||
if (session.current_workspace_id) {
|
||||
var currentMembership = session.memberships.find(function(item) {
|
||||
return item.workspace && item.workspace.id === session.current_workspace_id;
|
||||
});
|
||||
if (currentMembership) {
|
||||
return currentMembership;
|
||||
}
|
||||
}
|
||||
if (window.getCurrentWorkspace) {
|
||||
var currentWorkspace = window.getCurrentWorkspace();
|
||||
if (currentWorkspace) {
|
||||
var matched = session.memberships.find(function(item) {
|
||||
return item.workspace && item.workspace.id === currentWorkspace.id;
|
||||
});
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
return session.memberships[0];
|
||||
}
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
return source
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map(function(part) { return part.charAt(0).toUpperCase(); })
|
||||
.join('') || 'CR';
|
||||
}
|
||||
|
||||
function persistUserMirror(session) {
|
||||
var membership = primaryMembership(session);
|
||||
var user = {
|
||||
id: session.user.id,
|
||||
name: session.user.display_name,
|
||||
email: session.user.email,
|
||||
role: membership ? roleLabel(membership.role) : tKey('workspace_setup.role.viewer'),
|
||||
workspace: membership ? membership.workspace.slug : '',
|
||||
workspaceId: membership ? membership.workspace.id : '',
|
||||
initials: initials(session.user.display_name, session.user.email),
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function mirroredUser() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderShellIdentity(session) {
|
||||
var membership = primaryMembership(session);
|
||||
var fallbackUser = mirroredUser();
|
||||
var displayName = session && session.user
|
||||
? (session.user.display_name || session.user.email || 'Crank')
|
||||
: (fallbackUser && fallbackUser.name) || 'Crank';
|
||||
var email = session && session.user
|
||||
? session.user.email
|
||||
: fallbackUser && fallbackUser.email;
|
||||
var role = membership
|
||||
? roleLabel(membership.role)
|
||||
: (fallbackUser && fallbackUser.role) || tKey('workspace_setup.role.viewer');
|
||||
var workspace = membership && membership.workspace
|
||||
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
|
||||
: currentWorkspaceLabel();
|
||||
var avatarValue = initials(displayName, email);
|
||||
|
||||
document.querySelectorAll('.nav-avatar').forEach(function(element) {
|
||||
element.textContent = avatarValue;
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
|
||||
element.textContent = displayName;
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
|
||||
element.textContent = role + ' · ' + workspace;
|
||||
});
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureShellTestIds === 'function') {
|
||||
window.CrankDiagnostics.ensureShellTestIds();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceSession(session) {
|
||||
sessionCache = session;
|
||||
persistUserMirror(session);
|
||||
renderShellIdentity(session);
|
||||
window.dispatchEvent(new CustomEvent('crank:sessionchange', { detail: session }));
|
||||
return session;
|
||||
}
|
||||
|
||||
async function fetchSession(force) {
|
||||
if (!force && sessionCache) {
|
||||
return sessionCache;
|
||||
}
|
||||
if (!force && sessionPromise) {
|
||||
return sessionPromise;
|
||||
}
|
||||
|
||||
sessionPromise = window.CrankApi.getSession()
|
||||
.then(function(session) {
|
||||
return replaceSession(session);
|
||||
})
|
||||
.catch(function(error) {
|
||||
clearUserMirror();
|
||||
throw error;
|
||||
})
|
||||
.finally(function() {
|
||||
sessionPromise = null;
|
||||
});
|
||||
|
||||
return sessionPromise;
|
||||
}
|
||||
|
||||
async function guardProtectedPage() {
|
||||
try {
|
||||
return await fetchSession(false);
|
||||
} catch (error) {
|
||||
if (error && error.status === 401) {
|
||||
window.location.replace(loginUrl());
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function guardLoginPage() {
|
||||
try {
|
||||
await fetchSession(false);
|
||||
window.location.replace(homeUrl());
|
||||
} catch (error) {
|
||||
if (!error || error.status !== 401) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email, password) {
|
||||
var session = await window.CrankApi.login({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
replaceSession(session);
|
||||
window.location.href = homeUrl();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await window.CrankApi.logout();
|
||||
} finally {
|
||||
clearUserMirror();
|
||||
window.location.href = loginUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnauthorized() {
|
||||
if (isLoginPage()) {
|
||||
clearUserMirror();
|
||||
return;
|
||||
}
|
||||
clearUserMirror();
|
||||
window.location.replace(loginUrl());
|
||||
}
|
||||
|
||||
window.CrankAuth = {
|
||||
fetchSession: fetchSession,
|
||||
replaceSession: replaceSession,
|
||||
getCachedSession: function() { return sessionCache; },
|
||||
renderShellIdentity: function() {
|
||||
renderShellIdentity(sessionCache);
|
||||
},
|
||||
guardProtectedPage: guardProtectedPage,
|
||||
guardLoginPage: guardLoginPage,
|
||||
login: login,
|
||||
logout: logout,
|
||||
handleUnauthorized: handleUnauthorized,
|
||||
};
|
||||
|
||||
window.addEventListener('crank:workspacechange', function() {
|
||||
renderShellIdentity(sessionCache);
|
||||
});
|
||||
|
||||
window.addEventListener('crank:sessionchange', function(event) {
|
||||
renderShellIdentity(event.detail || sessionCache);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
renderShellIdentity(sessionCache);
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,535 @@
|
||||
const PAGE_SIZE_DESKTOP = 6;
|
||||
const PAGE_SIZE_MOBILE = 4;
|
||||
const MOBILE_BREAKPOINT = 960;
|
||||
|
||||
function getSortOptions() {
|
||||
return [
|
||||
{ value: 'created_desc', label: (window.t && t('sort.created_desc')) || 'Sort: Last created' },
|
||||
{ value: 'created_asc', label: (window.t && t('sort.created_asc')) || 'Sort: Oldest first' },
|
||||
{ value: 'name_asc', label: (window.t && t('sort.name_asc')) || 'Sort: Name A–Z' },
|
||||
{ value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name Z–A' },
|
||||
];
|
||||
}
|
||||
|
||||
function getTabDefs() {
|
||||
return [
|
||||
{ id: 'all', label: (window.t && t('tab.all')) || 'All' },
|
||||
{ id: 'active', label: (window.t && t('tab.active')) || 'Active' },
|
||||
{ id: 'draft', label: (window.t && t('tab.draft')) || 'Draft' },
|
||||
{ id: 'error', label: (window.t && t('tab.error')) || 'Error' },
|
||||
{ id: 'inactive', label: (window.t && t('tab.inactive')) || 'Inactive' },
|
||||
];
|
||||
}
|
||||
|
||||
function uiStatus(rawStatus) {
|
||||
if (rawStatus === 'published') return 'active';
|
||||
if (rawStatus === 'archived') return 'inactive';
|
||||
if (rawStatus === 'testing') return 'active';
|
||||
return rawStatus || 'draft';
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||||
}
|
||||
|
||||
function mapOperation(item) {
|
||||
var mapped = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
display_name: item.display_name,
|
||||
protocol: item.protocol,
|
||||
status: uiStatus(item.status),
|
||||
raw_status: item.status,
|
||||
category: item.category || 'general',
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
published_at: item.published_at,
|
||||
target_url: item.target_url || '',
|
||||
method: item.target_action || '',
|
||||
usage_summary: item.usage_summary || {
|
||||
calls_today: 0,
|
||||
error_rate_pct: 0,
|
||||
avg_latency_ms: 0,
|
||||
},
|
||||
agent_refs: item.agent_refs || [],
|
||||
};
|
||||
|
||||
return window.localizeDemoOperation ? window.localizeDemoOperation(mapped) : mapped;
|
||||
}
|
||||
|
||||
function emptyStats() {
|
||||
return {
|
||||
total: 0,
|
||||
active: 0,
|
||||
requestsToday: 0,
|
||||
averageLatencyMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function computeStats(operations) {
|
||||
if (!operations.length) return emptyStats();
|
||||
|
||||
var requestsToday = operations.reduce(function(sum, operation) {
|
||||
return sum + (operation.usage_summary ? operation.usage_summary.calls_today : 0);
|
||||
}, 0);
|
||||
var latencyBase = operations.filter(function(operation) {
|
||||
return operation.usage_summary && operation.usage_summary.calls_today > 0;
|
||||
});
|
||||
var latencyWeight = latencyBase.reduce(function(sum, operation) {
|
||||
return sum + operation.usage_summary.calls_today;
|
||||
}, 0);
|
||||
var weightedLatency = latencyBase.reduce(function(sum, operation) {
|
||||
return sum + (operation.usage_summary.avg_latency_ms * operation.usage_summary.calls_today);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
total: operations.length,
|
||||
active: operations.filter(function(operation) { return operation.status === 'active'; }).length,
|
||||
requestsToday: requestsToday,
|
||||
averageLatencyMs: latencyWeight > 0 ? Math.round(weightedLatency / latencyWeight) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', function() {
|
||||
Alpine.data('catalog', function() {
|
||||
return {
|
||||
operations: [],
|
||||
loading: true,
|
||||
loadError: '',
|
||||
pageSize: PAGE_SIZE_DESKTOP,
|
||||
search: '',
|
||||
tab: 'all',
|
||||
filterProtocol: null,
|
||||
filterCategory: null,
|
||||
filterAgent: null,
|
||||
sort: 'created_desc',
|
||||
page: 1,
|
||||
openDropdown: null,
|
||||
navOpen: false,
|
||||
categoryOptions: [],
|
||||
workspaceId: null,
|
||||
stats: emptyStats(),
|
||||
_opsVersion: 0,
|
||||
_nonStatusViewCache: null,
|
||||
_nonStatusViewKey: '',
|
||||
_filteredCache: null,
|
||||
_filteredKey: '',
|
||||
_agentsByOpIdCache: null,
|
||||
_agentsByOpIdCacheVersion: -1,
|
||||
_activeAgentsCache: null,
|
||||
_activeAgentsCacheVersion: -1,
|
||||
|
||||
async init() {
|
||||
var self = this;
|
||||
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
this.workspaceId = workspace ? workspace.id : null;
|
||||
|
||||
var updatePageSize = function() {
|
||||
self.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP;
|
||||
self.page = 1;
|
||||
};
|
||||
|
||||
updatePageSize();
|
||||
window.addEventListener('resize', updatePageSize);
|
||||
window.addEventListener('crank:langchange', function() {
|
||||
self.sort = self.sort;
|
||||
applyLang();
|
||||
});
|
||||
window.addEventListener('crank:workspacechange', async function(event) {
|
||||
self.workspaceId = event.detail ? event.detail.id : null;
|
||||
await self.reload();
|
||||
});
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!event.target.closest('.filter-dropdown, .sort-dropdown, .user-menu')) {
|
||||
self.openDropdown = null;
|
||||
}
|
||||
if (!event.target.closest('.navbar')) {
|
||||
self.navOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
await this.reload();
|
||||
},
|
||||
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listOperations(this.workspaceId);
|
||||
this.replaceOperations((response && response.items ? response.items : []).map(mapOperation));
|
||||
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
|
||||
return operation.category;
|
||||
}).filter(Boolean))).sort();
|
||||
this.stats = computeStats(this.operations);
|
||||
} catch (error) {
|
||||
this.replaceOperations([]);
|
||||
this.categoryOptions = [];
|
||||
this.stats = emptyStats();
|
||||
this.loadError = error.message || 'Failed to load operations';
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
replaceOperations(operations) {
|
||||
this.operations = operations;
|
||||
this._opsVersion += 1;
|
||||
this._nonStatusViewCache = null;
|
||||
this._nonStatusViewKey = '';
|
||||
this._filteredCache = null;
|
||||
this._filteredKey = '';
|
||||
this._agentsByOpIdCache = null;
|
||||
this._agentsByOpIdCacheVersion = -1;
|
||||
this._activeAgentsCache = null;
|
||||
this._activeAgentsCacheVersion = -1;
|
||||
},
|
||||
|
||||
_applyNonStatusFilters(operations) {
|
||||
var filtered = operations;
|
||||
|
||||
if (this.search.trim()) {
|
||||
var query = this.search.toLowerCase();
|
||||
filtered = filtered.filter(function(operation) {
|
||||
return operation.name.toLowerCase().includes(query)
|
||||
|| operation.display_name.toLowerCase().includes(query)
|
||||
|| operation.category.toLowerCase().includes(query)
|
||||
|| (operation.target_url || '').toLowerCase().includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.filterProtocol) {
|
||||
filtered = filtered.filter(function(operation) { return operation.protocol === this.filterProtocol; }, this);
|
||||
}
|
||||
|
||||
if (this.filterCategory) {
|
||||
filtered = filtered.filter(function(operation) { return operation.category === this.filterCategory; }, this);
|
||||
}
|
||||
|
||||
if (this.filterAgent) {
|
||||
filtered = filtered.filter(function(operation) {
|
||||
return (operation.agent_refs || []).some(function(agent) { return agent.agent_id === this.filterAgent; }, this);
|
||||
}, this);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
_nonStatusView() {
|
||||
var key = [
|
||||
this._opsVersion,
|
||||
this.search.trim(),
|
||||
this.filterProtocol || '',
|
||||
this.filterCategory || '',
|
||||
this.filterAgent || '',
|
||||
].join('|');
|
||||
|
||||
if (this._nonStatusViewCache && this._nonStatusViewKey === key) {
|
||||
return this._nonStatusViewCache;
|
||||
}
|
||||
|
||||
var operations = this._applyNonStatusFilters(this.operations);
|
||||
var tabCounts = {
|
||||
all: operations.length,
|
||||
active: 0,
|
||||
draft: 0,
|
||||
error: 0,
|
||||
inactive: 0,
|
||||
};
|
||||
|
||||
operations.forEach(function(operation) {
|
||||
if (Object.prototype.hasOwnProperty.call(tabCounts, operation.status)) {
|
||||
tabCounts[operation.status] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
this._nonStatusViewKey = key;
|
||||
this._nonStatusViewCache = {
|
||||
operations: operations,
|
||||
tabCounts: tabCounts,
|
||||
};
|
||||
return this._nonStatusViewCache;
|
||||
},
|
||||
|
||||
get filtered() {
|
||||
var nonStatusView = this._nonStatusView();
|
||||
var key = [
|
||||
this._nonStatusViewKey,
|
||||
this.tab,
|
||||
this.sort,
|
||||
].join('|');
|
||||
|
||||
if (this._filteredCache && this._filteredKey === key) {
|
||||
return this._filteredCache;
|
||||
}
|
||||
|
||||
var operations = nonStatusView.operations;
|
||||
|
||||
if (this.tab !== 'all') {
|
||||
operations = operations.filter(function(operation) {
|
||||
return operation.status === this.tab;
|
||||
}, this);
|
||||
}
|
||||
|
||||
operations = operations.slice().sort(function(left, right) {
|
||||
if (this.sort === 'created_desc') return new Date(right.created_at) - new Date(left.created_at);
|
||||
if (this.sort === 'created_asc') return new Date(left.created_at) - new Date(right.created_at);
|
||||
if (this.sort === 'name_asc') return left.name.localeCompare(right.name);
|
||||
if (this.sort === 'name_desc') return right.name.localeCompare(left.name);
|
||||
return 0;
|
||||
}.bind(this));
|
||||
|
||||
this._filteredKey = key;
|
||||
this._filteredCache = operations;
|
||||
return this._filteredCache;
|
||||
},
|
||||
|
||||
get totalFiltered() {
|
||||
return this.filtered.length;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize));
|
||||
},
|
||||
|
||||
get paginated() {
|
||||
var start = (this.page - 1) * this.pageSize;
|
||||
return this.filtered.slice(start, start + this.pageSize);
|
||||
},
|
||||
|
||||
get pageStart() {
|
||||
return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||||
},
|
||||
|
||||
get pageEnd() {
|
||||
return Math.min(this.page * this.pageSize, this.totalFiltered);
|
||||
},
|
||||
|
||||
tabCount(tab) {
|
||||
var tabCounts = this._nonStatusView().tabCounts;
|
||||
return Object.prototype.hasOwnProperty.call(tabCounts, tab) ? tabCounts[tab] : 0;
|
||||
},
|
||||
|
||||
get activeChips() {
|
||||
var chips = [];
|
||||
if (this.filterProtocol) chips.push({ key: 'protocol', label: this.tfKey('ops.filter.protocol', { value: this.filterProtocol.toUpperCase() }) });
|
||||
if (this.filterCategory) chips.push({ key: 'category', label: this.tfKey('ops.filter.category', { value: this.filterCategory }) });
|
||||
if (this.filterAgent) {
|
||||
var agent = this.activeAgents.find(function(item) { return item.agent_id === this.filterAgent; }, this);
|
||||
chips.push({ key: 'agent', label: this.tfKey('ops.filter.agent', { value: agent ? agent.display_name : '' }) });
|
||||
}
|
||||
return chips;
|
||||
},
|
||||
|
||||
get hasActiveFilters() {
|
||||
return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim());
|
||||
},
|
||||
|
||||
clearChip(key) {
|
||||
if (key === 'protocol') this.filterProtocol = null;
|
||||
if (key === 'category') this.filterCategory = null;
|
||||
if (key === 'agent') this.filterAgent = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
this.filterProtocol = null;
|
||||
this.filterCategory = null;
|
||||
this.filterAgent = null;
|
||||
this.search = '';
|
||||
this.tab = 'all';
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
get agentsByOpId() {
|
||||
if (this._agentsByOpIdCache && this._agentsByOpIdCacheVersion === this._opsVersion) {
|
||||
return this._agentsByOpIdCache;
|
||||
}
|
||||
var map = {};
|
||||
this.operations.forEach(function(operation) {
|
||||
map[operation.id] = operation.agent_refs || [];
|
||||
});
|
||||
this._agentsByOpIdCacheVersion = this._opsVersion;
|
||||
this._agentsByOpIdCache = map;
|
||||
return this._agentsByOpIdCache;
|
||||
},
|
||||
|
||||
get activeAgents() {
|
||||
if (this._activeAgentsCache && this._activeAgentsCacheVersion === this._opsVersion) {
|
||||
return this._activeAgentsCache;
|
||||
}
|
||||
var seen = {};
|
||||
var agents = [];
|
||||
this.operations.forEach(function(operation) {
|
||||
(operation.agent_refs || []).forEach(function(agent) {
|
||||
if (!seen[agent.agent_id]) {
|
||||
seen[agent.agent_id] = true;
|
||||
agents.push(agent);
|
||||
}
|
||||
});
|
||||
});
|
||||
this._activeAgentsCacheVersion = this._opsVersion;
|
||||
this._activeAgentsCache = agents.sort(function(left, right) {
|
||||
return left.display_name.localeCompare(right.display_name);
|
||||
});
|
||||
return this._activeAgentsCache;
|
||||
},
|
||||
|
||||
get sortLabel() {
|
||||
return getSortOptions().find(function(option) { return option.value === this.sort; }, this)?.label || 'Sort';
|
||||
},
|
||||
|
||||
setTab(tab) {
|
||||
this.tab = tab;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
setSearch(value) {
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
setProtocol(value) {
|
||||
this.filterProtocol = this.filterProtocol === value ? null : value;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
setCategory(value) {
|
||||
this.filterCategory = this.filterCategory === value ? null : value;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
setSort(value) {
|
||||
this.sort = value;
|
||||
this.openDropdown = null;
|
||||
},
|
||||
|
||||
setAgent(value) {
|
||||
this.filterAgent = this.filterAgent === value ? null : value;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
clearProtocol() {
|
||||
this.filterProtocol = null;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
clearCategory() {
|
||||
this.filterCategory = null;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
clearAgent() {
|
||||
this.filterAgent = null;
|
||||
this.openDropdown = null;
|
||||
this.page = 1;
|
||||
},
|
||||
|
||||
toggleDropdown(name) {
|
||||
this.openDropdown = this.openDropdown === name ? null : name;
|
||||
},
|
||||
|
||||
goPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) this.page = page;
|
||||
},
|
||||
|
||||
editOperation(operation) {
|
||||
window.location.href = ((window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/')
|
||||
+ '?mode=edit&operationId=' + encodeURIComponent(operation.id);
|
||||
},
|
||||
|
||||
async deleteOperation(id) {
|
||||
if (!confirm(this.tKey('ops.delete.confirm'))) return;
|
||||
try {
|
||||
var operation = this.operations.find(function(item) { return item.id === id; });
|
||||
await window.CrankApi.deleteOperation(this.workspaceId, id);
|
||||
this.replaceOperations(this.operations.filter(function(operation) { return operation.id !== id; }));
|
||||
this.categoryOptions = Array.from(new Set(this.operations.map(function(operation) {
|
||||
return operation.category;
|
||||
}).filter(Boolean))).sort();
|
||||
this.stats = computeStats(this.operations);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
this.tfKey('ops.delete.success.message', {
|
||||
name: operation ? (operation.display_name || operation.name) : ''
|
||||
}),
|
||||
this.tKey('ops.delete.success.title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || this.tKey('ops.delete.error.message'),
|
||||
this.tKey('ops.delete.error.title')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
protocolLabel(operation) {
|
||||
return operation.method ? ('REST · ' + operation.method) : 'REST';
|
||||
},
|
||||
|
||||
protocolClass(operation) {
|
||||
return 'badge badge-' + operation.protocol;
|
||||
},
|
||||
|
||||
statusClass(operation) {
|
||||
return 'status-badge status-' + operation.status;
|
||||
},
|
||||
|
||||
statusLabel(operation) {
|
||||
if (operation.status === 'active') return this.tKey('tab.active');
|
||||
if (operation.status === 'inactive') return this.tKey('tab.inactive');
|
||||
if (operation.status === 'error') return this.tKey('tab.error');
|
||||
return this.tKey('tab.draft');
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
},
|
||||
|
||||
truncateUrl(url) {
|
||||
if (!url) return '—';
|
||||
return url.length > 42 ? url.slice(0, 42) + '…' : url;
|
||||
},
|
||||
|
||||
formatMetricNumber(value) {
|
||||
return new Intl.NumberFormat(currentLocale()).format(value || 0);
|
||||
},
|
||||
|
||||
tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
},
|
||||
|
||||
tfKey(key, vars) {
|
||||
return window.tf ? tf(key, vars) : this.tKey(key);
|
||||
},
|
||||
|
||||
get sortOptions() {
|
||||
return getSortOptions();
|
||||
},
|
||||
|
||||
get tabDefs() {
|
||||
return getTabDefs();
|
||||
},
|
||||
|
||||
handleNewOperation() {
|
||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/';
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
window.CrankAuth.logout();
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
(function() {
|
||||
window.APP_BASE = '/';
|
||||
window.DATA_URL = '/data/';
|
||||
window.CrankRoutes = {
|
||||
home: '/',
|
||||
login: '/login',
|
||||
agents: '/agents',
|
||||
apiKeys: '/api-keys',
|
||||
secrets: '/secrets',
|
||||
logs: '/logs',
|
||||
usage: '/usage',
|
||||
settings: '/settings',
|
||||
workspaceSetup: '/workspace-setup',
|
||||
wizard: '/wizard/'
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,111 @@
|
||||
(function() {
|
||||
var currentPage = '';
|
||||
|
||||
function messageFor(error) {
|
||||
if (!error) {
|
||||
return 'unknown error';
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function setPage(pageName) {
|
||||
currentPage = pageName || '';
|
||||
document.documentElement.setAttribute('data-crank-page', currentPage || 'unknown');
|
||||
}
|
||||
|
||||
function markBootstrapState(pageName, status, details) {
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-page', pageName || currentPage || 'unknown');
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-state', status);
|
||||
if (status === 'error' && details) {
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-error', details);
|
||||
return;
|
||||
}
|
||||
document.documentElement.removeAttribute('data-crank-bootstrap-error');
|
||||
}
|
||||
|
||||
function report(stage, error, pageName) {
|
||||
var resolvedPage = pageName || currentPage || 'unknown';
|
||||
console.error('[CrankUI][' + resolvedPage + '][' + stage + ']', error);
|
||||
window.dispatchEvent(new CustomEvent('crank:ui-error', {
|
||||
detail: {
|
||||
page: resolvedPage,
|
||||
stage: stage,
|
||||
message: messageFor(error),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function bootstrap(pageName, init) {
|
||||
function run() {
|
||||
setPage(pageName);
|
||||
markBootstrapState(pageName, 'pending');
|
||||
try {
|
||||
var result = init();
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function() {
|
||||
markBootstrapState(pageName, 'ready');
|
||||
}).catch(function(error) {
|
||||
markBootstrapState(pageName, 'error', messageFor(error));
|
||||
report('bootstrap', error, pageName);
|
||||
});
|
||||
return;
|
||||
}
|
||||
markBootstrapState(pageName, 'ready');
|
||||
} catch (error) {
|
||||
markBootstrapState(pageName, 'error', messageFor(error));
|
||||
report('bootstrap', error, pageName);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run, { once: true });
|
||||
return;
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
function ensureTestId(element, value) {
|
||||
if (!element || !value) {
|
||||
return;
|
||||
}
|
||||
element.setAttribute('data-testid', value);
|
||||
}
|
||||
|
||||
function ensureShellTestIds() {
|
||||
document.querySelectorAll('.nav-avatar').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-avatar');
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-user-name');
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-user-role');
|
||||
});
|
||||
ensureTestId(document.getElementById('ws-current-name'), 'shell-workspace-name');
|
||||
}
|
||||
|
||||
window.addEventListener('error', function(event) {
|
||||
if (!currentPage) {
|
||||
return;
|
||||
}
|
||||
report('runtime', event.error || new Error(event.message || 'window error'));
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
if (!currentPage) {
|
||||
return;
|
||||
}
|
||||
report('unhandledrejection', event.reason || new Error('unhandled rejection'));
|
||||
});
|
||||
|
||||
window.CrankDiagnostics = {
|
||||
bootstrap: bootstrap,
|
||||
ensureTestId: ensureTestId,
|
||||
ensureShellTestIds: ensureShellTestIds,
|
||||
report: report,
|
||||
setPage: setPage,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,32 @@
|
||||
(function() {
|
||||
function clear(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyState(title, body) {
|
||||
var root = document.createElement('div');
|
||||
root.className = 'empty-state';
|
||||
|
||||
var titleNode = document.createElement('div');
|
||||
titleNode.className = 'empty-state-title';
|
||||
titleNode.textContent = title;
|
||||
root.appendChild(titleNode);
|
||||
|
||||
var bodyNode = document.createElement('div');
|
||||
bodyNode.className = 'empty-state-text';
|
||||
bodyNode.textContent = body;
|
||||
root.appendChild(bodyNode);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
window.CrankDom = {
|
||||
clear: clear,
|
||||
createEmptyState: createEmptyState,
|
||||
};
|
||||
}());
|
||||
+2099
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
(function() {
|
||||
function tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
var errorElement = document.getElementById('login-error');
|
||||
errorElement.classList.add('is-visible');
|
||||
errorElement.textContent = message;
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
var errorElement = document.getElementById('login-error');
|
||||
errorElement.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
function initLoginPage() {
|
||||
window.CrankAuth.guardLoginPage().catch(function(error) {
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.report === 'function') {
|
||||
window.CrankDiagnostics.report('guard-login-page', error, 'login');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var email = document.getElementById('email').value.trim();
|
||||
var password = document.getElementById('password').value;
|
||||
|
||||
if (!email || !password) {
|
||||
showError(tKey('login.error.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
hideError();
|
||||
|
||||
try {
|
||||
await window.CrankAuth.login(email, password);
|
||||
} catch (error) {
|
||||
if (error && error.status === 401) {
|
||||
showError(tKey('login.error.invalid'));
|
||||
return;
|
||||
}
|
||||
showError(tKey('login.error.generic'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('login', initLoginPage);
|
||||
return;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', initLoginPage);
|
||||
}());
|
||||
@@ -0,0 +1,419 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
logs: [],
|
||||
details: {},
|
||||
level: 'all',
|
||||
search: '',
|
||||
period: '7d',
|
||||
openId: null,
|
||||
liveMode: true,
|
||||
timer: null,
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
loadError: '',
|
||||
};
|
||||
|
||||
var logList = document.getElementById('log-list');
|
||||
var logSearch = document.getElementById('log-search');
|
||||
var refreshBtn = document.getElementById('refresh-btn');
|
||||
var timeRangeSel = document.getElementById('time-range');
|
||||
var liveDot = document.querySelector('.live-dot');
|
||||
var liveLabel = document.querySelector('.live-label');
|
||||
|
||||
function tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function durationLabel(durationMs) {
|
||||
if (!durationMs) {
|
||||
return '0ms';
|
||||
}
|
||||
if (durationMs >= 1000) {
|
||||
return (durationMs / 1000).toFixed(1) + 's';
|
||||
}
|
||||
return durationMs + 'ms';
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function formatTime(timestamp) {
|
||||
var date = new Date(timestamp);
|
||||
return date.toISOString().slice(11, 23);
|
||||
}
|
||||
|
||||
function element(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function statusClass(statusCode) {
|
||||
if (!statusCode) {
|
||||
return '';
|
||||
}
|
||||
if (statusCode < 300) {
|
||||
return 'ok';
|
||||
}
|
||||
if (statusCode < 500) {
|
||||
return 'warn';
|
||||
}
|
||||
return 'err';
|
||||
}
|
||||
|
||||
function normalizeLog(record) {
|
||||
var log = record.log;
|
||||
var agent = window.localizeDemoAgent
|
||||
? window.localizeDemoAgent({
|
||||
slug: record.agent_slug,
|
||||
display_name: record.agent_display_name,
|
||||
description: '',
|
||||
})
|
||||
: null;
|
||||
var operation = window.localizeDemoOperation
|
||||
? window.localizeDemoOperation({
|
||||
name: record.operation_name,
|
||||
operation_display_name: record.operation_display_name,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: log.id,
|
||||
createdAt: log.created_at,
|
||||
level: log.level,
|
||||
source: log.source,
|
||||
status: log.status,
|
||||
statusCode: log.status_code,
|
||||
durationMs: log.duration_ms,
|
||||
toolName: log.tool_name,
|
||||
message: log.message,
|
||||
operationName: record.operation_name,
|
||||
operationDisplayName: operation ? operation.operation_display_name : record.operation_display_name,
|
||||
agentSlug: record.agent_slug,
|
||||
agentDisplayName: agent ? agent.display_name : record.agent_display_name,
|
||||
requestPreview: log.request_preview,
|
||||
responsePreview: log.response_preview,
|
||||
errorKind: log.error_kind,
|
||||
requestId: log.request_id,
|
||||
};
|
||||
}
|
||||
|
||||
function renderEmpty(title, message) {
|
||||
logList.innerHTML = '';
|
||||
var empty = element('div', 'empty-state');
|
||||
empty.appendChild(element('div', 'empty-state-title', title));
|
||||
empty.appendChild(element('div', 'empty-state-text', message));
|
||||
logList.appendChild(empty);
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
if (state.loading && state.logs.length === 0) {
|
||||
renderEmpty(tKey('logs.loading.title'), tKey('logs.loading.sub'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.loadError) {
|
||||
renderEmpty(tKey('logs.error.title'), state.loadError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.logs.length) {
|
||||
renderEmpty(
|
||||
tKey('logs.empty.title'),
|
||||
state.search || state.level !== 'all'
|
||||
? tKey('logs.empty.filtered')
|
||||
: tKey('logs.empty.initial')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
state.logs.forEach(function (item) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'log-entry';
|
||||
row.setAttribute('data-id', item.id);
|
||||
|
||||
var timeEl = document.createElement('span');
|
||||
timeEl.className = 'log-time';
|
||||
timeEl.textContent = formatTime(item.createdAt);
|
||||
row.appendChild(timeEl);
|
||||
|
||||
var levelEl = document.createElement('span');
|
||||
levelEl.className = 'log-level ' + item.level;
|
||||
levelEl.textContent = item.level.toUpperCase();
|
||||
row.appendChild(levelEl);
|
||||
|
||||
var msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'log-msg';
|
||||
|
||||
var opSpan = document.createElement('span');
|
||||
opSpan.className = 'log-op-name';
|
||||
opSpan.textContent = item.operationDisplayName || item.operationName;
|
||||
msgDiv.appendChild(opSpan);
|
||||
msgDiv.appendChild(document.createTextNode('\u00a0\u00a0'));
|
||||
|
||||
if (item.statusCode) {
|
||||
var statusEl = document.createElement('span');
|
||||
statusEl.className = 'log-status ' + statusClass(item.statusCode);
|
||||
statusEl.textContent = item.statusCode;
|
||||
msgDiv.appendChild(statusEl);
|
||||
msgDiv.appendChild(document.createTextNode(' \u00b7 '));
|
||||
}
|
||||
|
||||
msgDiv.appendChild(document.createTextNode(item.message));
|
||||
row.appendChild(msgDiv);
|
||||
|
||||
var durationEl = document.createElement('span');
|
||||
var durationClass = item.level === 'error' ? ' error' : (item.durationMs >= 1000 ? ' slow' : '');
|
||||
durationEl.className = 'log-duration' + durationClass;
|
||||
durationEl.textContent = durationLabel(item.durationMs);
|
||||
row.appendChild(durationEl);
|
||||
|
||||
fragment.appendChild(row);
|
||||
|
||||
var expanded = document.createElement('div');
|
||||
expanded.className = 'log-entry-expanded' + (item.id === state.openId ? ' open' : '');
|
||||
expanded.setAttribute('data-exp', item.id);
|
||||
|
||||
if (item.id === state.openId) {
|
||||
var detail = state.details[item.id] || item;
|
||||
|
||||
if (detail.agentDisplayName || detail.agentSlug) {
|
||||
var agentLabel = document.createElement('div');
|
||||
agentLabel.className = 'log-detail-label';
|
||||
agentLabel.textContent = tKey('logs.detail.agent');
|
||||
expanded.appendChild(agentLabel);
|
||||
|
||||
var agentPre = document.createElement('pre');
|
||||
agentPre.className = 'log-detail-block';
|
||||
agentPre.textContent = detail.agentDisplayName || detail.agentSlug;
|
||||
expanded.appendChild(agentPre);
|
||||
}
|
||||
|
||||
var requestLabel = document.createElement('div');
|
||||
requestLabel.className = 'log-detail-label';
|
||||
requestLabel.textContent = tKey('logs.detail.request');
|
||||
expanded.appendChild(requestLabel);
|
||||
|
||||
var requestPre = document.createElement('pre');
|
||||
requestPre.className = 'log-detail-block';
|
||||
requestPre.textContent = formatJson(detail.requestPreview);
|
||||
expanded.appendChild(requestPre);
|
||||
|
||||
var responseLabel = document.createElement('div');
|
||||
responseLabel.className = 'log-detail-label';
|
||||
responseLabel.textContent = tKey('logs.detail.response');
|
||||
expanded.appendChild(responseLabel);
|
||||
|
||||
var responsePre = document.createElement('pre');
|
||||
responsePre.className = 'log-detail-block';
|
||||
responsePre.textContent = formatJson(detail.responsePreview);
|
||||
expanded.appendChild(responsePre);
|
||||
|
||||
if (detail.errorKind || detail.requestId) {
|
||||
var metaLabel = document.createElement('div');
|
||||
metaLabel.className = 'log-detail-label';
|
||||
metaLabel.textContent = tKey('logs.detail.meta');
|
||||
expanded.appendChild(metaLabel);
|
||||
|
||||
var metaPre = document.createElement('pre');
|
||||
metaPre.className = 'log-detail-block';
|
||||
metaPre.textContent = formatJson({
|
||||
request_id: detail.requestId || null,
|
||||
error_kind: detail.errorKind || null,
|
||||
source: detail.source,
|
||||
status: detail.status,
|
||||
});
|
||||
expanded.appendChild(metaPre);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(expanded);
|
||||
});
|
||||
|
||||
logList.innerHTML = '';
|
||||
logList.appendChild(fragment);
|
||||
|
||||
logList.querySelectorAll('.log-entry').forEach(function (row) {
|
||||
row.addEventListener('click', async function () {
|
||||
var id = this.getAttribute('data-id');
|
||||
state.openId = state.openId === id ? null : id;
|
||||
renderLogs();
|
||||
if (state.openId) {
|
||||
await loadLogDetail(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function queryParams() {
|
||||
var params = {
|
||||
period: state.period,
|
||||
limit: 100,
|
||||
};
|
||||
if (state.level !== 'all') {
|
||||
params.level = state.level;
|
||||
}
|
||||
if (state.search) {
|
||||
params.search = state.search;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (!window.CrankApi) {
|
||||
state.loadError = tKey('logs.error.api');
|
||||
renderLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.loadError = tKey('logs.error.workspace');
|
||||
renderLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.loadError = '';
|
||||
renderLogs();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listLogs(state.workspaceId, queryParams());
|
||||
state.logs = (response && response.items ? response.items : []).map(normalizeLog);
|
||||
if (state.openId && !state.logs.some(function (item) { return item.id === state.openId; })) {
|
||||
state.openId = null;
|
||||
}
|
||||
} catch (error) {
|
||||
state.loadError = error.message || tKey('logs.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
renderLogs();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogDetail(logId) {
|
||||
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var record = await window.CrankApi.getLog(state.workspaceId, logId);
|
||||
state.details[logId] = normalizeLog(record);
|
||||
if (state.openId === logId) {
|
||||
renderLogs();
|
||||
}
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
|
||||
function setLiveState() {
|
||||
if (liveDot) {
|
||||
liveDot.classList.toggle('is-paused', !state.liveMode);
|
||||
}
|
||||
if (liveLabel) {
|
||||
liveLabel.textContent = state.liveMode ? tKey('logs.live') : tKey('logs.paused');
|
||||
liveLabel.classList.toggle('is-paused', !state.liveMode);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (state.timer) {
|
||||
clearInterval(state.timer);
|
||||
state.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
if (!state.liveMode) {
|
||||
return;
|
||||
}
|
||||
state.timer = setInterval(loadLogs, 4000);
|
||||
}
|
||||
|
||||
function toggleLive() {
|
||||
state.liveMode = !state.liveMode;
|
||||
setLiveState();
|
||||
startPolling();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(
|
||||
state.liveMode ? tKey('logs.live.on.body') : tKey('logs.live.off.body'),
|
||||
state.liveMode ? tKey('logs.live.on.title') : tKey('logs.live.off.title')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
state.level = this.getAttribute('data-level');
|
||||
document.querySelectorAll('.filter-chip[data-level]').forEach(function (item) {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
loadLogs();
|
||||
});
|
||||
});
|
||||
|
||||
if (logSearch) {
|
||||
logSearch.addEventListener('input', function () {
|
||||
state.search = this.value.trim();
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function () {
|
||||
loadLogs().then(function () {
|
||||
if (!state.loadError && window.CrankUi) {
|
||||
window.CrankUi.info(tKey('logs.refresh.body'), tKey('logs.refresh.title'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (timeRangeSel) {
|
||||
timeRangeSel.value = state.period;
|
||||
timeRangeSel.addEventListener('change', function () {
|
||||
state.period = this.value;
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
if (liveDot) {
|
||||
liveDot.addEventListener('click', toggleLive);
|
||||
}
|
||||
|
||||
if (liveLabel) {
|
||||
liveLabel.addEventListener('click', toggleLive);
|
||||
}
|
||||
|
||||
window.addEventListener('crank:workspacechange', function () {
|
||||
state.details = {};
|
||||
state.openId = null;
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
setLiveState();
|
||||
startPolling();
|
||||
|
||||
if (window.whenWorkspacesReady) {
|
||||
window.whenWorkspacesReady().finally(loadLogs);
|
||||
} else {
|
||||
loadLogs();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
(function () {
|
||||
function initNav() {
|
||||
if (window.CrankAuth && typeof window.CrankAuth.renderShellIdentity === 'function') {
|
||||
window.CrankAuth.renderShellIdentity();
|
||||
}
|
||||
|
||||
var dropdown = document.querySelector('.user-dropdown');
|
||||
var avatar = document.querySelector('.nav-avatar');
|
||||
var hamburger = document.querySelector('.nav-hamburger');
|
||||
var mobileNav = document.querySelector('.mobile-nav');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.hidden = true;
|
||||
}
|
||||
if (mobileNav) {
|
||||
mobileNav.hidden = true;
|
||||
}
|
||||
|
||||
if (avatar && dropdown) {
|
||||
avatar.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
dropdown.hidden = !dropdown.hidden;
|
||||
});
|
||||
}
|
||||
|
||||
if (hamburger && mobileNav) {
|
||||
hamburger.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
mobileNav.hidden = !mobileNav.hidden;
|
||||
hamburger.classList.toggle('open', !mobileNav.hidden);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
if (dropdown && !event.target.closest('.user-menu')) {
|
||||
dropdown.hidden = true;
|
||||
}
|
||||
if (mobileNav && !event.target.closest('.navbar') && !event.target.closest('.mobile-nav')) {
|
||||
mobileNav.hidden = true;
|
||||
if (hamburger) {
|
||||
hamburger.classList.remove('open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-action="logout"]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
window.CrankAuth.logout();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-action="profile"]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
if (dropdown) {
|
||||
dropdown.hidden = true;
|
||||
}
|
||||
window.location.href = ((window.CrankRoutes && window.CrankRoutes.settings) || '/settings') + '#profile';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-action="settings-link"]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
if (dropdown) {
|
||||
dropdown.hidden = true;
|
||||
}
|
||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.settings) || '/settings';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initNav);
|
||||
} else {
|
||||
initNav();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,75 @@
|
||||
(function() {
|
||||
var manifestPromise = null;
|
||||
var loadedScripts = Object.create(null);
|
||||
|
||||
function overlayBasePath() {
|
||||
return (window.APP_BASE || '/') + 'overlay/';
|
||||
}
|
||||
|
||||
function fetchManifest() {
|
||||
return fetch(overlayBasePath() + 'manifest.json', { cache: 'no-store' })
|
||||
.then(function(response) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('overlay manifest request failed');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(function() {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScript(path) {
|
||||
if (loadedScripts[path]) {
|
||||
return loadedScripts[path];
|
||||
}
|
||||
loadedScripts[path] = new Promise(function(resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = overlayBasePath() + path.replace(/^\/+/, '');
|
||||
script.async = false;
|
||||
script.onload = function() { resolve(); };
|
||||
script.onerror = function() { reject(new Error('overlay script load failed')); };
|
||||
document.head.appendChild(script);
|
||||
}).catch(function() {
|
||||
return null;
|
||||
});
|
||||
return loadedScripts[path];
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (manifestPromise) {
|
||||
return manifestPromise;
|
||||
}
|
||||
manifestPromise = fetchManifest().then(function(manifest) {
|
||||
if (!(manifest && manifest.slots && window.CrankUiSlots)) {
|
||||
return null;
|
||||
}
|
||||
var tasks = Object.entries(manifest.slots)
|
||||
.filter(function(entry) {
|
||||
return window.CrankUiSlots.isAllowed(entry[0]) && typeof entry[1] === 'string';
|
||||
})
|
||||
.map(function(entry) {
|
||||
return loadScript(entry[1]);
|
||||
});
|
||||
return Promise.all(tasks).then(function() {
|
||||
return manifest;
|
||||
});
|
||||
});
|
||||
return manifestPromise;
|
||||
}
|
||||
|
||||
async function render(root, context) {
|
||||
await load();
|
||||
if (window.CrankUiSlots) {
|
||||
window.CrankUiSlots.renderAll(root, context);
|
||||
}
|
||||
}
|
||||
|
||||
window.CrankOverlay = {
|
||||
load: load,
|
||||
render: render,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,707 @@
|
||||
function initSecretsPage() {
|
||||
var state = {
|
||||
workspaceId: null,
|
||||
secrets: [],
|
||||
profiles: [],
|
||||
derived: {
|
||||
activeSecretCount: 0,
|
||||
secretsById: {},
|
||||
usageBySecret: {},
|
||||
},
|
||||
search: '',
|
||||
loading: false,
|
||||
error: '',
|
||||
modalMode: 'create',
|
||||
modalSecretId: null,
|
||||
};
|
||||
|
||||
var modal = document.getElementById('secret-modal');
|
||||
var tbody = document.getElementById('secrets-tbody');
|
||||
var cardList = document.getElementById('secrets-card-list');
|
||||
var profilesList = document.getElementById('auth-profiles-list');
|
||||
var summary = document.getElementById('secrets-summary');
|
||||
var profilesSummary = document.getElementById('secret-profiles-summary');
|
||||
var searchInput = document.getElementById('secret-search');
|
||||
var modalTitle = document.getElementById('secret-modal-title');
|
||||
var modalName = document.getElementById('secret-name');
|
||||
var modalKind = document.getElementById('secret-kind');
|
||||
var modalNote = document.getElementById('secret-modal-note');
|
||||
var modalSubmit = document.getElementById('secret-modal-submit-btn');
|
||||
var kindHint = document.getElementById('secret-kind-hint');
|
||||
var stringLabel = document.getElementById('secret-string-label');
|
||||
var stringField = document.getElementById('secret-string-value');
|
||||
var usernameField = document.getElementById('secret-username');
|
||||
var passwordField = document.getElementById('secret-password');
|
||||
var jsonField = document.getElementById('secret-json-value');
|
||||
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : tKey(key);
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function currentWorkspace() {
|
||||
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
}
|
||||
|
||||
function secretKindLabel(kind) {
|
||||
return tKey('secrets.kind.' + String(kind || '').toLowerCase());
|
||||
}
|
||||
|
||||
function authKindLabel(kind) {
|
||||
return tKey('secrets.auth_kind.' + String(kind || '').toLowerCase());
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
function secretIdsForProfile(profile) {
|
||||
var config = profile && profile.config ? profile.config : {};
|
||||
if (config.bearer) return [config.bearer.secret_id];
|
||||
if (config.basic) return [config.basic.username_secret_id, config.basic.password_secret_id];
|
||||
if (config.api_key_header) return [config.api_key_header.secret_id];
|
||||
if (config.api_key_query) return [config.api_key_query.secret_id];
|
||||
return [];
|
||||
}
|
||||
|
||||
function profileSummary(profile, secretsById) {
|
||||
var config = profile && profile.config ? profile.config : {};
|
||||
if (config.bearer) {
|
||||
return tfKey('secrets.profiles.summary_bearer', {
|
||||
header: config.bearer.header_name,
|
||||
secret: secretName(secretsById, config.bearer.secret_id),
|
||||
});
|
||||
}
|
||||
if (config.basic) {
|
||||
return tfKey('secrets.profiles.summary_basic', {
|
||||
username: secretName(secretsById, config.basic.username_secret_id),
|
||||
password: secretName(secretsById, config.basic.password_secret_id),
|
||||
});
|
||||
}
|
||||
if (config.api_key_header) {
|
||||
return tfKey('secrets.profiles.summary_api_key_header', {
|
||||
header: config.api_key_header.header_name,
|
||||
secret: secretName(secretsById, config.api_key_header.secret_id),
|
||||
});
|
||||
}
|
||||
if (config.api_key_query) {
|
||||
return tfKey('secrets.profiles.summary_api_key_query', {
|
||||
param: config.api_key_query.param_name,
|
||||
secret: secretName(secretsById, config.api_key_query.secret_id),
|
||||
});
|
||||
}
|
||||
return tKey('secrets.profiles.summary_unknown');
|
||||
}
|
||||
|
||||
function secretName(secretsById, secretId) {
|
||||
var secret = secretsById[secretId];
|
||||
return secret ? secret.name : secretId;
|
||||
}
|
||||
|
||||
function buildUsageBySecret() {
|
||||
var usage = {};
|
||||
state.profiles.forEach(function (profile) {
|
||||
secretIdsForProfile(profile).forEach(function (secretId) {
|
||||
if (!secretId) return;
|
||||
if (!usage[secretId]) usage[secretId] = [];
|
||||
usage[secretId].push(profile);
|
||||
});
|
||||
});
|
||||
return usage;
|
||||
}
|
||||
|
||||
function recomputeDerivedState() {
|
||||
var secretsById = {};
|
||||
var activeSecretCount = 0;
|
||||
|
||||
state.secrets.forEach(function (secret) {
|
||||
secretsById[secret.id] = secret;
|
||||
if (secret.status === 'active') {
|
||||
activeSecretCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
state.derived = {
|
||||
activeSecretCount: activeSecretCount,
|
||||
secretsById: secretsById,
|
||||
usageBySecret: buildUsageBySecret(),
|
||||
};
|
||||
}
|
||||
|
||||
function resetModalFields() {
|
||||
modalName.value = '';
|
||||
modalKind.value = 'token';
|
||||
stringField.value = '';
|
||||
usernameField.value = '';
|
||||
passwordField.value = '';
|
||||
jsonField.value = '{\n "value": ""\n}';
|
||||
}
|
||||
|
||||
function openModal(mode, secret) {
|
||||
state.modalMode = mode;
|
||||
state.modalSecretId = secret ? secret.id : null;
|
||||
resetModalFields();
|
||||
|
||||
if (mode === 'rotate' && secret) {
|
||||
modalTitle.textContent = tKey('secrets.modal.rotate_title');
|
||||
modalName.value = secret.name;
|
||||
modalName.disabled = true;
|
||||
modalKind.value = secret.kind;
|
||||
modalKind.disabled = true;
|
||||
modalSubmit.textContent = tKey('secrets.modal.rotate_action');
|
||||
modalNote.textContent = tKey('secrets.modal.rotate_note');
|
||||
} else {
|
||||
modalTitle.textContent = tKey('secrets.modal.create_title');
|
||||
modalName.disabled = false;
|
||||
modalKind.disabled = false;
|
||||
modalSubmit.textContent = tKey('secrets.modal.create_action');
|
||||
modalNote.textContent = tKey('secrets.modal.create_note');
|
||||
}
|
||||
|
||||
updateKindFields();
|
||||
modal.classList.add('open');
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureTestId === 'function') {
|
||||
window.CrankDiagnostics.ensureTestId(modal, mode === 'rotate' ? 'secret-rotate-modal' : 'secret-create-modal');
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (mode === 'rotate') {
|
||||
stringField.focus();
|
||||
} else {
|
||||
modalName.focus();
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove('open');
|
||||
state.modalMode = 'create';
|
||||
state.modalSecretId = null;
|
||||
}
|
||||
|
||||
function updateKindFields() {
|
||||
var kind = modalKind.value;
|
||||
document.querySelectorAll('[data-kind-field="string"]').forEach(function (element) {
|
||||
element.hidden = !(kind === 'token' || kind === 'header');
|
||||
});
|
||||
document.querySelectorAll('[data-kind-field="basic"]').forEach(function (element) {
|
||||
element.hidden = kind !== 'username_password';
|
||||
});
|
||||
document.querySelectorAll('[data-kind-field="json"]').forEach(function (element) {
|
||||
element.hidden = kind !== 'generic';
|
||||
});
|
||||
|
||||
if (kind === 'token') {
|
||||
stringLabel.textContent = tKey('secrets.modal.value');
|
||||
} else if (kind === 'header') {
|
||||
stringLabel.textContent = tKey('secrets.modal.header_value');
|
||||
} else {
|
||||
stringLabel.textContent = tKey('secrets.modal.value');
|
||||
}
|
||||
kindHint.textContent = tKey('secrets.hint.' + kind);
|
||||
}
|
||||
|
||||
function buildSecretValue() {
|
||||
var kind = modalKind.value;
|
||||
if (kind === 'token' || kind === 'header') {
|
||||
var value = stringField.value.trim();
|
||||
if (!value) {
|
||||
throw new Error(tKey('secrets.validation.value_required'));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (kind === 'username_password') {
|
||||
var username = usernameField.value.trim();
|
||||
var password = passwordField.value;
|
||||
if (!username || !password) {
|
||||
throw new Error(tKey('secrets.validation.username_password_required'));
|
||||
}
|
||||
return {
|
||||
username: username,
|
||||
password: password,
|
||||
};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(jsonField.value);
|
||||
} catch (_error) {
|
||||
throw new Error(tKey('secrets.validation.json_invalid'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderSecrets() {
|
||||
var usage = state.derived.usageBySecret;
|
||||
var query = state.search.toLowerCase();
|
||||
var rows = state.secrets.filter(function (secret) {
|
||||
return !query || secret.name.toLowerCase().includes(query) || String(secret.kind || '').toLowerCase().includes(query);
|
||||
});
|
||||
|
||||
summary.textContent = tfKey('secrets.active.subtitle', {
|
||||
active: state.derived.activeSecretCount,
|
||||
total: state.secrets.length,
|
||||
});
|
||||
|
||||
window.CrankDom.clear(tbody);
|
||||
if (cardList) {
|
||||
window.CrankDom.clear(cardList);
|
||||
}
|
||||
|
||||
if (state.loading && state.secrets.length === 0) {
|
||||
var loadingRow = document.createElement('tr');
|
||||
var loadingCell = document.createElement('td');
|
||||
loadingCell.colSpan = 7;
|
||||
loadingCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
||||
loadingCell.textContent = tKey('secrets.loading');
|
||||
loadingRow.appendChild(loadingCell);
|
||||
tbody.appendChild(loadingRow);
|
||||
renderSecretCards([], tKey('secrets.loading'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
var errorRow = document.createElement('tr');
|
||||
var errorCell = document.createElement('td');
|
||||
errorCell.colSpan = 7;
|
||||
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
|
||||
errorCell.textContent = state.error;
|
||||
errorRow.appendChild(errorCell);
|
||||
tbody.appendChild(errorRow);
|
||||
renderSecretCards([], state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
var emptyRow = document.createElement('tr');
|
||||
var emptyCell = document.createElement('td');
|
||||
emptyCell.colSpan = 7;
|
||||
emptyCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
|
||||
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
|
||||
emptyRow.appendChild(emptyCell);
|
||||
tbody.appendChild(emptyRow);
|
||||
renderSecretCards(rows);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach(function (secret) {
|
||||
var tr = document.createElement('tr');
|
||||
var references = usage[secret.id] || [];
|
||||
|
||||
var nameCell = document.createElement('td');
|
||||
nameCell.className = 'col-name';
|
||||
nameCell.textContent = secret.name;
|
||||
tr.appendChild(nameCell);
|
||||
|
||||
var kindCell = document.createElement('td');
|
||||
kindCell.textContent = secretKindLabel(secret.kind);
|
||||
tr.appendChild(kindCell);
|
||||
|
||||
var versionCell = document.createElement('td');
|
||||
versionCell.className = 'col-mono';
|
||||
versionCell.textContent = 'v' + secret.current_version;
|
||||
tr.appendChild(versionCell);
|
||||
|
||||
var lastUsedCell = document.createElement('td');
|
||||
lastUsedCell.textContent = secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never');
|
||||
tr.appendChild(lastUsedCell);
|
||||
|
||||
var usedByCell = document.createElement('td');
|
||||
usedByCell.textContent = window.tPlural('secrets.used_by_count', references.length, { count: references.length });
|
||||
tr.appendChild(usedByCell);
|
||||
|
||||
var statusCell = document.createElement('td');
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
|
||||
badge.textContent = tKey('secrets.status.' + secret.status);
|
||||
statusCell.appendChild(badge);
|
||||
tr.appendChild(statusCell);
|
||||
|
||||
var actions = document.createElement('td');
|
||||
actions.className = 'col-actions';
|
||||
var rotateButton = document.createElement('button');
|
||||
rotateButton.className = 'icon-btn';
|
||||
rotateButton.textContent = tKey('secrets.action.rotate');
|
||||
rotateButton.setAttribute('data-testid', 'secret-rotate-action');
|
||||
rotateButton.setAttribute('data-secret-id', secret.id);
|
||||
rotateButton.addEventListener('click', function () {
|
||||
openModal('rotate', secret);
|
||||
});
|
||||
actions.appendChild(rotateButton);
|
||||
|
||||
var deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'icon-btn danger';
|
||||
deleteButton.textContent = tKey('secrets.action.delete');
|
||||
deleteButton.setAttribute('data-testid', 'secret-delete-action');
|
||||
deleteButton.setAttribute('data-secret-id', secret.id);
|
||||
deleteButton.addEventListener('click', async function () {
|
||||
await deleteSecret(secret);
|
||||
});
|
||||
actions.appendChild(deleteButton);
|
||||
tr.appendChild(actions);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
renderSecretCards(rows);
|
||||
}
|
||||
|
||||
function renderSecretCards(rows, statusText) {
|
||||
if (!cardList) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.CrankDom.clear(cardList);
|
||||
|
||||
if (statusText && !rows.length) {
|
||||
var messageCard = document.createElement('div');
|
||||
messageCard.className = 'resource-card';
|
||||
var message = document.createElement('div');
|
||||
message.className = 'resource-meta-value';
|
||||
message.textContent = statusText;
|
||||
message.style.color = state.error ? 'var(--danger,#f85149)' : 'var(--text-muted)';
|
||||
messageCard.appendChild(message);
|
||||
cardList.appendChild(messageCard);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach(function(secret) {
|
||||
var references = state.derived.usageBySecret[secret.id] || [];
|
||||
var card = document.createElement('div');
|
||||
card.className = 'resource-card';
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'resource-card-header';
|
||||
var headerBody = document.createElement('div');
|
||||
var title = document.createElement('div');
|
||||
title.className = 'resource-card-title';
|
||||
title.textContent = secret.name;
|
||||
var subtitle = document.createElement('div');
|
||||
subtitle.className = 'resource-card-subtitle';
|
||||
subtitle.textContent = secretKindLabel(secret.kind);
|
||||
headerBody.appendChild(title);
|
||||
headerBody.appendChild(subtitle);
|
||||
|
||||
var actions = document.createElement('div');
|
||||
actions.className = 'resource-card-actions';
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
|
||||
badge.textContent = tKey('secrets.status.' + secret.status);
|
||||
actions.appendChild(badge);
|
||||
header.appendChild(headerBody);
|
||||
header.appendChild(actions);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = document.createElement('div');
|
||||
metaGrid.className = 'resource-meta-grid';
|
||||
metaGrid.appendChild(buildSecretMetaItem('secrets.th.version', 'v' + secret.current_version));
|
||||
metaGrid.appendChild(buildSecretMetaItem(
|
||||
'secrets.th.last_used',
|
||||
secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')
|
||||
));
|
||||
metaGrid.appendChild(buildSecretMetaItem(
|
||||
'secrets.th.used_by',
|
||||
window.tPlural('secrets.used_by_count', references.length, { count: references.length })
|
||||
));
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
var actionRow = document.createElement('div');
|
||||
actionRow.className = 'resource-card-actions';
|
||||
var rotateButton = document.createElement('button');
|
||||
rotateButton.className = 'btn-secondary';
|
||||
rotateButton.type = 'button';
|
||||
rotateButton.textContent = tKey('secrets.action.rotate');
|
||||
rotateButton.addEventListener('click', function() {
|
||||
openModal('rotate', secret);
|
||||
});
|
||||
actionRow.appendChild(rotateButton);
|
||||
|
||||
var deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'btn-secondary';
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.textContent = tKey('secrets.action.delete');
|
||||
deleteButton.addEventListener('click', async function() {
|
||||
await deleteSecret(secret);
|
||||
});
|
||||
actionRow.appendChild(deleteButton);
|
||||
card.appendChild(actionRow);
|
||||
|
||||
cardList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function buildSecretMetaItem(labelKey, valueText) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'resource-meta-item';
|
||||
var label = document.createElement('div');
|
||||
label.className = 'resource-meta-label';
|
||||
label.textContent = tKey(labelKey);
|
||||
var value = document.createElement('div');
|
||||
value.className = 'resource-meta-value';
|
||||
value.textContent = valueText;
|
||||
item.appendChild(label);
|
||||
item.appendChild(value);
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderProfiles() {
|
||||
var usage = state.derived.usageBySecret;
|
||||
var secretsById = state.derived.secretsById;
|
||||
|
||||
profilesSummary.textContent = window.tPlural(
|
||||
'secrets.profiles.subtitle_count',
|
||||
state.profiles.length,
|
||||
{ count: state.profiles.length }
|
||||
);
|
||||
|
||||
window.CrankDom.clear(profilesList);
|
||||
|
||||
if (state.error) {
|
||||
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||
tKey('secrets.profiles.error_title'),
|
||||
state.error
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.loading && state.profiles.length === 0) {
|
||||
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||
tKey('secrets.profiles.loading_title'),
|
||||
tKey('secrets.profiles.loading_body')
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.profiles.length) {
|
||||
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||
tKey('secrets.profiles.empty_title'),
|
||||
tKey('secrets.profiles.empty_body')
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
state.profiles.forEach(function (profile) {
|
||||
var secretIds = secretIdsForProfile(profile);
|
||||
var card = document.createElement('div');
|
||||
card.className = 'resource-card';
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'resource-card-header';
|
||||
var headerBody = document.createElement('div');
|
||||
|
||||
var title = document.createElement('div');
|
||||
title.className = 'resource-card-title';
|
||||
title.textContent = profile.name;
|
||||
headerBody.appendChild(title);
|
||||
|
||||
var subtitle = document.createElement('div');
|
||||
subtitle.className = 'resource-card-subtitle';
|
||||
subtitle.textContent = profileSummary(profile, secretsById);
|
||||
headerBody.appendChild(subtitle);
|
||||
|
||||
var pillRow = document.createElement('div');
|
||||
pillRow.className = 'resource-pill-row';
|
||||
var statusPill = document.createElement('span');
|
||||
statusPill.className = 'resource-status-pill active';
|
||||
statusPill.textContent = authKindLabel(profile.kind);
|
||||
pillRow.appendChild(statusPill);
|
||||
headerBody.appendChild(pillRow);
|
||||
header.appendChild(headerBody);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = document.createElement('div');
|
||||
metaGrid.className = 'resource-meta-grid';
|
||||
[
|
||||
[tKey('secrets.profiles.meta.created'), formatDate(profile.created_at)],
|
||||
[tKey('secrets.profiles.meta.updated'), formatDate(profile.updated_at)]
|
||||
].forEach(function(entry) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'resource-meta-item';
|
||||
var label = document.createElement('div');
|
||||
label.className = 'resource-meta-label';
|
||||
label.textContent = entry[0];
|
||||
var value = document.createElement('div');
|
||||
value.className = 'resource-meta-value';
|
||||
value.textContent = entry[1];
|
||||
item.appendChild(label);
|
||||
item.appendChild(value);
|
||||
metaGrid.appendChild(item);
|
||||
});
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
var detail = document.createElement('div');
|
||||
detail.className = 'resource-detail-block';
|
||||
var detailTitle = document.createElement('div');
|
||||
detailTitle.className = 'resource-detail-title';
|
||||
detailTitle.textContent = tKey('secrets.profiles.references');
|
||||
detail.appendChild(detailTitle);
|
||||
|
||||
var refList = document.createElement('div');
|
||||
refList.className = 'secret-ref-list';
|
||||
secretIds.forEach(function(secretId) {
|
||||
var users = usage[secretId] || [];
|
||||
var pill = document.createElement('span');
|
||||
pill.className = 'secret-ref-pill';
|
||||
var strong = document.createElement('strong');
|
||||
strong.textContent = secretName(secretsById, secretId);
|
||||
var count = document.createElement('span');
|
||||
count.textContent = tfKey('secrets.profiles.reference_count', { count: users.length });
|
||||
pill.appendChild(strong);
|
||||
pill.appendChild(count);
|
||||
refList.appendChild(pill);
|
||||
});
|
||||
detail.appendChild(refList);
|
||||
card.appendChild(detail);
|
||||
|
||||
profilesList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
state.loading = true;
|
||||
state.error = '';
|
||||
renderSecrets();
|
||||
renderProfiles();
|
||||
|
||||
if (!state.workspaceId) {
|
||||
state.secrets = [];
|
||||
state.profiles = [];
|
||||
recomputeDerivedState();
|
||||
state.loading = false;
|
||||
state.error = tKey('secrets.error.workspace');
|
||||
renderSecrets();
|
||||
renderProfiles();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var results = await Promise.all([
|
||||
window.CrankApi.listSecrets(state.workspaceId),
|
||||
window.CrankApi.listAuthProfiles(state.workspaceId),
|
||||
]);
|
||||
state.secrets = (results[0] && results[0].items) || [];
|
||||
state.profiles = (results[1] && results[1].items) || [];
|
||||
recomputeDerivedState();
|
||||
} catch (error) {
|
||||
state.error = error.message || tKey('secrets.error.load');
|
||||
state.secrets = [];
|
||||
state.profiles = [];
|
||||
recomputeDerivedState();
|
||||
} finally {
|
||||
state.loading = false;
|
||||
renderSecrets();
|
||||
renderProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSecret(secret) {
|
||||
if (!confirm(tfKey('secrets.confirm.delete', { name: secret.name }))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await window.CrankApi.deleteSecret(state.workspaceId, secret.id);
|
||||
await load();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tfKey('secrets.toast.delete_message', { name: secret.name }),
|
||||
tKey('secrets.toast.delete_title')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('secrets.toast.delete_error_message'),
|
||||
tKey('secrets.toast.delete_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitModal() {
|
||||
var kind = modalKind.value;
|
||||
var value = buildSecretValue();
|
||||
modalSubmit.disabled = true;
|
||||
modalSubmit.textContent = state.modalMode === 'rotate'
|
||||
? tKey('secrets.modal.rotating')
|
||||
: tKey('secrets.modal.creating');
|
||||
|
||||
try {
|
||||
if (state.modalMode === 'rotate') {
|
||||
await window.CrankApi.rotateSecret(state.workspaceId, state.modalSecretId, { value: value });
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('secrets.toast.rotate_message'),
|
||||
tKey('secrets.toast.rotate_title')
|
||||
);
|
||||
}
|
||||
} else {
|
||||
var name = modalName.value.trim();
|
||||
if (!name) {
|
||||
throw new Error(tKey('secrets.validation.name_required'));
|
||||
}
|
||||
await window.CrankApi.createSecret(state.workspaceId, {
|
||||
name: name,
|
||||
kind: kind,
|
||||
value: value,
|
||||
});
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('secrets.toast.create_message'),
|
||||
tKey('secrets.toast.create_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
closeModal();
|
||||
await load();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_message' : 'secrets.toast.create_error_message'),
|
||||
tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_title' : 'secrets.toast.create_error_title')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
modalSubmit.disabled = false;
|
||||
modalSubmit.textContent = state.modalMode === 'rotate'
|
||||
? tKey('secrets.modal.rotate_action')
|
||||
: tKey('secrets.modal.create_action');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btn-create-secret').addEventListener('click', function () {
|
||||
openModal('create');
|
||||
});
|
||||
document.getElementById('secret-modal-close-btn').addEventListener('click', closeModal);
|
||||
document.getElementById('secret-modal-cancel-btn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function (event) {
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
modalKind.addEventListener('change', updateKindFields);
|
||||
modalSubmit.addEventListener('click', function () {
|
||||
void submitModal();
|
||||
});
|
||||
searchInput.addEventListener('input', function (event) {
|
||||
state.search = event.target.value || '';
|
||||
renderSecrets();
|
||||
});
|
||||
document.addEventListener('workspace:changed', function () {
|
||||
void load();
|
||||
});
|
||||
|
||||
updateKindFields();
|
||||
void load();
|
||||
}
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('secrets', initSecretsPage);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initSecretsPage);
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
var settingsSession = null;
|
||||
var settingsCapabilities = null;
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function settingsWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function initials(displayName, email) {
|
||||
var source = displayName || email || 'Crank';
|
||||
return source
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map(function(part) { return part.charAt(0).toUpperCase(); })
|
||||
.join('') || 'CR';
|
||||
}
|
||||
|
||||
function setStatus(id, text, isError) {
|
||||
var node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
node.textContent = text || '';
|
||||
node.style.color = text
|
||||
? (isError ? 'var(--red)' : 'var(--green)')
|
||||
: 'var(--text-muted)';
|
||||
}
|
||||
|
||||
function titleCaseRole(role) {
|
||||
var normalized = String(role || 'viewer').toLowerCase();
|
||||
if (normalized === 'operator') return tKey('workspace_setup.role.operator');
|
||||
if (normalized === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (normalized === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (normalized === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return normalized.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 = tKey('settings.session.unavailable');
|
||||
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)
|
||||
: tKey('settings.session.current_workspace');
|
||||
var roleLabel = membership ? titleCaseRole(membership.role) : tKey('workspace_setup.role.viewer');
|
||||
summary.textContent = tfKey('settings.session.summary', {
|
||||
email: session.user.email,
|
||||
role: roleLabel,
|
||||
workspace: workspaceLabel,
|
||||
});
|
||||
}
|
||||
|
||||
function capabilityLabel(key) {
|
||||
return tKey('settings.capability.' + key);
|
||||
}
|
||||
|
||||
function renderWorkspaceProtocolOptions(capabilities, selectedProtocol) {
|
||||
var select = document.getElementById('settings-ws-protocol');
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
var supportedProtocols = capabilities && Array.isArray(capabilities.supported_protocols) && capabilities.supported_protocols.length
|
||||
? capabilities.supported_protocols
|
||||
: ['rest'];
|
||||
var fallbackProtocol = supportedProtocols.indexOf('rest') !== -1 ? 'rest' : supportedProtocols[0];
|
||||
var effectiveSelection = supportedProtocols.indexOf(selectedProtocol) !== -1
|
||||
? selectedProtocol
|
||||
: fallbackProtocol;
|
||||
|
||||
select.innerHTML = '';
|
||||
supportedProtocols.forEach(function (protocol) {
|
||||
var option = document.createElement('option');
|
||||
option.value = protocol;
|
||||
option.textContent = capabilityLabel(protocol);
|
||||
option.selected = protocol === effectiveSelection;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateWorkspaceCapabilityHint(capabilities) {
|
||||
var body = document.getElementById('settings-ws-protocol-hint');
|
||||
if (!body || !capabilities) {
|
||||
return;
|
||||
}
|
||||
|
||||
var protocols = (capabilities.supported_protocols || []).map(capabilityLabel).join(', ');
|
||||
body.textContent = tfKey('settings.ws.protocol_hint_capabilities', {
|
||||
protocols: protocols || capabilityLabel('none'),
|
||||
});
|
||||
}
|
||||
|
||||
function populateNotificationsCapabilityHint(capabilities) {
|
||||
var title = document.getElementById('settings-notifications-capability-title');
|
||||
var body = document.getElementById('settings-notifications-capability-body');
|
||||
if (!title || !body || !capabilities) {
|
||||
return;
|
||||
}
|
||||
|
||||
title.textContent = tKey('settings.notifications.capability_title_' + capabilities.edition);
|
||||
body.textContent = tfKey('settings.notifications.capability_body_' + capabilities.edition, {
|
||||
edition: capabilities.edition,
|
||||
});
|
||||
}
|
||||
|
||||
function populateCapabilities(capabilities) {
|
||||
var title = document.getElementById('settings-security-capability-title');
|
||||
var body = document.getElementById('settings-security-capability-body');
|
||||
if (!title || !body || !capabilities) {
|
||||
return;
|
||||
}
|
||||
|
||||
var protocols = (capabilities.supported_protocols || []).map(capabilityLabel).join(', ');
|
||||
var accessModes = (capabilities.machine_access_modes || []).map(capabilityLabel).join(', ');
|
||||
var securityLevels = (capabilities.supported_security_levels || []).map(capabilityLabel).join(', ');
|
||||
var limits = capabilities.limits || {};
|
||||
|
||||
title.textContent = tKey('settings.security.capability_title_' + capabilities.edition);
|
||||
body.textContent = tfKey('settings.security.capability_summary', {
|
||||
protocols: protocols || capabilityLabel('none'),
|
||||
access_modes: accessModes || capabilityLabel('none'),
|
||||
security_levels: securityLevels || capabilityLabel('none'),
|
||||
max_workspaces: limits.max_workspaces == null ? capabilityLabel('unlimited') : String(limits.max_workspaces),
|
||||
max_users: limits.max_users_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_users_per_workspace),
|
||||
max_agents: limits.max_agents_per_workspace == null ? capabilityLabel('unlimited') : String(limits.max_agents_per_workspace),
|
||||
});
|
||||
populateWorkspaceCapabilityHint(capabilities);
|
||||
populateNotificationsCapabilityHint(capabilities);
|
||||
renderWorkspaceProtocolOptions(capabilities, document.getElementById('settings-ws-protocol').value || 'rest');
|
||||
}
|
||||
|
||||
function populateProfile(session) {
|
||||
if (!session || !session.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
var user = session.user;
|
||||
document.getElementById('profile-avatar').textContent = initials(user.display_name, user.email);
|
||||
document.getElementById('profile-display-name').textContent = user.display_name || 'Crank';
|
||||
document.getElementById('profile-email').textContent = user.email || '';
|
||||
|
||||
var firstName = document.getElementById('field-firstname');
|
||||
var email = document.getElementById('field-email');
|
||||
if (firstName) {
|
||||
firstName.value = user.display_name || '';
|
||||
}
|
||||
if (email) {
|
||||
email.value = user.email || '';
|
||||
}
|
||||
|
||||
populateCurrentSession(session);
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
if (!window.CrankAuth || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsSession = await window.CrankAuth.fetchSession(true);
|
||||
populateProfile(settingsSession);
|
||||
}
|
||||
|
||||
async function loadCapabilities() {
|
||||
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
settingsCapabilities = await window.CrankApi.getCapabilities();
|
||||
populateCapabilities(settingsCapabilities);
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderOverlaySlots() {
|
||||
if (!window.CrankOverlay || typeof window.CrankOverlay.render !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
await window.CrankOverlay.render(document, {
|
||||
workspace: window.getCurrentWorkspace ? window.getCurrentWorkspace() : null,
|
||||
capabilities: settingsCapabilities,
|
||||
locale: localStorage.getItem('crank_lang') || 'en',
|
||||
});
|
||||
}
|
||||
|
||||
function bindProfileSave() {
|
||||
var button = document.getElementById('settings-profile-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
button.dataset.bound = 'true';
|
||||
|
||||
button.addEventListener('click', async function() {
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = tKey('settings.profile.saving');
|
||||
setStatus('settings-profile-status', '', false);
|
||||
|
||||
try {
|
||||
var session = await window.CrankApi.updateProfile({
|
||||
display_name: document.getElementById('field-firstname').value.trim(),
|
||||
email: document.getElementById('field-email').value.trim(),
|
||||
});
|
||||
settingsSession = session;
|
||||
if (window.CrankAuth && typeof window.CrankAuth.replaceSession === 'function') {
|
||||
window.CrankAuth.replaceSession(session);
|
||||
}
|
||||
populateProfile(session);
|
||||
setStatus('settings-profile-status', tKey('settings.profile.saved_status'), false);
|
||||
button.textContent = tKey('settings.profile.saved');
|
||||
} catch (error) {
|
||||
setStatus('settings-profile-status', error.message || tKey('settings.profile.save_error'), true);
|
||||
button.textContent = original;
|
||||
} finally {
|
||||
setTimeout(function() {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindPasswordSave() {
|
||||
var button = document.getElementById('settings-password-save-btn');
|
||||
if (!button || button.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
button.dataset.bound = 'true';
|
||||
|
||||
button.addEventListener('click', async function() {
|
||||
var currentPassword = document.getElementById('security-current-password').value;
|
||||
var newPassword = document.getElementById('security-new-password').value;
|
||||
var confirmPassword = document.getElementById('security-confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setStatus('settings-password-status', tKey('settings.security.mismatch'), true);
|
||||
return;
|
||||
}
|
||||
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = tKey('settings.profile.saving');
|
||||
setStatus('settings-password-status', '', false);
|
||||
|
||||
try {
|
||||
await window.CrankApi.changePassword({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
document.getElementById('security-current-password').value = '';
|
||||
document.getElementById('security-new-password').value = '';
|
||||
document.getElementById('security-confirm-password').value = '';
|
||||
setStatus('settings-password-status', tKey('settings.security.saved'), false);
|
||||
button.textContent = tKey('settings.profile.saved');
|
||||
} catch (error) {
|
||||
setStatus('settings-password-status', error.message || tKey('settings.security.save_error'), true);
|
||||
button.textContent = original;
|
||||
} finally {
|
||||
setTimeout(function() {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function injectLanguageSwitcher() {
|
||||
var profileSection = document.getElementById('section-profile');
|
||||
if (!profileSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cardBody = profileSection.querySelector('.section-card-body');
|
||||
if (!cardBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('html/fragments/lang-switcher.html')
|
||||
.then(function(response) { return response.text(); })
|
||||
.then(function(html) {
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
var fragment = template.content.cloneNode(true);
|
||||
var saveRow = cardBody.querySelector('div[style*="justify-content:flex-end"]');
|
||||
if (saveRow) {
|
||||
saveRow.parentNode.insertBefore(fragment, saveRow);
|
||||
} else {
|
||||
cardBody.appendChild(fragment);
|
||||
}
|
||||
if (typeof applyLang === 'function') {
|
||||
applyLang();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindSectionNavigation() {
|
||||
var navItems = document.querySelectorAll('.settings-nav-item[data-section]');
|
||||
navItems.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
navItems.forEach(function (item) { item.classList.remove('active'); });
|
||||
this.classList.add('active');
|
||||
var target = document.getElementById('section-' + this.dataset.section);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var initialSection = window.location.hash ? window.location.hash.slice(1) : 'profile';
|
||||
var initialButton = document.querySelector('.settings-nav-item[data-section="' + initialSection + '"]')
|
||||
|| document.querySelector('.settings-nav-item[data-section="profile"]');
|
||||
if (initialButton) {
|
||||
setTimeout(function () { initialButton.click(); }, 50);
|
||||
}
|
||||
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
var section = entry.target.id.replace('section-', '');
|
||||
document.querySelectorAll('.settings-nav-item[data-section]').forEach(function (button) {
|
||||
button.classList.toggle('active', button.dataset.section === section);
|
||||
});
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
|
||||
document.querySelectorAll('[id^="section-"]').forEach(function (section) {
|
||||
if (!section.hidden) {
|
||||
observer.observe(section);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWorkspaceSettings() {
|
||||
if (!window.CrankApi || !window.whenWorkspacesReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.whenWorkspacesReady();
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var record = await window.CrankApi.getWorkspace(workspace.id);
|
||||
var item = record.workspace;
|
||||
var settings = item.settings || {};
|
||||
|
||||
document.getElementById('settings-ws-slug').value = item.slug || '';
|
||||
document.getElementById('settings-ws-display-name').value = item.display_name || '';
|
||||
document.getElementById('settings-ws-description').value = settings.description || '';
|
||||
document.getElementById('settings-ws-timezone').value = settings.timezone || 'UTC';
|
||||
renderWorkspaceProtocolOptions(settingsCapabilities, settings.default_protocol || 'rest');
|
||||
|
||||
document.querySelectorAll('#section-workspace .lang-btn').forEach(function (button) {
|
||||
button.classList.toggle('active', button.dataset.lang === (settings.language || (localStorage.getItem('crank_lang') || 'en')));
|
||||
});
|
||||
|
||||
var saveButton = document.getElementById('settings-ws-save-btn');
|
||||
if (saveButton.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
saveButton.dataset.bound = 'true';
|
||||
|
||||
saveButton.addEventListener('click', async function () {
|
||||
var saveButton = this;
|
||||
var original = saveButton.textContent;
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = tKey('settings.workspace.saving');
|
||||
|
||||
try {
|
||||
await window.CrankApi.updateWorkspace(item.id, {
|
||||
slug: document.getElementById('settings-ws-slug').value.trim(),
|
||||
display_name: document.getElementById('settings-ws-display-name').value.trim(),
|
||||
settings: {
|
||||
description: document.getElementById('settings-ws-description').value.trim(),
|
||||
timezone: document.getElementById('settings-ws-timezone').value,
|
||||
default_protocol: document.getElementById('settings-ws-protocol').value,
|
||||
language: localStorage.getItem('crank_lang') || 'en',
|
||||
color: (workspace.settings && workspace.settings.color) || '#0d9488',
|
||||
},
|
||||
});
|
||||
|
||||
if (window.refreshWorkspaces) {
|
||||
var workspaces = await window.refreshWorkspaces();
|
||||
var updated = workspaces.find(function (entry) { return entry.id === item.id; });
|
||||
if (updated && window.setCurrentWorkspace) {
|
||||
await window.setCurrentWorkspace(updated);
|
||||
}
|
||||
}
|
||||
|
||||
saveButton.textContent = tKey('settings.workspace.saved');
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('settings.workspace.save_error'), tKey('settings.workspace.save_error_title'));
|
||||
}
|
||||
saveButton.textContent = original;
|
||||
} finally {
|
||||
setTimeout(function () {
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = original;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initSettingsPage() {
|
||||
injectLanguageSwitcher();
|
||||
bindSectionNavigation();
|
||||
await loadProfile();
|
||||
await loadCapabilities();
|
||||
bindProfileSave();
|
||||
bindPasswordSave();
|
||||
await loadWorkspaceSettings();
|
||||
await renderOverlaySlots();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
void initSettingsPage();
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
(function() {
|
||||
var allowedSlots = {
|
||||
'settings.sso_panel': true,
|
||||
'settings.totp_panel': true,
|
||||
'settings.audit_panel': true,
|
||||
'settings.billing_panel': true,
|
||||
'wizard.protocol_cards.graphql': true,
|
||||
'wizard.protocol_cards.grpc': true,
|
||||
'wizard.protocol_cards.soap': true,
|
||||
'wizard.protocol_cards.websocket': true,
|
||||
};
|
||||
|
||||
var handlers = Object.create(null);
|
||||
|
||||
function noop() {}
|
||||
|
||||
function isAllowed(slotId) {
|
||||
return !!allowedSlots[slotId];
|
||||
}
|
||||
|
||||
function register(slotId, render) {
|
||||
if (!isAllowed(slotId) || typeof render !== 'function') {
|
||||
return false;
|
||||
}
|
||||
handlers[slotId] = render;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderInto(target, slotId, context) {
|
||||
if (!(target && slotId && isAllowed(slotId))) {
|
||||
return;
|
||||
}
|
||||
var handler = handlers[slotId] || noop;
|
||||
handler(target, context || {});
|
||||
}
|
||||
|
||||
function renderAll(root, context) {
|
||||
var scope = root || document;
|
||||
scope.querySelectorAll('[data-slot]').forEach(function(target) {
|
||||
renderInto(target, target.getAttribute('data-slot'), context);
|
||||
});
|
||||
}
|
||||
|
||||
function knownSlots() {
|
||||
return Object.keys(allowedSlots);
|
||||
}
|
||||
|
||||
window.CrankUiSlots = {
|
||||
isAllowed: isAllowed,
|
||||
knownSlots: knownSlots,
|
||||
register: register,
|
||||
renderAll: renderAll,
|
||||
renderInto: renderInto,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,105 @@
|
||||
(function() {
|
||||
var CONTAINER_ID = 'crank-toast-container';
|
||||
var nextToastId = 0;
|
||||
|
||||
function ensureContainer() {
|
||||
var container = document.getElementById(CONTAINER_ID);
|
||||
if (container) return container;
|
||||
|
||||
container = document.createElement('div');
|
||||
container.id = CONTAINER_ID;
|
||||
container.className = 'toast-stack';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
function dismiss(toast) {
|
||||
if (!toast || !toast.parentNode) return;
|
||||
toast.classList.add('closing');
|
||||
window.setTimeout(function() {
|
||||
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||
}, 180);
|
||||
}
|
||||
|
||||
function notify(options) {
|
||||
var config = options || {};
|
||||
var duration = config.duration === 0 ? 0 : (config.duration || 3600);
|
||||
var toast = document.createElement('div');
|
||||
var copy = document.createElement('div');
|
||||
var close = document.createElement('button');
|
||||
var closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
var lineA = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
var lineB = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
|
||||
toast.id = 'toast_' + (++nextToastId);
|
||||
toast.className = 'toast-card toast-' + (config.type || 'info');
|
||||
copy.className = 'toast-copy';
|
||||
if (config.title) {
|
||||
var title = document.createElement('div');
|
||||
title.className = 'toast-title';
|
||||
title.textContent = config.title;
|
||||
copy.appendChild(title);
|
||||
}
|
||||
if (config.message) {
|
||||
var message = document.createElement('div');
|
||||
message.className = 'toast-message';
|
||||
message.textContent = config.message;
|
||||
copy.appendChild(message);
|
||||
}
|
||||
|
||||
close.className = 'toast-close';
|
||||
close.type = 'button';
|
||||
close.setAttribute('aria-label', 'Dismiss');
|
||||
|
||||
closeIcon.setAttribute('width', '12');
|
||||
closeIcon.setAttribute('height', '12');
|
||||
closeIcon.setAttribute('viewBox', '0 0 12 12');
|
||||
closeIcon.setAttribute('fill', 'none');
|
||||
closeIcon.setAttribute('stroke', 'currentColor');
|
||||
closeIcon.setAttribute('stroke-width', '1.8');
|
||||
closeIcon.setAttribute('stroke-linecap', 'round');
|
||||
|
||||
lineA.setAttribute('x1', '1');
|
||||
lineA.setAttribute('y1', '1');
|
||||
lineA.setAttribute('x2', '11');
|
||||
lineA.setAttribute('y2', '11');
|
||||
lineB.setAttribute('x1', '11');
|
||||
lineB.setAttribute('y1', '1');
|
||||
lineB.setAttribute('x2', '1');
|
||||
lineB.setAttribute('y2', '11');
|
||||
|
||||
closeIcon.appendChild(lineA);
|
||||
closeIcon.appendChild(lineB);
|
||||
close.appendChild(closeIcon);
|
||||
|
||||
toast.appendChild(copy);
|
||||
toast.appendChild(close);
|
||||
|
||||
close.addEventListener('click', function() {
|
||||
dismiss(toast);
|
||||
});
|
||||
|
||||
ensureContainer().appendChild(toast);
|
||||
|
||||
if (duration > 0) {
|
||||
window.setTimeout(function() {
|
||||
dismiss(toast);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return toast.id;
|
||||
}
|
||||
|
||||
window.CrankUi = {
|
||||
notify: notify,
|
||||
success: function(message, title) {
|
||||
return notify({ type: 'success', title: title || 'Done', message: message });
|
||||
},
|
||||
error: function(message, title) {
|
||||
return notify({ type: 'error', title: title || 'Request failed', message: message });
|
||||
},
|
||||
info: function(message, title) {
|
||||
return notify({ type: 'info', title: title || 'Info', message: message });
|
||||
}
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,549 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
period: '7d',
|
||||
workspaceId: null,
|
||||
usage: null,
|
||||
derived: {
|
||||
localizedOperations: [],
|
||||
normalizedTimeline: [],
|
||||
timelineMaxValue: 0,
|
||||
totalCalls: 0,
|
||||
},
|
||||
loading: false,
|
||||
loadError: '',
|
||||
};
|
||||
|
||||
var periodSelect = document.getElementById('period');
|
||||
var exportBtn = document.querySelector('.page-header-actions .btn-secondary');
|
||||
var chartWrap = document.getElementById('chart-bars');
|
||||
var tableBody = document.getElementById('usage-tbody');
|
||||
var cardList = document.getElementById('usage-card-list');
|
||||
var chartTemplate = document.getElementById('tmpl-chart-bar');
|
||||
var rowTemplate = document.getElementById('tmpl-usage-row');
|
||||
var subtitle = document.querySelector('.section-card-subtitle');
|
||||
var statCards = document.querySelectorAll('.stats-grid .stat-card');
|
||||
|
||||
function tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
}
|
||||
|
||||
function tfKey(key, vars) {
|
||||
return window.tf ? tf(key, vars) : tKey(key);
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function element(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
return Number(value || 0).toLocaleString(currentLocale());
|
||||
}
|
||||
|
||||
function formatMs(value) {
|
||||
if (!value) {
|
||||
return '0ms';
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 's';
|
||||
}
|
||||
return value + 'ms';
|
||||
}
|
||||
|
||||
function periodLabel(period) {
|
||||
return tKey('usage.period.label.' + period);
|
||||
}
|
||||
|
||||
function protocolColor(protocol) {
|
||||
return 'var(--blue)';
|
||||
}
|
||||
|
||||
function protocolLabel(protocol) {
|
||||
return 'REST';
|
||||
}
|
||||
|
||||
function localizedUsageOperation(operation) {
|
||||
if (!window.localizeDemoOperation) {
|
||||
return operation;
|
||||
}
|
||||
|
||||
return window.localizeDemoOperation({
|
||||
name: operation.operation_name,
|
||||
operation_display_name: operation.operation_display_name,
|
||||
description: operation.operation_description || null,
|
||||
});
|
||||
}
|
||||
|
||||
function chartBucketLabel(timestamp, period, index) {
|
||||
var date = new Date(timestamp);
|
||||
if (period === '7d') {
|
||||
return date.toLocaleDateString(currentLocale(), { weekday: 'short' });
|
||||
}
|
||||
if (period === '90d') {
|
||||
return date.toLocaleDateString(currentLocale(), { month: 'short' });
|
||||
}
|
||||
return tfKey('usage.chart.week', { index: index + 1 });
|
||||
}
|
||||
|
||||
function toUtcBucketStart(date, period) {
|
||||
var bucket = new Date(date.getTime());
|
||||
bucket.setUTCMilliseconds(0);
|
||||
bucket.setUTCSeconds(0);
|
||||
bucket.setUTCMinutes(0);
|
||||
bucket.setUTCHours(0);
|
||||
|
||||
if (period === '30d' || period === 'this_month') {
|
||||
var day = bucket.getUTCDay();
|
||||
var offset = day === 0 ? 6 : (day - 1);
|
||||
bucket.setUTCDate(bucket.getUTCDate() - offset);
|
||||
return bucket;
|
||||
}
|
||||
|
||||
if (period === '90d') {
|
||||
bucket.setUTCDate(1);
|
||||
return bucket;
|
||||
}
|
||||
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function shiftUtcBucket(date, period, amount) {
|
||||
var shifted = new Date(date.getTime());
|
||||
if (period === '30d' || period === 'this_month') {
|
||||
shifted.setUTCDate(shifted.getUTCDate() + (amount * 7));
|
||||
return shifted;
|
||||
}
|
||||
if (period === '90d') {
|
||||
shifted.setUTCMonth(shifted.getUTCMonth() + amount);
|
||||
return shifted;
|
||||
}
|
||||
shifted.setUTCDate(shifted.getUTCDate() + amount);
|
||||
return shifted;
|
||||
}
|
||||
|
||||
function bucketKey(date) {
|
||||
return date.toISOString().slice(0, 19) + 'Z';
|
||||
}
|
||||
|
||||
function expectedBucketCount(period, endBucket) {
|
||||
if (period === '7d') {
|
||||
return 7;
|
||||
}
|
||||
if (period === '30d') {
|
||||
return 5;
|
||||
}
|
||||
if (period === '90d') {
|
||||
return 4;
|
||||
}
|
||||
if (period === 'this_month') {
|
||||
var firstDay = new Date(Date.UTC(endBucket.getUTCFullYear(), endBucket.getUTCMonth(), 1));
|
||||
var firstWeek = toUtcBucketStart(firstDay, period);
|
||||
var diffMs = endBucket.getTime() - firstWeek.getTime();
|
||||
return Math.max(1, Math.floor(diffMs / (7 * 24 * 60 * 60 * 1000)) + 1);
|
||||
}
|
||||
return 7;
|
||||
}
|
||||
|
||||
function normalizeTimeline(timeline, period) {
|
||||
if (!timeline.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var byBucket = {};
|
||||
timeline.forEach(function(point) {
|
||||
byBucket[point.bucket_start] = point;
|
||||
});
|
||||
|
||||
var latestBucket = timeline.reduce(function(current, point) {
|
||||
return point.bucket_start > current.bucket_start ? point : current;
|
||||
}, timeline[0]);
|
||||
var endBucket = toUtcBucketStart(new Date(latestBucket.bucket_start), period);
|
||||
var bucketCount = expectedBucketCount(period, endBucket);
|
||||
var startBucket = shiftUtcBucket(endBucket, period, -(bucketCount - 1));
|
||||
var normalized = [];
|
||||
|
||||
for (var index = 0; index < bucketCount; index += 1) {
|
||||
var bucketDate = shiftUtcBucket(startBucket, period, index);
|
||||
var key = bucketKey(bucketDate);
|
||||
normalized.push(byBucket[key] || {
|
||||
bucket_start: key,
|
||||
calls_ok: 0,
|
||||
calls_error: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function recomputeDerivedState() {
|
||||
var operations = state.usage ? (state.usage.operations || []) : [];
|
||||
var timeline = state.usage ? normalizeTimeline(state.usage.timeline || [], state.period) : [];
|
||||
var totalCalls = operations.reduce(function (sum, operation) {
|
||||
return sum + operation.calls_total;
|
||||
}, 0);
|
||||
var timelineMaxValue = timeline.reduce(function (maxValue, point) {
|
||||
return Math.max(maxValue, point.calls_ok + point.calls_error);
|
||||
}, 0);
|
||||
|
||||
state.derived = {
|
||||
localizedOperations: operations.map(function (operation) {
|
||||
return {
|
||||
operation: operation,
|
||||
localized: localizedUsageOperation(operation),
|
||||
};
|
||||
}),
|
||||
normalizedTimeline: timeline,
|
||||
timelineMaxValue: timelineMaxValue,
|
||||
totalCalls: totalCalls,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEmptyState(title, message, compact) {
|
||||
var empty = element('div', 'empty-state');
|
||||
if (compact) {
|
||||
empty.style.padding = '0';
|
||||
}
|
||||
empty.appendChild(element('div', 'empty-state-title', title));
|
||||
empty.appendChild(element('div', 'empty-state-text', message));
|
||||
return empty;
|
||||
}
|
||||
|
||||
function renderEmpty(title, message) {
|
||||
chartWrap.innerHTML = '';
|
||||
var chartEmpty = buildEmptyState(title, message, false);
|
||||
chartEmpty.style.width = '100%';
|
||||
chartWrap.appendChild(chartEmpty);
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
var row = document.createElement('tr');
|
||||
var cell = document.createElement('td');
|
||||
cell.colSpan = 7;
|
||||
cell.style.padding = '24px';
|
||||
cell.appendChild(buildEmptyState(title, message, true));
|
||||
row.appendChild(cell);
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
var summary = state.usage ? state.usage.summary : null;
|
||||
var cards = [
|
||||
{
|
||||
value: formatCount(summary ? summary.rollup.calls_total : 0),
|
||||
text: tfKey('usage.stats.across', { period: periodLabel(state.period) }),
|
||||
},
|
||||
{
|
||||
value: ((summary ? summary.success_rate : 0) || 0).toFixed(1) + '%',
|
||||
text: tfKey('usage.stats.success_calls', { count: formatCount(summary ? summary.rollup.calls_ok : 0) }),
|
||||
},
|
||||
{
|
||||
value: formatMs(summary ? summary.rollup.p50_ms : 0),
|
||||
text: tKey('usage.stats.median'),
|
||||
},
|
||||
{
|
||||
value: formatMs(summary ? summary.rollup.p99_ms : 0),
|
||||
text: tfKey('usage.stats.error_calls', { count: formatCount(summary ? summary.rollup.calls_error : 0) }),
|
||||
}
|
||||
];
|
||||
|
||||
cards.forEach(function (card, index) {
|
||||
var node = statCards[index];
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
node.querySelector('.stat-value').textContent = card.value;
|
||||
var delta = node.querySelector('.stat-delta');
|
||||
delta.className = 'stat-delta';
|
||||
delta.textContent = card.text;
|
||||
});
|
||||
}
|
||||
|
||||
function renderChart() {
|
||||
var timeline = state.derived.normalizedTimeline;
|
||||
chartWrap.innerHTML = '';
|
||||
|
||||
if (!timeline.length) {
|
||||
var chartEmpty = buildEmptyState(
|
||||
tKey('usage.chart.empty.title'),
|
||||
tKey('usage.chart.empty.sub'),
|
||||
false
|
||||
);
|
||||
chartEmpty.style.width = '100%';
|
||||
chartWrap.appendChild(chartEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
var maxValue = state.derived.timelineMaxValue;
|
||||
|
||||
timeline.forEach(function (point, index) {
|
||||
var node = chartTemplate.content.cloneNode(true);
|
||||
var okHeight = maxValue ? Math.round((point.calls_ok / maxValue) * 160) : 0;
|
||||
var errorHeight = maxValue ? Math.round((point.calls_error / maxValue) * 160) : 0;
|
||||
if (point.calls_ok > 0) {
|
||||
okHeight = Math.max(okHeight, 6);
|
||||
}
|
||||
if (point.calls_error > 0) {
|
||||
errorHeight = Math.max(errorHeight, 6);
|
||||
}
|
||||
node.querySelector('.chart-bar.success').style.height = okHeight + 'px';
|
||||
node.querySelector('.chart-bar.success').dataset.tip = tfKey('usage.chart.ok', { count: formatCount(point.calls_ok) });
|
||||
node.querySelector('.chart-bar.error').style.height = errorHeight + 'px';
|
||||
node.querySelector('.chart-bar.error').dataset.tip = tfKey('usage.chart.errors', { count: formatCount(point.calls_error) });
|
||||
node.querySelector('.chart-col-label').textContent = chartBucketLabel(point.bucket_start, state.period, index);
|
||||
chartWrap.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
var operations = state.derived.localizedOperations;
|
||||
tableBody.innerHTML = '';
|
||||
if (cardList) {
|
||||
cardList.innerHTML = '';
|
||||
}
|
||||
|
||||
if (!operations.length) {
|
||||
var row = document.createElement('tr');
|
||||
var cell = document.createElement('td');
|
||||
cell.colSpan = 7;
|
||||
cell.style.padding = '24px';
|
||||
cell.appendChild(
|
||||
buildEmptyState(
|
||||
tKey('usage.table.empty.title'),
|
||||
tKey('usage.table.empty.sub'),
|
||||
true
|
||||
)
|
||||
);
|
||||
row.appendChild(cell);
|
||||
tableBody.appendChild(row);
|
||||
renderOperationCards([]);
|
||||
return;
|
||||
}
|
||||
|
||||
operations.forEach(function (entry) {
|
||||
var operation = entry.operation;
|
||||
var localizedOperation = entry.localized;
|
||||
var node = rowTemplate.content.cloneNode(true);
|
||||
var errorRate = operation.calls_total === 0
|
||||
? 0
|
||||
: ((operation.calls_error / operation.calls_total) * 100);
|
||||
var share = state.derived.totalCalls === 0
|
||||
? 0
|
||||
: ((operation.calls_total / state.derived.totalCalls) * 100);
|
||||
|
||||
node.querySelector('.col-name').textContent = localizedOperation.operation_display_name || operation.operation_display_name;
|
||||
node.querySelector('.col-op-slug').textContent = operation.operation_name;
|
||||
|
||||
var proto = node.querySelector('.col-proto');
|
||||
proto.textContent = protocolLabel(operation.protocol);
|
||||
proto.style.color = protocolColor(operation.protocol);
|
||||
|
||||
node.querySelector('.col-calls').textContent = formatCount(operation.calls_total);
|
||||
node.querySelector('.col-errors').textContent = formatCount(operation.calls_error);
|
||||
|
||||
var errorRateEl = node.querySelector('.col-err-rate');
|
||||
errorRateEl.textContent = errorRate.toFixed(2) + '%';
|
||||
errorRateEl.style.color = errorRate > 5 ? 'var(--red)' : (errorRate > 1 ? 'var(--amber)' : 'var(--green)');
|
||||
|
||||
var latencyText = node.querySelector('.col-latency-text');
|
||||
latencyText.textContent =
|
||||
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms);
|
||||
|
||||
var base = Math.max(operation.p99_ms, 1);
|
||||
node.querySelector('.latency-p50').style.width = Math.max(4, Math.round((operation.p50_ms / base) * 64)) + 'px';
|
||||
node.querySelector('.latency-p95').style.width = Math.max(4, Math.round(((operation.p95_ms - operation.p50_ms) / base) * 64)) + 'px';
|
||||
node.querySelector('.latency-p99').style.width = Math.max(4, Math.round(((operation.p99_ms - operation.p95_ms) / base) * 64)) + 'px';
|
||||
|
||||
node.querySelector('.col-quota-label').textContent = tfKey('usage.table.share', { value: share.toFixed(1) });
|
||||
node.querySelector('.col-quota-fill').style.width = share.toFixed(1) + '%';
|
||||
|
||||
tableBody.appendChild(node);
|
||||
});
|
||||
|
||||
renderOperationCards(operations);
|
||||
}
|
||||
|
||||
function renderOperationCards(operations) {
|
||||
if (!cardList) {
|
||||
return;
|
||||
}
|
||||
|
||||
cardList.innerHTML = '';
|
||||
|
||||
if (!operations.length) {
|
||||
cardList.appendChild(buildEmptyState(
|
||||
tKey('usage.table.empty.title'),
|
||||
tKey('usage.table.empty.sub'),
|
||||
false
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
operations.forEach(function(entry) {
|
||||
var operation = entry.operation;
|
||||
var localizedOperation = entry.localized;
|
||||
var errorRate = operation.calls_total === 0
|
||||
? 0
|
||||
: ((operation.calls_error / operation.calls_total) * 100);
|
||||
var share = state.derived.totalCalls === 0
|
||||
? 0
|
||||
: ((operation.calls_total / state.derived.totalCalls) * 100);
|
||||
|
||||
var card = element('div', 'resource-card');
|
||||
var header = element('div', 'resource-card-header');
|
||||
var headerMain = element('div');
|
||||
headerMain.appendChild(element(
|
||||
'div',
|
||||
'resource-card-title',
|
||||
localizedOperation.operation_display_name || operation.operation_display_name
|
||||
));
|
||||
headerMain.appendChild(element('div', 'resource-card-subtitle', operation.operation_name));
|
||||
|
||||
var actions = element('div', 'resource-card-actions');
|
||||
var protocolBadge = element('span', 'badge');
|
||||
protocolBadge.textContent = protocolLabel(operation.protocol);
|
||||
protocolBadge.style.color = protocolColor(operation.protocol);
|
||||
actions.appendChild(protocolBadge);
|
||||
header.appendChild(headerMain);
|
||||
header.appendChild(actions);
|
||||
card.appendChild(header);
|
||||
|
||||
var metaGrid = element('div', 'resource-meta-grid');
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(operation.calls_total)));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.errors', formatCount(operation.calls_error)));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.error_rate', errorRate.toFixed(2) + '%'));
|
||||
metaGrid.appendChild(buildUsageMetaItem(
|
||||
'usage.table.th.latency',
|
||||
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms)
|
||||
));
|
||||
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.share', tfKey('usage.table.share', { value: share.toFixed(1) })));
|
||||
card.appendChild(metaGrid);
|
||||
|
||||
cardList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function buildUsageMetaItem(labelKey, valueText) {
|
||||
var item = element('div', 'resource-meta-item');
|
||||
item.appendChild(element('div', 'resource-meta-label', tKey(labelKey)));
|
||||
item.appendChild(element('div', 'resource-meta-value', valueText));
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderUsage() {
|
||||
if (state.loading && !state.usage) {
|
||||
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.loadError) {
|
||||
renderEmpty(tKey('usage.error.title'), state.loadError);
|
||||
return;
|
||||
}
|
||||
|
||||
renderStats();
|
||||
renderChart();
|
||||
renderTable();
|
||||
if (subtitle) {
|
||||
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsage() {
|
||||
if (!window.CrankApi) {
|
||||
state.loadError = tKey('usage.error.api');
|
||||
renderUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.loadError = tKey('usage.error.workspace');
|
||||
renderUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.loadError = '';
|
||||
renderUsage();
|
||||
|
||||
try {
|
||||
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
|
||||
recomputeDerivedState();
|
||||
} catch (error) {
|
||||
state.loadError = error.message || tKey('usage.error.load');
|
||||
state.usage = null;
|
||||
recomputeDerivedState();
|
||||
} finally {
|
||||
state.loading = false;
|
||||
renderUsage();
|
||||
}
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
if (!state.usage) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = [tKey('usage.csv.header')];
|
||||
state.usage.operations.forEach(function (operation) {
|
||||
var localizedOperation = localizedUsageOperation(operation);
|
||||
var errorRate = operation.calls_total === 0
|
||||
? 0
|
||||
: ((operation.calls_error / operation.calls_total) * 100);
|
||||
rows.push([
|
||||
'"' + (localizedOperation.operation_display_name || operation.operation_display_name) + '"',
|
||||
'"' + protocolLabel(operation.protocol) + '"',
|
||||
operation.calls_total,
|
||||
operation.calls_error,
|
||||
errorRate.toFixed(2),
|
||||
operation.p50_ms,
|
||||
operation.p95_ms,
|
||||
operation.p99_ms,
|
||||
].join(','));
|
||||
});
|
||||
|
||||
var blob = new Blob([rows.join('\r\n')], { type: 'text/csv' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'crank-usage-' + state.period + '.csv';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
|
||||
}
|
||||
}
|
||||
|
||||
if (periodSelect) {
|
||||
periodSelect.value = state.period;
|
||||
periodSelect.addEventListener('change', function () {
|
||||
state.period = this.value;
|
||||
loadUsage();
|
||||
});
|
||||
}
|
||||
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', exportCsv);
|
||||
}
|
||||
|
||||
window.addEventListener('crank:workspacechange', loadUsage);
|
||||
|
||||
if (window.whenWorkspacesReady) {
|
||||
window.whenWorkspacesReady().finally(loadUsage);
|
||||
} else {
|
||||
loadUsage();
|
||||
}
|
||||
});
|
||||
Vendored
+5
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1,409 @@
|
||||
function buildToolDescription() {
|
||||
return {
|
||||
title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'),
|
||||
description: textValue('tool-description'),
|
||||
tags: [],
|
||||
examples: [],
|
||||
};
|
||||
}
|
||||
|
||||
function collectWizardPayload() {
|
||||
var name = textValue('tool-name');
|
||||
if (!name) throw new Error(tKey('wizard.error.tool_name'));
|
||||
|
||||
var inputSchemaValue = parseStructuredText(textValue('tool-input-schema'));
|
||||
var outputSchemaValue = parseStructuredText(textValue('tool-output-schema'));
|
||||
var inputMappingValue = parseStructuredText(textValue('tool-input-mapping'));
|
||||
var outputMappingValue = parseStructuredText(textValue('tool-output-mapping'));
|
||||
|
||||
return {
|
||||
name: name,
|
||||
display_name: textValue('tool-display-name') || name,
|
||||
category: 'general',
|
||||
protocol: wizardProtocol,
|
||||
target: buildTarget(),
|
||||
input_schema: convertJsonSchemaToCrankSchema(inputSchemaValue, []),
|
||||
output_schema: convertJsonSchemaToCrankSchema(outputSchemaValue, []),
|
||||
input_mapping: buildMappingSet(inputMappingValue, 'input'),
|
||||
output_mapping: buildMappingSet(outputMappingValue, 'output'),
|
||||
execution_config: parseExecutionConfig(textValue('tool-exec-config')),
|
||||
tool_description: buildToolDescription(),
|
||||
};
|
||||
}
|
||||
|
||||
async function saveOperation(stayOnPage) {
|
||||
try {
|
||||
await persistCurrentDraft(stayOnPage);
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('wizard.error.save'), tKey('wizard.error.save_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOperationForEdit() {
|
||||
if (!wizardWorkspaceId || !wizardEditId) return;
|
||||
var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId);
|
||||
wizardProtocol = detail.protocol || 'rest';
|
||||
await loadWizardPanels([3]);
|
||||
var draftVersion = await window.CrankApi.getOperationVersion(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
detail.draft_version_ref.version
|
||||
);
|
||||
wizardCurrentOperation = detail;
|
||||
wizardCurrentVersion = draftVersion;
|
||||
prefillWizardFromEdit(detail, draftVersion);
|
||||
}
|
||||
|
||||
function setEditModePresentation() {
|
||||
var progressLabel = document.querySelector('.progress-label');
|
||||
if (progressLabel) progressLabel.textContent = tKey('wizard.progress.edit');
|
||||
renderSidebarBrand('edit');
|
||||
}
|
||||
|
||||
function selectProtocol(protocol) {
|
||||
wizardProtocol = protocol || 'rest';
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
var proto = card.dataset.protocol || 'rest';
|
||||
var selected = proto === wizardProtocol;
|
||||
card.classList.toggle('selected', selected);
|
||||
card.setAttribute('aria-checked', selected ? 'true' : 'false');
|
||||
});
|
||||
var s3name = document.getElementById('sidebar-step-3-name');
|
||||
if (s3name) {
|
||||
var step3Labels = window.CrankWizardState.step3Labels();
|
||||
s3name.textContent = step3Labels[wizardProtocol] || tKey('wizard.step3.rest.label');
|
||||
}
|
||||
updateWizardProtocolVisibility();
|
||||
}
|
||||
|
||||
function bindWizardLiveActions() {
|
||||
bindLiveAction('wizard-upload-input-sample', tKey('wizard.busy.input_sample'), uploadInputSampleFromWizard);
|
||||
bindLiveAction('wizard-upload-output-sample', tKey('wizard.busy.output_sample'), uploadOutputSampleFromWizard);
|
||||
bindLiveAction('wizard-generate-draft', tKey('wizard.busy.generate'), generateDraftFromWizard);
|
||||
bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest);
|
||||
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
|
||||
bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml);
|
||||
bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml);
|
||||
bindLiveAction('wizard-publish-operation', tKey('wizard.busy.publish'), publishWizardOperation);
|
||||
bindClick('wizard-import-yaml-file-trigger', function() {
|
||||
var input = document.getElementById('wizard-import-yaml-file');
|
||||
if (input) input.click();
|
||||
});
|
||||
|
||||
var yamlInput = document.getElementById('wizard-import-yaml-file');
|
||||
if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection);
|
||||
}
|
||||
|
||||
function bindClick(id, handler) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
element.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
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(
|
||||
tKey('wizard.live.failed_title'),
|
||||
error && error.message ? error.message : tKey('wizard.live.failed_body'),
|
||||
true
|
||||
);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error && error.message ? error.message : tKey('wizard.live.failed_body'),
|
||||
tKey('wizard.live.failed_title')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
button.dataset.busy = 'false';
|
||||
button.disabled = false;
|
||||
button.classList.remove('is-busy');
|
||||
button.textContent = originalLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWizardProtocolVisibility() {
|
||||
if (typeof window.renderEditionCapabilityHints === 'function'
|
||||
&& window.CrankWizardState
|
||||
&& typeof window.CrankWizardState.currentEditionCapabilities === 'function') {
|
||||
window.renderEditionCapabilityHints(window.CrankWizardState.currentEditionCapabilities());
|
||||
}
|
||||
}
|
||||
|
||||
function showWizardLiveStatus(title, text, isError) {
|
||||
var root = document.getElementById('wizard-live-status');
|
||||
var titleEl = document.getElementById('wizard-live-status-title');
|
||||
var textEl = document.getElementById('wizard-live-status-text');
|
||||
if (!root || !titleEl || !textEl) return;
|
||||
titleEl.textContent = title;
|
||||
textEl.textContent = text;
|
||||
root.hidden = false;
|
||||
root.classList.toggle('is-error', !!isError);
|
||||
root.classList.toggle('is-success', !isError);
|
||||
}
|
||||
|
||||
function currentDraftVersion() {
|
||||
if (wizardCurrentOperation && wizardCurrentOperation.draft_version_ref) {
|
||||
return wizardCurrentOperation.draft_version_ref.version;
|
||||
}
|
||||
if (wizardCurrentOperation && wizardCurrentOperation.current_draft_version) {
|
||||
return wizardCurrentOperation.current_draft_version;
|
||||
}
|
||||
if (wizardCurrentVersion && wizardCurrentVersion.version) {
|
||||
return wizardCurrentVersion.version;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function updateOperationPayload(payload) {
|
||||
return {
|
||||
display_name: payload.display_name,
|
||||
category: payload.category,
|
||||
target: payload.target,
|
||||
input_schema: payload.input_schema,
|
||||
output_schema: payload.output_schema,
|
||||
input_mapping: payload.input_mapping,
|
||||
output_mapping: payload.output_mapping,
|
||||
execution_config: payload.execution_config,
|
||||
tool_description: payload.tool_description,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshCurrentOperationState() {
|
||||
if (!wizardWorkspaceId || !wizardEditId) return null;
|
||||
var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId);
|
||||
var versionNumber = detail.draft_version_ref
|
||||
? detail.draft_version_ref.version
|
||||
: detail.current_draft_version;
|
||||
var versionDocument = await window.CrankApi.getOperationVersion(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
versionNumber
|
||||
);
|
||||
wizardCurrentOperation = detail;
|
||||
wizardCurrentVersion = versionDocument;
|
||||
updateWizardProtocolVisibility();
|
||||
return {
|
||||
detail: detail,
|
||||
version: versionDocument,
|
||||
};
|
||||
}
|
||||
|
||||
async function persistCurrentDraft(stayOnPage) {
|
||||
if (!wizardWorkspaceId) {
|
||||
throw new Error(tKey('wizard.error.no_workspace'));
|
||||
}
|
||||
|
||||
var payload = collectWizardPayload();
|
||||
var response;
|
||||
var createdNow = false;
|
||||
|
||||
if (wizardMode === 'edit' && wizardEditId) {
|
||||
response = await window.CrankApi.updateOperation(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
updateOperationPayload(payload)
|
||||
);
|
||||
} else {
|
||||
response = await window.CrankApi.createOperation(wizardWorkspaceId, payload);
|
||||
createdNow = true;
|
||||
wizardEditId = response.operation_id;
|
||||
wizardMode = 'edit';
|
||||
history.replaceState(
|
||||
{},
|
||||
'',
|
||||
window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(wizardEditId)
|
||||
);
|
||||
setEditModePresentation();
|
||||
_doGoToStep(currentStep);
|
||||
var toolNameInput = document.getElementById('tool-name');
|
||||
if (toolNameInput) toolNameInput.disabled = true;
|
||||
}
|
||||
|
||||
await refreshCurrentOperationState();
|
||||
|
||||
if (stayOnPage) {
|
||||
var saveDraftBtn = document.querySelector('.btn-save-draft');
|
||||
if (saveDraftBtn) {
|
||||
var label = saveDraftBtn.textContent;
|
||||
saveDraftBtn.textContent = tKey('wizard.save.saved');
|
||||
setTimeout(function() {
|
||||
saveDraftBtn.textContent = label;
|
||||
}, 1400);
|
||||
}
|
||||
}
|
||||
|
||||
showWizardLiveStatus(
|
||||
createdNow ? tKey('wizard.save.created') : tKey('wizard.save.updated'),
|
||||
tKey('wizard.save.body')
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
function safeStringify(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function downloadTextFile(fileName, content, mimeType) {
|
||||
var blob = new Blob([content], { type: mimeType || 'text/plain;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 setTextareaValue(id, value) {
|
||||
var element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
element.value = safeStringify(value);
|
||||
}
|
||||
|
||||
async function exportWizardYaml() {
|
||||
await persistCurrentDraft(true);
|
||||
var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, {
|
||||
mode: 'portable',
|
||||
version: currentDraftVersion(),
|
||||
});
|
||||
downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml');
|
||||
showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body'));
|
||||
}
|
||||
|
||||
async function importWizardYaml() {
|
||||
if (!wizardWorkspaceId) {
|
||||
throw new Error(tKey('wizard.error.no_workspace'));
|
||||
}
|
||||
var yamlDocument = textValue('wizard-import-yaml-text');
|
||||
if (!yamlDocument) {
|
||||
showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
|
||||
return;
|
||||
}
|
||||
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
|
||||
showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
|
||||
window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
|
||||
}
|
||||
|
||||
async function publishWizardOperation() {
|
||||
await persistCurrentDraft(true);
|
||||
var published = await window.CrankApi.publishOperation(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
currentDraftVersion()
|
||||
);
|
||||
await refreshCurrentOperationState();
|
||||
showWizardLiveStatus(
|
||||
tKey('wizard.publish.done'),
|
||||
tfKey('wizard.publish.done_body', { version: published.published_version })
|
||||
);
|
||||
}
|
||||
|
||||
function handleImportYamlFileSelection(event) {
|
||||
var file = event.target.files && event.target.files[0];
|
||||
if (!file) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(loadEvent) {
|
||||
setValue('wizard-import-yaml-text', loadEvent.target.result || '');
|
||||
showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body'));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function describeWizardTestResult(result) {
|
||||
return {
|
||||
title: result.ok ? tKey('wizard.test.completed') : tKey('wizard.test.failed'),
|
||||
body: result.ok ? tKey('wizard.test.completed_body') : tKey('wizard.test.failed_body'),
|
||||
isError: !result.ok,
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadInputSampleFromWizard() {
|
||||
await persistCurrentDraft(true);
|
||||
await window.CrankApi.uploadInputSample(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
parseStructuredText(textValue('wizard-input-sample'))
|
||||
);
|
||||
showWizardLiveStatus(tKey('wizard.sample.input_saved'), tKey('wizard.sample.input_saved_body'));
|
||||
}
|
||||
|
||||
async function uploadOutputSampleFromWizard() {
|
||||
await persistCurrentDraft(true);
|
||||
await window.CrankApi.uploadOutputSample(
|
||||
wizardWorkspaceId,
|
||||
wizardEditId,
|
||||
parseStructuredText(textValue('wizard-output-sample'))
|
||||
);
|
||||
showWizardLiveStatus(tKey('wizard.sample.output_saved'), tKey('wizard.sample.output_saved_body'));
|
||||
}
|
||||
|
||||
async function generateDraftFromWizard() {
|
||||
await persistCurrentDraft(true);
|
||||
var generated = await window.CrankApi.generateDraft(wizardWorkspaceId, wizardEditId, {});
|
||||
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(generated.input_schema), null, 2));
|
||||
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2));
|
||||
setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol));
|
||||
setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol));
|
||||
showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body'));
|
||||
}
|
||||
|
||||
async function runWizardTest() {
|
||||
await persistCurrentDraft(true);
|
||||
var result = await window.CrankApi.runOperationTest(wizardWorkspaceId, wizardEditId, {
|
||||
version: currentDraftVersion(),
|
||||
input: parseStructuredText(textValue('wizard-test-input')),
|
||||
});
|
||||
wizardTestResponsePreview = result.response_preview;
|
||||
setTextareaValue('wizard-test-request-preview', result.request_preview);
|
||||
setTextareaValue('wizard-test-response-preview', result.response_preview);
|
||||
setTextareaValue('wizard-test-errors', result.errors && result.errors.length ? result.errors : []);
|
||||
var status = describeWizardTestResult(result);
|
||||
showWizardLiveStatus(
|
||||
status.title,
|
||||
status.body,
|
||||
status.isError
|
||||
);
|
||||
}
|
||||
|
||||
function copyTestResponseToOutputSample() {
|
||||
if (!wizardTestResponsePreview) {
|
||||
showWizardLiveStatus(tKey('wizard.test.no_response'), tKey('wizard.test.no_response_body'), true);
|
||||
return;
|
||||
}
|
||||
setTextareaValue('wizard-output-sample', wizardTestResponsePreview);
|
||||
showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body'));
|
||||
}
|
||||
|
||||
window.CrankWizardLive = {
|
||||
saveOperation: saveOperation,
|
||||
loadOperationForEdit: loadOperationForEdit,
|
||||
bindWizardLiveActions: bindWizardLiveActions,
|
||||
updateWizardProtocolVisibility: updateWizardProtocolVisibility,
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
function inferSchemaKind(typeName) {
|
||||
if (typeName === 'object') return 'object';
|
||||
if (typeName === 'array') return 'array';
|
||||
if (typeName === 'string') return 'string';
|
||||
if (typeName === 'integer') return 'integer';
|
||||
if (typeName === 'number') return 'number';
|
||||
if (typeName === 'boolean') return 'boolean';
|
||||
if (typeName === 'null') return 'null';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
function convertJsonSchemaToCrankSchema(node, requiredNames) {
|
||||
var schemaNode = node || {};
|
||||
var typeName = Array.isArray(schemaNode.type)
|
||||
? schemaNode.type.find(function(value) { return value !== 'null'; }) || 'string'
|
||||
: schemaNode.type || (schemaNode.properties ? 'object' : 'string');
|
||||
var nullable = Array.isArray(schemaNode.type) && schemaNode.type.indexOf('null') >= 0;
|
||||
var kind = schemaNode.enum ? 'enum' : inferSchemaKind(typeName);
|
||||
var schema = {
|
||||
type: kind,
|
||||
description: schemaNode.description || null,
|
||||
required: false,
|
||||
nullable: nullable,
|
||||
default_value: Object.prototype.hasOwnProperty.call(schemaNode, 'default') ? schemaNode.default : null,
|
||||
fields: {},
|
||||
items: null,
|
||||
enum_values: schemaNode.enum || [],
|
||||
variants: [],
|
||||
};
|
||||
|
||||
if (kind === 'object') {
|
||||
var requiredSet = new Set(schemaNode.required || requiredNames || []);
|
||||
Object.keys(schemaNode.properties || {}).forEach(function(fieldName) {
|
||||
var field = convertJsonSchemaToCrankSchema(schemaNode.properties[fieldName], []);
|
||||
field.required = requiredSet.has(fieldName);
|
||||
schema.fields[fieldName] = field;
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === 'array' && schemaNode.items) {
|
||||
schema.items = convertJsonSchemaToCrankSchema(schemaNode.items, []);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function mappingRootForProtocol(protocol) {
|
||||
return '$.request.body';
|
||||
}
|
||||
|
||||
function normalizeInputSource(path) {
|
||||
if (!path) return '$.mcp';
|
||||
if (path === '$') return '$.mcp';
|
||||
if (path.indexOf('$.mcp') === 0) return path;
|
||||
if (path.indexOf('$.input') === 0) return '$.mcp' + path.slice('$.input'.length);
|
||||
return '$.mcp.' + path.replace(/^\$\./, '');
|
||||
}
|
||||
|
||||
function normalizeOutputSource(path) {
|
||||
if (!path) return '$.response.body';
|
||||
if (path.indexOf('$.') === 0) return path;
|
||||
return '$.' + path.replace(/^\$\./, '');
|
||||
}
|
||||
|
||||
function buildMappingSet(rawValue, mode) {
|
||||
var value = rawValue || {};
|
||||
var root = mappingRootForProtocol(wizardProtocol);
|
||||
var rules = [];
|
||||
|
||||
Object.keys(value).forEach(function(target) {
|
||||
var source = value[target];
|
||||
if (mode === 'input') {
|
||||
rules.push({
|
||||
source: normalizeInputSource(source),
|
||||
target: root + '.' + target,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
rules.push({
|
||||
source: normalizeOutputSource(source),
|
||||
target: '$.output.' + target,
|
||||
});
|
||||
});
|
||||
|
||||
return { rules: rules };
|
||||
}
|
||||
|
||||
function parseExecutionConfig(text) {
|
||||
var value = parseStructuredText(text);
|
||||
var retry = value.retry || {};
|
||||
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
||||
var selectedUpstream = currentUpstream();
|
||||
if (!selectedUpstream && wizardMode !== 'edit') {
|
||||
throw new Error(tKey('wizard.error.save_upstream_first'));
|
||||
}
|
||||
var authProfileRef = selectedUpstream && selectedUpstream.authProfileId
|
||||
? selectedUpstream.authProfileId
|
||||
: (value.auth && value.auth.profile ? value.auth.profile : null);
|
||||
var config = {
|
||||
timeout_ms: Number(value.timeout_ms || 10000),
|
||||
retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null,
|
||||
auth_profile_ref: authProfileRef,
|
||||
headers: headers,
|
||||
protocol_options: null,
|
||||
streaming: null,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function currentUpstream() {
|
||||
var selected = upstreams.find(function(item) { return item.id === selectedUpstreamId; });
|
||||
if (selected) return selected;
|
||||
|
||||
var customName = textValue('new-upstream-name');
|
||||
var customUrl = textValue('new-upstream-url');
|
||||
if (!customUrl) return null;
|
||||
|
||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null;
|
||||
var authProfile = authProfileById(authProfileId);
|
||||
return {
|
||||
id: 'custom',
|
||||
name: customName || 'custom-upstream',
|
||||
url: customUrl,
|
||||
staticHeaders: textValue('new-upstream-static-headers') || '{\n}',
|
||||
authMode: authMode,
|
||||
authProfileId: authProfileId || null,
|
||||
authProfileName: authProfile ? authProfile.name : '',
|
||||
authKind: authProfile ? authProfile.kind : null,
|
||||
};
|
||||
}
|
||||
|
||||
function joinUrl(baseUrl, path) {
|
||||
if (!baseUrl) return path || '';
|
||||
if (!path) return baseUrl;
|
||||
return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function parseHeaderMap(text) {
|
||||
if (!text || !text.trim()) return {};
|
||||
var value = parseStructuredText(text);
|
||||
return value && typeof value === 'object' ? value : {};
|
||||
}
|
||||
|
||||
function buildTarget() {
|
||||
var upstream = currentUpstream();
|
||||
if (!upstream || !upstream.url) {
|
||||
throw new Error(tKey('wizard.error.select_upstream'));
|
||||
}
|
||||
|
||||
var pathTemplate = textValue('endpoint-path') || '/';
|
||||
var activeMethod = document.querySelector('.method-card.active');
|
||||
return {
|
||||
kind: 'rest',
|
||||
base_url: upstream.url,
|
||||
method: activeMethod ? activeMethod.dataset.method : 'POST',
|
||||
path_template: pathTemplate,
|
||||
static_headers: parseHeaderMap(upstream.staticHeaders),
|
||||
};
|
||||
}
|
||||
|
||||
function crankSchemaToJsonSchema(schema) {
|
||||
var result = {};
|
||||
var typeName = schema.type === 'enum' ? 'string' : schema.type;
|
||||
result.type = schema.nullable ? [typeName, 'null'] : typeName;
|
||||
if (schema.description) result.description = schema.description;
|
||||
if (schema.default_value !== null && schema.default_value !== undefined) result.default = schema.default_value;
|
||||
if (schema.enum_values && schema.enum_values.length) result.enum = schema.enum_values.slice();
|
||||
if (schema.type === 'object') {
|
||||
result.type = schema.nullable ? ['object', 'null'] : 'object';
|
||||
result.properties = {};
|
||||
var required = [];
|
||||
Object.keys(schema.fields || {}).forEach(function(fieldName) {
|
||||
var field = schema.fields[fieldName];
|
||||
result.properties[fieldName] = crankSchemaToJsonSchema(field);
|
||||
if (field.required) required.push(fieldName);
|
||||
});
|
||||
if (required.length) result.required = required;
|
||||
result.additionalProperties = false;
|
||||
}
|
||||
if (schema.type === 'array' && schema.items) {
|
||||
result.items = crankSchemaToJsonSchema(schema.items);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function mappingSetToEditorValue(mappingSet, mode, protocol) {
|
||||
var rules = mappingSet && mappingSet.rules ? mappingSet.rules : [];
|
||||
var object = {};
|
||||
rules.forEach(function(rule) {
|
||||
if (mode === 'input') {
|
||||
var key = rule.target.replace(mappingRootForProtocol(protocol) + '.', '');
|
||||
object[key] = rule.source.replace('$.mcp.', '$.input.');
|
||||
return;
|
||||
}
|
||||
var outputKey = rule.target.replace('$.output.', '');
|
||||
object[outputKey] = rule.source;
|
||||
});
|
||||
return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1 }) : JSON.stringify(object, null, 2);
|
||||
}
|
||||
|
||||
function executionConfigToEditorValue(config) {
|
||||
var value = {
|
||||
timeout_ms: config.timeout_ms,
|
||||
};
|
||||
if (config.retry_policy) {
|
||||
value.retry = { max_attempts: config.retry_policy.max_attempts };
|
||||
}
|
||||
if (config.auth_profile_ref) {
|
||||
value.auth = { profile: config.auth_profile_ref };
|
||||
}
|
||||
if (config.headers && Object.keys(config.headers).length) {
|
||||
value.headers = config.headers;
|
||||
}
|
||||
return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function operationSnapshot(versionDocument) {
|
||||
if (!versionDocument) return {};
|
||||
return versionDocument.snapshot || versionDocument;
|
||||
}
|
||||
|
||||
function prefillUpstream(baseUrl, headers, authProfileRef) {
|
||||
var known = upstreams.find(function(item) {
|
||||
return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null);
|
||||
});
|
||||
if (known) {
|
||||
pickUpstream(known.id);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedUpstreamId = null;
|
||||
var trigger = document.getElementById('upstream-new-trigger');
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (trigger) trigger.classList.add('active');
|
||||
if (form) form.hidden = false;
|
||||
setValue('new-upstream-name', 'imported-upstream');
|
||||
setValue('new-upstream-url', baseUrl);
|
||||
setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
|
||||
setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none');
|
||||
setValue('new-upstream-auth-profile', authProfileRef || '');
|
||||
updateUpstreamAuthUi();
|
||||
|
||||
var valueEl = document.getElementById('upstream-combobox-value');
|
||||
if (valueEl) {
|
||||
valueEl.innerHTML = '';
|
||||
valueEl.appendChild(buildUpstreamTriggerContent('imported-upstream', baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
function prefillWizardFromEdit(detail, versionDocument) {
|
||||
if (!detail || !versionDocument) return;
|
||||
|
||||
var snapshot = operationSnapshot(versionDocument);
|
||||
setEditModePresentation();
|
||||
selectProtocol(snapshot.protocol || detail.protocol);
|
||||
|
||||
var target = snapshot.target || {};
|
||||
if (target.kind === 'rest') {
|
||||
prefillUpstream(target.base_url, target.static_headers, snapshot.execution_config ? snapshot.execution_config.auth_profile_ref : null);
|
||||
setValue('endpoint-path', target.path_template);
|
||||
document.querySelectorAll('.method-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.method === target.method);
|
||||
});
|
||||
}
|
||||
|
||||
setValue('tool-name', snapshot.name || detail.name);
|
||||
setValue('tool-display-name', snapshot.display_name || detail.display_name);
|
||||
setValue('tool-title', snapshot.tool_description ? snapshot.tool_description.title : (snapshot.display_name || detail.display_name));
|
||||
setValue('tool-description', snapshot.tool_description ? snapshot.tool_description.description : '');
|
||||
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(snapshot.input_schema), null, 2));
|
||||
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(snapshot.output_schema), null, 2));
|
||||
setValue('tool-input-mapping', mappingSetToEditorValue(snapshot.input_mapping, 'input', snapshot.protocol || detail.protocol));
|
||||
setValue('tool-output-mapping', mappingSetToEditorValue(snapshot.output_mapping, 'output', snapshot.protocol || detail.protocol));
|
||||
setValue('tool-exec-config', executionConfigToEditorValue(snapshot.execution_config || {}));
|
||||
|
||||
var toolNameInput = document.getElementById('tool-name');
|
||||
if (toolNameInput) toolNameInput.disabled = true;
|
||||
updateWizardProtocolVisibility();
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
(function() {
|
||||
var TOTAL_STEPS = 5;
|
||||
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function step3PanelId() {
|
||||
return 'step-panel-3-rest';
|
||||
}
|
||||
|
||||
function stepFile(step) {
|
||||
return step === 3 ? 'step3-rest.html' : 'step' + step + '.html';
|
||||
}
|
||||
|
||||
function buildIconSvg(href, width, height) {
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', String(width));
|
||||
svg.setAttribute('height', String(height));
|
||||
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('href', href);
|
||||
svg.appendChild(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function appendHtmlFragment(container, html) {
|
||||
if (!container) return;
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
container.appendChild(template.content.cloneNode(true));
|
||||
}
|
||||
|
||||
function setContinueButtonContent(button, label) {
|
||||
if (!button) return;
|
||||
button.textContent = label + ' ';
|
||||
button.appendChild(
|
||||
buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/arrow-right.svg#icon',
|
||||
13,
|
||||
13
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderStepCounter(counter, step, total) {
|
||||
if (!counter) return;
|
||||
var language = localStorage.getItem('crank_lang') || 'en';
|
||||
var parts = language === 'ru'
|
||||
? { prefix: 'Шаг ', suffix: ' из ' + total }
|
||||
: { prefix: 'Step ', suffix: ' of ' + total };
|
||||
var strong = document.createElement('strong');
|
||||
counter.textContent = parts.prefix;
|
||||
strong.textContent = String(step);
|
||||
counter.appendChild(strong);
|
||||
counter.appendChild(document.createTextNode(parts.suffix));
|
||||
}
|
||||
|
||||
function loadStep(step, callback) {
|
||||
var panelId = step === 3 ? step3PanelId() : 'step-panel-' + step;
|
||||
if (document.getElementById(panelId)) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
fetch(stepFile(step))
|
||||
.then(function(response) { return response.text(); })
|
||||
.then(function(html) {
|
||||
var container = document.getElementById('step-panel-container');
|
||||
appendHtmlFragment(container, html);
|
||||
if (typeof applyLang === 'function') applyLang();
|
||||
if (callback) callback();
|
||||
})
|
||||
.catch(function() {
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
function doGoToStep(step) {
|
||||
document.querySelectorAll('.step-number[data-step]').forEach(function(element) {
|
||||
var stepNumber = Number(element.getAttribute('data-step')) || 0;
|
||||
element.textContent = tfKey('wizard.step_short', { step: stepNumber });
|
||||
});
|
||||
|
||||
document.querySelectorAll('.step-pane').forEach(function(pane) { pane.hidden = true; });
|
||||
var panelId = step === 3 ? step3PanelId() : 'step-panel-' + step;
|
||||
var pane = document.getElementById(panelId);
|
||||
if (pane) pane.hidden = false;
|
||||
|
||||
document.querySelectorAll('.step-item').forEach(function(item, index) {
|
||||
var stepNumber = index + 1;
|
||||
item.classList.remove('active', 'done', 'pending');
|
||||
var indicator = item.querySelector('.step-indicator');
|
||||
var status = item.querySelector('.step-status-text');
|
||||
if (stepNumber < step) {
|
||||
item.classList.add('done');
|
||||
if (indicator) {
|
||||
indicator.replaceChildren(
|
||||
buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
|
||||
10,
|
||||
10
|
||||
)
|
||||
);
|
||||
}
|
||||
if (status) status.textContent = tKey('wizard.status.completed');
|
||||
} else if (stepNumber === step) {
|
||||
item.classList.add('active');
|
||||
if (indicator) indicator.textContent = stepNumber;
|
||||
if (status) status.textContent = tKey('wizard.status.in_progress');
|
||||
} else {
|
||||
item.classList.add('pending');
|
||||
if (indicator) indicator.textContent = stepNumber;
|
||||
if (status) status.textContent = tKey('wizard.status.not_started');
|
||||
}
|
||||
});
|
||||
|
||||
var percent = Math.round((step / TOTAL_STEPS) * 100);
|
||||
var fill = document.querySelector('.progress-bar-fill');
|
||||
if (fill) fill.style.width = percent + '%';
|
||||
var percentNode = document.querySelector('.progress-pct');
|
||||
if (percentNode) percentNode.textContent = percent + '%';
|
||||
|
||||
var counter = pane ? pane.querySelector('[data-step-counter]') : null;
|
||||
renderStepCounter(counter, step, TOTAL_STEPS);
|
||||
|
||||
var backButton = document.querySelector('.btn-back');
|
||||
if (backButton) {
|
||||
backButton.disabled = step === 1;
|
||||
}
|
||||
|
||||
var continueButton = document.querySelector('.btn-continue');
|
||||
if (continueButton) {
|
||||
if (step === TOTAL_STEPS) {
|
||||
var finalLabel = window.wizardMode === 'edit' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create');
|
||||
setContinueButtonContent(continueButton, finalLabel);
|
||||
continueButton.classList.add('is-final-step');
|
||||
} else {
|
||||
setContinueButtonContent(continueButton, tKey('wizard.button.continue'));
|
||||
continueButton.classList.remove('is-final-step');
|
||||
}
|
||||
}
|
||||
|
||||
window.currentStep = step;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function goToStep(step) {
|
||||
if (step < 1 || step > TOTAL_STEPS) return;
|
||||
loadStep(step, function() { doGoToStep(step); });
|
||||
}
|
||||
|
||||
function loadWizardPanels(steps) {
|
||||
return steps.reduce(function(chain, step) {
|
||||
return chain.then(function() {
|
||||
return new Promise(function(resolve) {
|
||||
loadStep(step, resolve);
|
||||
});
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
window.addEventListener('crank:langchange', function() {
|
||||
if (window.currentStep) {
|
||||
doGoToStep(window.currentStep);
|
||||
}
|
||||
});
|
||||
|
||||
function bindProtocolCards() {
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
if (card.hidden || card.getAttribute('aria-disabled') === 'true') {
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('.protocol-card').forEach(function(item) {
|
||||
item.classList.remove('selected');
|
||||
item.setAttribute('aria-checked', 'false');
|
||||
});
|
||||
card.classList.add('selected');
|
||||
card.setAttribute('aria-checked', 'true');
|
||||
|
||||
var protocol = card.dataset.protocol
|
||||
|| 'rest';
|
||||
|
||||
window.wizardProtocol = protocol;
|
||||
var step3Name = document.getElementById('sidebar-step-3-name');
|
||||
var labels = window.CrankWizardState.step3Labels();
|
||||
if (step3Name) step3Name.textContent = labels[protocol] || tKey('wizard.step3.rest.label');
|
||||
|
||||
});
|
||||
card.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
card.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyEditionProtocolVisibility(capabilities) {
|
||||
var supported = capabilities && Array.isArray(capabilities.supported_protocols)
|
||||
? capabilities.supported_protocols.slice()
|
||||
: ['rest'];
|
||||
var knownProtocols = ['rest', 'graphql', 'grpc', 'websocket', 'soap'];
|
||||
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
var protocol = card.dataset.protocol || 'rest';
|
||||
var isSupported = supported.indexOf(protocol) >= 0;
|
||||
card.hidden = !isSupported;
|
||||
card.setAttribute('aria-disabled', isSupported ? 'false' : 'true');
|
||||
if (!isSupported) {
|
||||
card.classList.remove('selected');
|
||||
card.setAttribute('aria-checked', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
var hiddenProtocols = knownProtocols.filter(function(protocol) {
|
||||
return supported.indexOf(protocol) === -1;
|
||||
});
|
||||
|
||||
if (supported.indexOf(window.wizardProtocol) === -1) {
|
||||
window.wizardProtocol = supported[0] || 'rest';
|
||||
}
|
||||
|
||||
var selectedCard = document.querySelector('.protocol-card[data-protocol="' + window.wizardProtocol + '"]');
|
||||
if (selectedCard) {
|
||||
selectedCard.classList.add('selected');
|
||||
selectedCard.setAttribute('aria-checked', 'true');
|
||||
}
|
||||
|
||||
var step3Name = document.getElementById('sidebar-step-3-name');
|
||||
var labels = window.CrankWizardState.step3Labels();
|
||||
if (step3Name) {
|
||||
step3Name.textContent = labels[window.wizardProtocol] || tKey('wizard.step3.rest.label');
|
||||
}
|
||||
|
||||
var note = document.getElementById('wizard-edition-protocol-note');
|
||||
var noteText = document.getElementById('wizard-edition-protocol-note-text');
|
||||
if (note && noteText) {
|
||||
if (hiddenProtocols.length) {
|
||||
note.hidden = false;
|
||||
noteText.textContent = tfKey('wizard.step1.community_protocol_note', {
|
||||
protocols: hiddenProtocols.map(function(protocol) {
|
||||
return tKey('settings.capability.' + protocol);
|
||||
}).join(', ')
|
||||
});
|
||||
} else {
|
||||
note.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.CrankWizardShell = {
|
||||
TOTAL_STEPS: TOTAL_STEPS,
|
||||
step3PanelId: step3PanelId,
|
||||
loadStep: loadStep,
|
||||
doGoToStep: doGoToStep,
|
||||
goToStep: goToStep,
|
||||
loadWizardPanels: loadWizardPanels,
|
||||
bindProtocolCards: bindProtocolCards,
|
||||
applyEditionProtocolVisibility: applyEditionProtocolVisibility,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,124 @@
|
||||
(function() {
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
currentStep: 1,
|
||||
wizardProtocol: 'rest',
|
||||
wizardMode: 'create',
|
||||
wizardEditId: null,
|
||||
wizardWorkspaceId: null,
|
||||
wizardCurrentOperation: null,
|
||||
wizardCurrentVersion: null,
|
||||
wizardTestResponsePreview: null,
|
||||
wizardProtocolCapabilities: null,
|
||||
wizardEditionCapabilities: null,
|
||||
wizardSecrets: [],
|
||||
wizardAuthProfiles: [],
|
||||
selectedUpstreamId: null,
|
||||
editingUpstreamId: null,
|
||||
protoParsed: null,
|
||||
selectedRpcMethod: null,
|
||||
};
|
||||
|
||||
Object.keys(defaults).forEach(function(key) {
|
||||
if (typeof window[key] !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
var value = defaults[key];
|
||||
if (Array.isArray(value)) {
|
||||
window[key] = value.slice();
|
||||
return;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
window[key] = Object.assign({}, value);
|
||||
return;
|
||||
}
|
||||
window[key] = value;
|
||||
});
|
||||
|
||||
function defaultProtocolCapabilities() {
|
||||
return {
|
||||
rest: {
|
||||
supports_execution_modes: ['unary'],
|
||||
supports_transport_behaviors: ['request_response'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function defaultEditionCapabilities() {
|
||||
return {
|
||||
edition: 'community',
|
||||
supported_protocols: ['rest'],
|
||||
supported_security_levels: ['standard'],
|
||||
machine_access_modes: ['static_agent_key'],
|
||||
limits: {
|
||||
max_workspaces: 1,
|
||||
max_users_per_workspace: 1,
|
||||
max_agents_per_workspace: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function currentProtocolCapabilities() {
|
||||
var capabilities = window.wizardProtocolCapabilities || defaultProtocolCapabilities();
|
||||
return capabilities[window.wizardProtocol] || capabilities.rest;
|
||||
}
|
||||
|
||||
function currentEditionCapabilities() {
|
||||
return window.wizardEditionCapabilities || defaultEditionCapabilities();
|
||||
}
|
||||
|
||||
async function loadProtocolCapabilities() {
|
||||
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
|
||||
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
return window.wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.getProtocolCapabilities(window.wizardWorkspaceId);
|
||||
var mapped = {};
|
||||
(response.items || []).forEach(function(item) {
|
||||
mapped[item.protocol] = item;
|
||||
});
|
||||
window.wizardProtocolCapabilities = mapped;
|
||||
} catch (_error) {
|
||||
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
}
|
||||
|
||||
return window.wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
async function loadEditionCapabilities() {
|
||||
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||
window.wizardEditionCapabilities = defaultEditionCapabilities();
|
||||
return window.wizardEditionCapabilities;
|
||||
}
|
||||
|
||||
try {
|
||||
window.wizardEditionCapabilities = await window.CrankApi.getCapabilities();
|
||||
} catch (_error) {
|
||||
window.wizardEditionCapabilities = defaultEditionCapabilities();
|
||||
}
|
||||
|
||||
return window.wizardEditionCapabilities;
|
||||
}
|
||||
|
||||
function step3Labels() {
|
||||
return {
|
||||
rest: tKey('wizard.step3.rest.label'),
|
||||
};
|
||||
}
|
||||
|
||||
window.CrankWizardState = {
|
||||
state: window,
|
||||
defaultProtocolCapabilities: defaultProtocolCapabilities,
|
||||
defaultEditionCapabilities: defaultEditionCapabilities,
|
||||
currentProtocolCapabilities: currentProtocolCapabilities,
|
||||
currentEditionCapabilities: currentEditionCapabilities,
|
||||
loadProtocolCapabilities: loadProtocolCapabilities,
|
||||
loadEditionCapabilities: loadEditionCapabilities,
|
||||
step3Labels: step3Labels,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,556 @@
|
||||
/* ── Upstream selector (Step 2) ── */
|
||||
|
||||
var upstreams = [
|
||||
{ id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }
|
||||
];
|
||||
|
||||
var dropdownOpen = false;
|
||||
|
||||
function createDivWithClass(className, text) {
|
||||
var element = document.createElement('div');
|
||||
element.className = className;
|
||||
element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
function authProfileById(authProfileId) {
|
||||
return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null;
|
||||
}
|
||||
|
||||
function syncUpstreamAuthMetadata(upstream) {
|
||||
if (!upstream) return upstream;
|
||||
if (!upstream.authProfileId) {
|
||||
upstream.authProfileName = '';
|
||||
upstream.authKind = null;
|
||||
return upstream;
|
||||
}
|
||||
|
||||
var profile = authProfileById(upstream.authProfileId);
|
||||
if (profile) {
|
||||
upstream.authProfileName = profile.name;
|
||||
upstream.authKind = profile.kind;
|
||||
}
|
||||
return upstream;
|
||||
}
|
||||
|
||||
function inferUpstreamAuth(upstream) {
|
||||
syncUpstreamAuthMetadata(upstream);
|
||||
if (!upstream || !upstream.authProfileId || !upstream.authKind) {
|
||||
return { kind: 'none', label: tKey('wizard.step2.auth_none') };
|
||||
}
|
||||
if (upstream.authKind === 'bearer') {
|
||||
return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') };
|
||||
}
|
||||
if (upstream.authKind === 'basic') {
|
||||
return { kind: 'basic', label: tKey('wizard.step2.auth_basic') };
|
||||
}
|
||||
return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') };
|
||||
}
|
||||
|
||||
function upstreamMeta(upstream) {
|
||||
var auth = inferUpstreamAuth(upstream);
|
||||
return {
|
||||
kind: auth.kind,
|
||||
label: auth.label,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadWizardAuthResources() {
|
||||
if (!wizardWorkspaceId || !window.CrankApi) {
|
||||
wizardSecrets = [];
|
||||
wizardAuthProfiles = [];
|
||||
renderAuthProfileOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var results = await Promise.all([
|
||||
window.CrankApi.listSecrets(wizardWorkspaceId),
|
||||
window.CrankApi.listAuthProfiles(wizardWorkspaceId),
|
||||
]);
|
||||
wizardSecrets = (results[0] && results[0].items) || [];
|
||||
wizardAuthProfiles = (results[1] && results[1].items) || [];
|
||||
} catch (_error) {
|
||||
wizardSecrets = [];
|
||||
wizardAuthProfiles = [];
|
||||
}
|
||||
|
||||
upstreams.forEach(syncUpstreamAuthMetadata);
|
||||
renderAuthProfileOptions();
|
||||
}
|
||||
|
||||
function renderAuthProfileOptions() {
|
||||
var profileSelect = document.getElementById('new-upstream-auth-profile');
|
||||
var secretSelect = document.getElementById('new-auth-profile-secret-id');
|
||||
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
|
||||
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
|
||||
var profileHint = document.getElementById('new-upstream-auth-profile-hint');
|
||||
|
||||
if (profileSelect) {
|
||||
var previousProfile = profileSelect.value;
|
||||
profileSelect.innerHTML = '';
|
||||
if (!wizardAuthProfiles.length) {
|
||||
var emptyProfile = document.createElement('option');
|
||||
emptyProfile.value = '';
|
||||
emptyProfile.textContent = tKey('wizard.step2.auth_profile_empty');
|
||||
profileSelect.appendChild(emptyProfile);
|
||||
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_empty');
|
||||
} else {
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = tKey('wizard.step2.auth_profile');
|
||||
profileSelect.appendChild(placeholder);
|
||||
wizardAuthProfiles.forEach(function(profile) {
|
||||
var option = document.createElement('option');
|
||||
option.value = profile.id;
|
||||
option.textContent = profile.name + ' · ' + profile.kind;
|
||||
profileSelect.appendChild(option);
|
||||
});
|
||||
if (previousProfile) {
|
||||
profileSelect.value = previousProfile;
|
||||
}
|
||||
if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_hint');
|
||||
}
|
||||
}
|
||||
|
||||
[secretSelect, usernameSelect, passwordSelect].forEach(function(select) {
|
||||
if (!select) return;
|
||||
var previous = select.value;
|
||||
select.innerHTML = '';
|
||||
var placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = tKey('wizard.step2.secret_value');
|
||||
select.appendChild(placeholder);
|
||||
wizardSecrets.forEach(function(secret) {
|
||||
var option = document.createElement('option');
|
||||
option.value = secret.id;
|
||||
option.textContent = secret.name + ' · ' + secret.kind;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if (previous) {
|
||||
select.value = previous;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuthProfileCreateUi() {
|
||||
var kind = textValue('new-auth-profile-kind') || 'bearer';
|
||||
var headerGroup = document.getElementById('auth-profile-header-name-group');
|
||||
var queryGroup = document.getElementById('auth-profile-query-param-group');
|
||||
var basicRow = document.getElementById('auth-profile-basic-secret-row');
|
||||
var singleSecretGroup = document.getElementById('auth-profile-single-secret-group');
|
||||
|
||||
if (headerGroup) {
|
||||
headerGroup.hidden = !(kind === 'bearer' || kind === 'api_key_header');
|
||||
}
|
||||
if (queryGroup) {
|
||||
queryGroup.hidden = kind !== 'api_key_query';
|
||||
}
|
||||
if (basicRow) {
|
||||
basicRow.hidden = kind !== 'basic';
|
||||
}
|
||||
if (singleSecretGroup) {
|
||||
singleSecretGroup.hidden = kind === 'basic';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUpstreamAuthUi() {
|
||||
var mode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var existingGroup = document.getElementById('upstream-auth-existing-group');
|
||||
var createGroup = document.getElementById('upstream-auth-create-group');
|
||||
if (existingGroup) existingGroup.hidden = mode !== 'existing';
|
||||
if (createGroup) createGroup.hidden = mode !== 'create';
|
||||
updateAuthProfileCreateUi();
|
||||
}
|
||||
|
||||
function buildUpstreamTriggerContent(name, url) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(createDivWithClass('upstream-trigger-name', name));
|
||||
fragment.appendChild(createDivWithClass('upstream-trigger-url', url));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function setUpstreamComboboxPlaceholder(container) {
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
var placeholder = document.createElement('span');
|
||||
placeholder.className = 'upstream-combobox-placeholder';
|
||||
placeholder.textContent = tKey('wizard.step2.upstream_placeholder');
|
||||
container.appendChild(placeholder);
|
||||
}
|
||||
|
||||
function renderDropdownList(filter) {
|
||||
var list = document.getElementById('upstream-dropdown-list');
|
||||
if (!list) return;
|
||||
var q = (filter || '').toLowerCase();
|
||||
var filtered = upstreams.filter(function(u) {
|
||||
return !q || u.name.toLowerCase().includes(q) || u.url.toLowerCase().includes(q);
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '';
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'upstream-dropdown-empty';
|
||||
empty.textContent = tfKey('agents.drawer.ops_no_match', { query: filter || '' });
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var tmpl = document.getElementById('tmpl-upstream-item');
|
||||
list.innerHTML = '';
|
||||
filtered.forEach(function(u) {
|
||||
var sel = u.id === selectedUpstreamId;
|
||||
var meta = upstreamMeta(u);
|
||||
var node = tmpl.content.cloneNode(true);
|
||||
var item = node.querySelector('.upstream-dropdown-item');
|
||||
if (sel) item.classList.add('selected');
|
||||
item.addEventListener('click', function() { pickUpstream(u.id); });
|
||||
node.querySelector('.upstream-dropdown-item-name').textContent = u.name;
|
||||
node.querySelector('.upstream-dropdown-item-url').textContent = u.url;
|
||||
var badge = node.querySelector('.upstream-auth-badge');
|
||||
badge.textContent = meta.label;
|
||||
badge.className = 'upstream-auth-badge auth-' + meta.kind;
|
||||
list.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function openUpstreamDropdown() {
|
||||
var combobox = document.getElementById('upstream-combobox');
|
||||
var dropdown = document.getElementById('upstream-dropdown');
|
||||
var search = document.getElementById('upstream-search');
|
||||
if (!combobox || !dropdown) return;
|
||||
combobox.classList.add('open');
|
||||
dropdown.hidden = false;
|
||||
dropdownOpen = true;
|
||||
renderDropdownList('');
|
||||
if (search) { search.value = ''; search.focus(); }
|
||||
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.hidden = true;
|
||||
}
|
||||
|
||||
function closeUpstreamDropdown() {
|
||||
var combobox = document.getElementById('upstream-combobox');
|
||||
var dropdown = document.getElementById('upstream-dropdown');
|
||||
if (!combobox || !dropdown) return;
|
||||
combobox.classList.remove('open');
|
||||
dropdown.hidden = true;
|
||||
dropdownOpen = false;
|
||||
}
|
||||
|
||||
function toggleUpstreamDropdown(e) {
|
||||
e.stopPropagation();
|
||||
if (dropdownOpen) { closeUpstreamDropdown(); } else { openUpstreamDropdown(); }
|
||||
}
|
||||
|
||||
function filterUpstreams(val) {
|
||||
renderDropdownList(val);
|
||||
}
|
||||
|
||||
function pickUpstream(id) {
|
||||
selectedUpstreamId = id;
|
||||
editingUpstreamId = null;
|
||||
closeUpstreamDropdown();
|
||||
|
||||
var u = upstreams.find(function(x) { return x.id === id; });
|
||||
if (!u) return;
|
||||
|
||||
var val = document.getElementById('upstream-combobox-value');
|
||||
if (val) {
|
||||
var trigTmpl = document.getElementById('tmpl-upstream-trigger-value');
|
||||
if (trigTmpl) {
|
||||
val.innerHTML = '';
|
||||
var trigNode = trigTmpl.content.cloneNode(true);
|
||||
trigNode.querySelector('.upstream-trigger-name').textContent = u.name;
|
||||
trigNode.querySelector('.upstream-trigger-url').textContent = u.url;
|
||||
val.appendChild(trigNode);
|
||||
} else {
|
||||
val.innerHTML = '';
|
||||
val.appendChild(buildUpstreamTriggerContent(u.name, u.url));
|
||||
}
|
||||
}
|
||||
|
||||
var preview = document.getElementById('upstream-preview');
|
||||
var pName = document.getElementById('upstream-preview-name');
|
||||
var pUrl = document.getElementById('upstream-preview-url');
|
||||
var pBadge = document.getElementById('upstream-preview-badge');
|
||||
if (preview) {
|
||||
var meta = upstreamMeta(u);
|
||||
pName.textContent = u.name;
|
||||
pUrl.textContent = u.url;
|
||||
pBadge.textContent = meta.label;
|
||||
pBadge.className = 'upstream-auth-badge auth-' + meta.kind;
|
||||
preview.hidden = false;
|
||||
}
|
||||
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.hidden = true;
|
||||
var trigger = document.getElementById('upstream-new-trigger');
|
||||
if (trigger) trigger.classList.remove('active');
|
||||
|
||||
}
|
||||
|
||||
function startNewUpstream() {
|
||||
closeUpstreamDropdown();
|
||||
editingUpstreamId = null;
|
||||
var trigger = document.getElementById('upstream-new-trigger');
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
var isOpen = form && !form.hidden;
|
||||
|
||||
if (isOpen) {
|
||||
if (form) form.hidden = true;
|
||||
if (trigger) trigger.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
var val = document.getElementById('upstream-combobox-value');
|
||||
if (val) setUpstreamComboboxPlaceholder(val);
|
||||
var preview = document.getElementById('upstream-preview');
|
||||
if (preview) preview.hidden = true;
|
||||
selectedUpstreamId = null;
|
||||
|
||||
if (trigger) trigger.classList.add('active');
|
||||
if (form) {
|
||||
form.hidden = false;
|
||||
setValue('new-upstream-name', '');
|
||||
setValue('new-upstream-url', '');
|
||||
setValue('new-upstream-static-headers', '{\n}');
|
||||
setValue('new-upstream-auth-mode', 'none');
|
||||
setValue('new-upstream-auth-profile', '');
|
||||
setValue('new-auth-profile-name', '');
|
||||
setValue('new-auth-profile-kind', 'bearer');
|
||||
setValue('new-auth-profile-header-name', 'Authorization');
|
||||
setValue('new-auth-profile-query-param', 'api_key');
|
||||
setValue('new-auth-profile-secret-id', '');
|
||||
setValue('new-auth-profile-username-secret', '');
|
||||
setValue('new-auth-profile-password-secret', '');
|
||||
updateUpstreamAuthUi();
|
||||
var first = form.querySelector('input');
|
||||
if (first) setTimeout(function() { first.focus(); }, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function beginEditSelectedUpstream(e) {
|
||||
if (e) e.stopPropagation();
|
||||
var upstream = upstreams.find(function(item) { return item.id === selectedUpstreamId; });
|
||||
if (!upstream) return;
|
||||
|
||||
closeUpstreamDropdown();
|
||||
editingUpstreamId = upstream.id;
|
||||
|
||||
var trigger = document.getElementById('upstream-new-trigger');
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
var preview = document.getElementById('upstream-preview');
|
||||
if (trigger) trigger.classList.add('active');
|
||||
if (preview) preview.hidden = true;
|
||||
if (form) {
|
||||
form.hidden = false;
|
||||
setValue('new-upstream-name', upstream.name);
|
||||
setValue('new-upstream-url', upstream.url);
|
||||
setValue('new-upstream-static-headers', upstream.staticHeaders || '{\n}');
|
||||
setValue('new-upstream-auth-profile', upstream.authProfileId || '');
|
||||
setValue('new-upstream-auth-mode', upstream.authProfileId ? 'existing' : 'none');
|
||||
updateUpstreamAuthUi();
|
||||
}
|
||||
}
|
||||
|
||||
function buildAuthProfileConfig(kind) {
|
||||
if (kind === 'bearer') {
|
||||
var bearerHeader = textValue('new-auth-profile-header-name') || 'Authorization';
|
||||
var bearerSecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!bearerHeader) throw new Error(tKey('wizard.error.header_name_required'));
|
||||
if (!bearerSecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
bearer: {
|
||||
header_name: bearerHeader,
|
||||
secret_id: bearerSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (kind === 'basic') {
|
||||
var usernameSecretId = textValue('new-auth-profile-username-secret');
|
||||
var passwordSecretId = textValue('new-auth-profile-password-secret');
|
||||
if (!usernameSecretId || !passwordSecretId) throw new Error(tKey('wizard.error.basic_secrets_required'));
|
||||
return {
|
||||
basic: {
|
||||
username_secret_id: usernameSecretId,
|
||||
password_secret_id: passwordSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (kind === 'api_key_header') {
|
||||
var headerName = textValue('new-auth-profile-header-name');
|
||||
var headerSecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!headerName) throw new Error(tKey('wizard.error.header_name_required'));
|
||||
if (!headerSecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
api_key_header: {
|
||||
header_name: headerName,
|
||||
secret_id: headerSecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var queryParam = textValue('new-auth-profile-query-param');
|
||||
var querySecretId = textValue('new-auth-profile-secret-id');
|
||||
if (!queryParam) throw new Error(tKey('wizard.error.query_param_required'));
|
||||
if (!querySecretId) throw new Error(tKey('wizard.error.secret_required'));
|
||||
return {
|
||||
api_key_query: {
|
||||
param_name: queryParam,
|
||||
secret_id: querySecretId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function createAuthProfileFromForm() {
|
||||
var name = textValue('new-auth-profile-name');
|
||||
var kind = textValue('new-auth-profile-kind') || 'bearer';
|
||||
if (!name) throw new Error(tKey('wizard.error.auth_profile_name'));
|
||||
|
||||
var profile = await window.CrankApi.createAuthProfile(wizardWorkspaceId, {
|
||||
name: name,
|
||||
kind: kind,
|
||||
config: buildAuthProfileConfig(kind),
|
||||
});
|
||||
wizardAuthProfiles.push(profile);
|
||||
renderAuthProfileOptions();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('wizard.toast.auth_profile_created_body'),
|
||||
tKey('wizard.toast.auth_profile_created_title')
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
async function saveNewUpstream(e) {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
var nameEl = document.getElementById('new-upstream-name');
|
||||
var urlEl = document.getElementById('new-upstream-url');
|
||||
var name = nameEl ? nameEl.value.trim() : '';
|
||||
var url = urlEl ? urlEl.value.trim() : '';
|
||||
if (!name) { if (nameEl) nameEl.focus(); return; }
|
||||
if (!url) { if (urlEl) urlEl.focus(); return; }
|
||||
|
||||
var staticHeadersEl = document.getElementById('new-upstream-static-headers');
|
||||
var staticHeaders = staticHeadersEl ? staticHeadersEl.value : '{\n}';
|
||||
parseHeaderMap(staticHeaders);
|
||||
|
||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||
var authProfileId = null;
|
||||
var authProfileName = '';
|
||||
var authKind = null;
|
||||
|
||||
if (authMode === 'existing') {
|
||||
authProfileId = textValue('new-upstream-auth-profile');
|
||||
if (!authProfileId) throw new Error(tKey('wizard.error.auth_profile_required'));
|
||||
var existingProfile = authProfileById(authProfileId);
|
||||
authProfileName = existingProfile ? existingProfile.name : '';
|
||||
authKind = existingProfile ? existingProfile.kind : null;
|
||||
} else if (authMode === 'create') {
|
||||
var createdProfile = await createAuthProfileFromForm();
|
||||
authProfileId = createdProfile.id;
|
||||
authProfileName = createdProfile.name;
|
||||
authKind = createdProfile.kind;
|
||||
setValue('new-upstream-auth-profile', createdProfile.id);
|
||||
setValue('new-upstream-auth-mode', 'existing');
|
||||
}
|
||||
var nextRecord = {
|
||||
id: editingUpstreamId || ('custom-' + Date.now()),
|
||||
name: name,
|
||||
url: url,
|
||||
staticHeaders: staticHeaders,
|
||||
authProfileId: authProfileId,
|
||||
authProfileName: authProfileName,
|
||||
authKind: authKind,
|
||||
};
|
||||
|
||||
if (editingUpstreamId) {
|
||||
upstreams = upstreams.map(function(item) {
|
||||
return item.id === editingUpstreamId ? nextRecord : item;
|
||||
});
|
||||
} else {
|
||||
upstreams.push(nextRecord);
|
||||
}
|
||||
|
||||
if (nameEl) nameEl.value = '';
|
||||
if (urlEl) urlEl.value = '';
|
||||
if (staticHeadersEl) staticHeadersEl.value = '{\n}';
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.hidden = true;
|
||||
editingUpstreamId = null;
|
||||
pickUpstream(nextRecord.id);
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('wizard.toast.auth_profile_error_title'),
|
||||
tKey('wizard.toast.auth_profile_error_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewUpstream(e) {
|
||||
e.stopPropagation();
|
||||
var form = document.getElementById('upstream-new-form');
|
||||
if (form) form.hidden = true;
|
||||
var trigger = document.getElementById('upstream-new-trigger');
|
||||
if (trigger) trigger.classList.remove('active');
|
||||
editingUpstreamId = null;
|
||||
}
|
||||
|
||||
function openQuickSecretModal(e) {
|
||||
if (e) e.preventDefault();
|
||||
var modal = document.getElementById('quick-secret-modal');
|
||||
if (!modal) return;
|
||||
setValue('quick-secret-name', '');
|
||||
setValue('quick-secret-value', '');
|
||||
setValue('quick-secret-kind', 'token');
|
||||
modal.classList.add('open');
|
||||
setTimeout(function() {
|
||||
var input = document.getElementById('quick-secret-name');
|
||||
if (input) input.focus();
|
||||
}, 30);
|
||||
}
|
||||
|
||||
function closeQuickSecretModal() {
|
||||
var modal = document.getElementById('quick-secret-modal');
|
||||
if (modal) modal.classList.remove('open');
|
||||
}
|
||||
|
||||
async function submitQuickSecret() {
|
||||
var name = textValue('quick-secret-name');
|
||||
var value = textValue('quick-secret-value');
|
||||
var kind = textValue('quick-secret-kind') || 'token';
|
||||
if (!name) throw new Error(tKey('wizard.error.secret_name_required'));
|
||||
if (!value) throw new Error(tKey('wizard.error.secret_value_required'));
|
||||
|
||||
var secret = await window.CrankApi.createSecret(wizardWorkspaceId, {
|
||||
name: name,
|
||||
kind: kind,
|
||||
value: value,
|
||||
});
|
||||
wizardSecrets.push(secret);
|
||||
renderAuthProfileOptions();
|
||||
var primarySelect = document.getElementById('new-auth-profile-secret-id');
|
||||
var usernameSelect = document.getElementById('new-auth-profile-username-secret');
|
||||
var passwordSelect = document.getElementById('new-auth-profile-password-secret');
|
||||
if (primarySelect && primarySelect.offsetParent !== null) {
|
||||
primarySelect.value = secret.id;
|
||||
} else if (usernameSelect && !usernameSelect.value) {
|
||||
usernameSelect.value = secret.id;
|
||||
} else if (passwordSelect) {
|
||||
passwordSelect.value = secret.id;
|
||||
}
|
||||
closeQuickSecretModal();
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(
|
||||
tKey('wizard.toast.quick_secret_created_body'),
|
||||
tKey('wizard.toast.quick_secret_created_title')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
var TOTAL_STEPS = window.CrankWizardShell.TOTAL_STEPS;
|
||||
var loadProtocolCapabilities = window.CrankWizardState.loadProtocolCapabilities;
|
||||
var loadEditionCapabilities = window.CrankWizardState.loadEditionCapabilities;
|
||||
var currentProtocolCapabilities = window.CrankWizardState.currentProtocolCapabilities;
|
||||
var loadStep = window.CrankWizardShell.loadStep;
|
||||
var goToStep = window.CrankWizardShell.goToStep;
|
||||
var _doGoToStep = window.CrankWizardShell.doGoToStep;
|
||||
var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
|
||||
var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
|
||||
var applyEditionProtocolVisibility = window.CrankWizardShell.applyEditionProtocolVisibility;
|
||||
var step3PanelId = window.CrankWizardShell.step3PanelId;
|
||||
var saveOperation = window.CrankWizardLive.saveOperation;
|
||||
var loadOperationForEdit = window.CrankWizardLive.loadOperationForEdit;
|
||||
var bindWizardLiveActions = window.CrankWizardLive.bindWizardLiveActions;
|
||||
var updateWizardProtocolVisibility = window.CrankWizardLive.updateWizardProtocolVisibility;
|
||||
var currentStep = window.currentStep;
|
||||
var wizardProtocol = window.wizardProtocol;
|
||||
var wizardMode = window.wizardMode;
|
||||
var wizardEditId = window.wizardEditId;
|
||||
var wizardWorkspaceId = window.wizardWorkspaceId;
|
||||
var wizardCurrentOperation = window.wizardCurrentOperation;
|
||||
var wizardCurrentVersion = window.wizardCurrentVersion;
|
||||
var wizardProtoUpload = window.wizardProtoUpload;
|
||||
var wizardDescriptorSetUpload = window.wizardDescriptorSetUpload;
|
||||
var wizardSoapWsdlUpload = window.wizardSoapWsdlUpload;
|
||||
var wizardSoapXsdUpload = window.wizardSoapXsdUpload;
|
||||
var wizardTestResponsePreview = window.wizardTestResponsePreview;
|
||||
var wizardProtocolCapabilities = window.wizardProtocolCapabilities;
|
||||
var wizardSecrets = window.wizardSecrets;
|
||||
var wizardAuthProfiles = window.wizardAuthProfiles;
|
||||
var selectedUpstreamId = window.selectedUpstreamId;
|
||||
var editingUpstreamId = window.editingUpstreamId;
|
||||
var protoParsed = window.protoParsed;
|
||||
var selectedRpcMethod = window.selectedRpcMethod;
|
||||
|
||||
async function initWizardPage() {
|
||||
renderSidebarBrand('create');
|
||||
document.querySelector('.btn-continue').addEventListener('click', function() {
|
||||
if (currentStep < TOTAL_STEPS) {
|
||||
goToStep(currentStep + 1);
|
||||
} else {
|
||||
saveOperation();
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.btn-back').addEventListener('click', function() {
|
||||
if (!this.disabled && currentStep > 1) goToStep(currentStep - 1);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.step-item').forEach(function(item, i) {
|
||||
item.addEventListener('click', function() { goToStep(i + 1); });
|
||||
});
|
||||
|
||||
var backToCatalog = document.getElementById('back-to-catalog');
|
||||
if (backToCatalog) {
|
||||
backToCatalog.addEventListener('click', function() {
|
||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/';
|
||||
});
|
||||
}
|
||||
|
||||
var closeBtn = document.querySelector('.progress-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/';
|
||||
});
|
||||
}
|
||||
|
||||
var saveDraftBtn = document.querySelector('.btn-save-draft');
|
||||
if (saveDraftBtn) {
|
||||
saveDraftBtn.addEventListener('click', function() {
|
||||
saveOperation(true);
|
||||
});
|
||||
}
|
||||
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
wizardWorkspaceId = workspace ? workspace.id : null;
|
||||
window.wizardWorkspaceId = wizardWorkspaceId;
|
||||
var editionCapabilities = await loadEditionCapabilities();
|
||||
await loadProtocolCapabilities();
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
if (window.CrankOverlay && typeof window.CrankOverlay.render === 'function') {
|
||||
await window.CrankOverlay.render(document, {
|
||||
workspace: workspace,
|
||||
capabilities: editionCapabilities,
|
||||
locale: localStorage.getItem('crank_lang') || 'en',
|
||||
});
|
||||
}
|
||||
await loadWizardAuthResources();
|
||||
bindProtocolCards();
|
||||
applyEditionProtocolVisibility(editionCapabilities);
|
||||
bindWizardLiveActions();
|
||||
updateUpstreamAuthUi();
|
||||
renderEditionCapabilityHints(editionCapabilities);
|
||||
|
||||
var quickSecretModal = document.getElementById('quick-secret-modal');
|
||||
var quickSecretClose = document.getElementById('quick-secret-close-btn');
|
||||
var quickSecretCancel = document.getElementById('quick-secret-cancel-btn');
|
||||
var quickSecretSubmit = document.getElementById('quick-secret-submit-btn');
|
||||
if (quickSecretClose) quickSecretClose.addEventListener('click', closeQuickSecretModal);
|
||||
if (quickSecretCancel) quickSecretCancel.addEventListener('click', closeQuickSecretModal);
|
||||
if (quickSecretModal) {
|
||||
quickSecretModal.addEventListener('click', function(event) {
|
||||
if (event.target === quickSecretModal) closeQuickSecretModal();
|
||||
});
|
||||
}
|
||||
if (quickSecretSubmit) {
|
||||
quickSecretSubmit.addEventListener('click', async function() {
|
||||
var button = quickSecretSubmit;
|
||||
var original = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = tKey('wizard.step2.quick_secret_submit');
|
||||
try {
|
||||
await submitQuickSecret();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(
|
||||
error.message || tKey('wizard.toast.quick_secret_error_title'),
|
||||
tKey('wizard.toast.quick_secret_error_title')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
||||
wizardMode = 'edit';
|
||||
wizardEditId = params.get('operationId');
|
||||
window.wizardMode = wizardMode;
|
||||
window.wizardEditId = wizardEditId;
|
||||
document.title = 'Crank — ' + tKey('wizard.progress.edit');
|
||||
await loadOperationForEdit();
|
||||
}
|
||||
|
||||
updateWizardProtocolVisibility();
|
||||
_doGoToStep(1);
|
||||
}
|
||||
|
||||
function renderEditionCapabilityHints(capabilities) {
|
||||
var securityNote = document.getElementById('wizard-security-level-note');
|
||||
var securityText = securityNote ? securityNote.querySelector('.info-callout-text') : null;
|
||||
if (!securityText) return;
|
||||
var levels = capabilities && Array.isArray(capabilities.supported_security_levels)
|
||||
? capabilities.supported_security_levels
|
||||
: ['standard'];
|
||||
securityNote.hidden = !(levels.length === 1 && levels[0] === 'standard');
|
||||
if (!securityNote.hidden) {
|
||||
securityText.textContent = tKey('wizard.step5.community_security_note');
|
||||
}
|
||||
}
|
||||
|
||||
window.renderEditionCapabilityHints = renderEditionCapabilityHints;
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
void initWizardPage();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* ── HTTP method picker (Step 4 REST) ── */
|
||||
|
||||
var METHOD_CALLOUTS = {
|
||||
GET: null,
|
||||
DELETE: null,
|
||||
POST: { icon: 'info', text: { en: { title: 'POST selected — body encoding required.', body: 'In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.' }, ru: { title: 'Выбран POST — требуется кодирование тела запроса.', body: 'На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } } },
|
||||
PUT: { icon: 'info', text: { en: { title: 'PUT selected — full-resource replacement.', body: 'The request body must contain a complete resource representation. Define the body schema in step 4.' }, ru: { title: 'Выбран PUT — полная замена ресурса.', body: 'Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } } },
|
||||
PATCH: { icon: 'info', text: { en: { title: 'PATCH selected — partial update.', body: 'Only include the fields you want to modify in the body schema defined in step 4.' }, ru: { title: 'Выбран PATCH — частичное обновление.', body: 'В body schema на шаге 4 включайте только те поля, которые хотите изменить.' } } },
|
||||
};
|
||||
|
||||
function renderSidebarBrand(mode) {
|
||||
var sidebarBrand = document.querySelector('.step-sidebar-brand');
|
||||
if (!sidebarBrand) return;
|
||||
var language = localStorage.getItem('crank_lang') || 'en';
|
||||
var parts;
|
||||
if (mode === 'edit') {
|
||||
parts = language === 'ru'
|
||||
? { prefix: 'Редактировать ', suffix: 'операцию' }
|
||||
: { prefix: 'Edit ', suffix: 'operation' };
|
||||
} else {
|
||||
parts = language === 'ru'
|
||||
? { prefix: 'Создать ', suffix: 'операцию' }
|
||||
: { prefix: 'Create ', suffix: 'operation' };
|
||||
}
|
||||
sidebarBrand.dataset.wizardBrand = mode;
|
||||
sidebarBrand.textContent = parts.prefix;
|
||||
var accent = document.createElement('span');
|
||||
accent.textContent = parts.suffix;
|
||||
sidebarBrand.appendChild(accent);
|
||||
}
|
||||
|
||||
window.addEventListener('crank:langchange', function() {
|
||||
renderSidebarBrand(wizardMode === 'edit' ? 'edit' : 'create');
|
||||
});
|
||||
|
||||
function selectMethod(btn) {
|
||||
document.querySelectorAll('.method-card').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
var method = btn.dataset.method;
|
||||
var callout = document.getElementById('method-callout-rest');
|
||||
if (!callout) return;
|
||||
var info = METHOD_CALLOUTS[method];
|
||||
if (info) {
|
||||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||||
var text = typeof info.text === 'string' ? info.text : (info.text[lang] || info.text.en);
|
||||
callout.innerHTML = '';
|
||||
var icon = buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/info-circle.svg#icon',
|
||||
15,
|
||||
15
|
||||
);
|
||||
icon.style.flexShrink = '0';
|
||||
icon.style.color = 'var(--accent)';
|
||||
callout.appendChild(icon);
|
||||
|
||||
var content = document.createElement('span');
|
||||
var title = document.createElement('strong');
|
||||
title.textContent = text.title;
|
||||
content.appendChild(title);
|
||||
content.appendChild(document.createTextNode(' ' + text.body));
|
||||
callout.appendChild(content);
|
||||
callout.hidden = false;
|
||||
} else {
|
||||
callout.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── GraphQL operation type picker (Step 4 GraphQL) ── */
|
||||
|
||||
function selectGqlType(btn) {
|
||||
document.querySelectorAll('.gql-type-card').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
// Update query editor placeholder if it's still the default
|
||||
var editor = document.getElementById('gql-query-editor');
|
||||
if (!editor) return;
|
||||
var type = btn.dataset.gqlType;
|
||||
var defaults = {
|
||||
query: 'query GetContact($id: ID!) {\n contact(id: $id) {\n id\n firstName\n lastName\n email\n company {\n id\n name\n }\n createdAt\n }\n}',
|
||||
mutation: 'mutation CreateContact(\n $firstName: String!\n $lastName: String!\n $email: String!\n $company: String\n) {\n createContact(input: {\n firstName: $firstName\n lastName: $lastName\n email: $email\n company: $company\n }) {\n id\n firstName\n lastName\n createdAt\n }\n}'
|
||||
};
|
||||
if (defaults[type]) editor.value = defaults[type];
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', function() {
|
||||
if (dropdownOpen) closeUpstreamDropdown();
|
||||
});
|
||||
|
||||
|
||||
function textValue(id) {
|
||||
var element = document.getElementById(id);
|
||||
return element ? element.value.trim() : '';
|
||||
}
|
||||
|
||||
function setValue(id, value) {
|
||||
var element = document.getElementById(id);
|
||||
if (element) element.value = value || '';
|
||||
}
|
||||
|
||||
function parseStructuredText(text) {
|
||||
if (!text.trim()) return {};
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (_error) {
|
||||
if (!window.jsyaml) throw new Error(tKey('wizard.error.parser_unavailable'));
|
||||
return window.jsyaml.load(text) || {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
var workspaceFormState = {
|
||||
selectedColor: '#0d9488',
|
||||
slugManual: false,
|
||||
isCreateMode: false,
|
||||
formDirty: false,
|
||||
workspaceId: null,
|
||||
workspaceRecord: null,
|
||||
memberships: [],
|
||||
invitations: [],
|
||||
currentUserId: null,
|
||||
};
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function normalizeSlug(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
function uiRoleToApi(value) {
|
||||
if (value === 'developer') {
|
||||
return 'operator';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function roleLabel(value) {
|
||||
if (value === 'operator') return tKey('workspace_setup.role.operator');
|
||||
if (value === 'owner') return tKey('workspace_setup.role.owner');
|
||||
if (value === 'admin') return tKey('workspace_setup.role.admin');
|
||||
if (value === 'viewer') return tKey('workspace_setup.role.viewer');
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function roleClass(value) {
|
||||
if (value === 'operator') {
|
||||
return 'role-developer';
|
||||
}
|
||||
return 'role-' + value;
|
||||
}
|
||||
|
||||
function currentLanguage() {
|
||||
return localStorage.getItem('crank_lang') || 'en';
|
||||
}
|
||||
|
||||
function setFormDirty(value) {
|
||||
workspaceFormState.formDirty = value;
|
||||
}
|
||||
|
||||
function formElements() {
|
||||
return {
|
||||
name: document.getElementById('ws-name'),
|
||||
slug: document.getElementById('ws-slug'),
|
||||
desc: document.getElementById('ws-desc'),
|
||||
protocol: document.getElementById('ws-protocol'),
|
||||
timezone: document.getElementById('ws-timezone'),
|
||||
submit: document.getElementById('submit-btn'),
|
||||
avatar: document.getElementById('ws-avatar-preview'),
|
||||
};
|
||||
}
|
||||
|
||||
function createElement(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) {
|
||||
node.className = className;
|
||||
}
|
||||
if (text !== undefined && text !== null) {
|
||||
node.textContent = text;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function buildIconSvg(href, width, height, viewBox, attrs) {
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', String(width));
|
||||
svg.setAttribute('height', String(height));
|
||||
if (viewBox) {
|
||||
svg.setAttribute('viewBox', viewBox);
|
||||
}
|
||||
if (attrs) {
|
||||
Object.keys(attrs).forEach(function(key) {
|
||||
svg.setAttribute(key, attrs[key]);
|
||||
});
|
||||
}
|
||||
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('href', href);
|
||||
svg.appendChild(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function workspacePayload() {
|
||||
var elements = formElements();
|
||||
return {
|
||||
slug: elements.slug.value.trim(),
|
||||
display_name: elements.name.value.trim(),
|
||||
settings: {
|
||||
description: elements.desc.value.trim(),
|
||||
default_protocol: elements.protocol.value,
|
||||
timezone: elements.timezone.value,
|
||||
language: currentLanguage(),
|
||||
color: workspaceFormState.selectedColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyWorkspaceRecord(record) {
|
||||
var workspace = record.workspace;
|
||||
var settings = workspace.settings || {};
|
||||
var elements = formElements();
|
||||
|
||||
workspaceFormState.workspaceId = workspace.id;
|
||||
workspaceFormState.workspaceRecord = record;
|
||||
workspaceFormState.selectedColor = settings.color || '#0d9488';
|
||||
workspaceFormState.slugManual = true;
|
||||
|
||||
elements.name.value = workspace.display_name || '';
|
||||
elements.slug.value = workspace.slug || '';
|
||||
elements.desc.value = settings.description || '';
|
||||
elements.protocol.value = settings.default_protocol || 'rest';
|
||||
elements.timezone.value = settings.timezone || 'UTC';
|
||||
elements.avatar.textContent = (workspace.display_name || workspace.slug || '?').charAt(0).toUpperCase();
|
||||
elements.avatar.style.background = workspaceFormState.selectedColor;
|
||||
|
||||
document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) {
|
||||
swatch.classList.toggle('active', swatch.dataset.color === workspaceFormState.selectedColor);
|
||||
});
|
||||
|
||||
setFormDirty(false);
|
||||
}
|
||||
|
||||
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';
|
||||
var avatar = createElement(
|
||||
'div',
|
||||
'member-avatar',
|
||||
membership.user.display_name.slice(0, 2).toUpperCase()
|
||||
);
|
||||
avatar.style.background =
|
||||
'linear-gradient(135deg,' + workspaceFormState.selectedColor + ',#6366f1)';
|
||||
row.appendChild(avatar);
|
||||
|
||||
var info = createElement('div', 'member-info');
|
||||
var name = createElement('div', 'member-name', membership.user.display_name);
|
||||
if (isCurrentUser) {
|
||||
name.appendChild(document.createTextNode(' '));
|
||||
var currentUserLabel = createElement('span', null, '(you)');
|
||||
currentUserLabel.style.fontSize = '11px';
|
||||
currentUserLabel.style.fontWeight = '400';
|
||||
currentUserLabel.style.color = 'var(--text-muted)';
|
||||
name.appendChild(currentUserLabel);
|
||||
}
|
||||
info.appendChild(name);
|
||||
info.appendChild(createElement('div', 'member-email', membership.user.email));
|
||||
row.appendChild(info);
|
||||
|
||||
row.appendChild(createElement('div', 'member-last-active', '—'));
|
||||
|
||||
if (canManage) {
|
||||
var select = createElement('select', 'member-role-select');
|
||||
select.dataset.previous = membership.role;
|
||||
[
|
||||
['owner', tKey('workspace_setup.role.owner')],
|
||||
['admin', tKey('workspace_setup.role.admin')],
|
||||
['operator', tKey('workspace_setup.role.operator')],
|
||||
['viewer', tKey('workspace_setup.role.viewer')]
|
||||
].forEach(function(optionData) {
|
||||
var option = document.createElement('option');
|
||||
option.value = optionData[0];
|
||||
option.textContent = optionData[1];
|
||||
option.selected = membership.role === optionData[0];
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', function() {
|
||||
updateRole(select, membership.user.id);
|
||||
});
|
||||
row.appendChild(select);
|
||||
|
||||
var removeButton = createElement('button', 'member-remove-btn');
|
||||
removeButton.type = 'button';
|
||||
removeButton.title = tKey('workspace_setup.members.remove');
|
||||
removeButton.appendChild(
|
||||
buildIconSvg(
|
||||
'',
|
||||
12,
|
||||
12,
|
||||
'0 0 16 16',
|
||||
{
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '1.8',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round'
|
||||
}
|
||||
)
|
||||
);
|
||||
var removeSvg = removeButton.querySelector('svg');
|
||||
var pathA = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
pathA.setAttribute('d', 'M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4');
|
||||
var pathB = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
pathB.setAttribute('d', 'M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9');
|
||||
removeSvg.appendChild(pathA);
|
||||
removeSvg.appendChild(pathB);
|
||||
removeButton.addEventListener('click', function() {
|
||||
removeMember(membership.user.id, membership.user.display_name);
|
||||
});
|
||||
row.appendChild(removeButton);
|
||||
} else {
|
||||
row.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
'member-role-badge ' + roleClass(membership.role),
|
||||
roleLabel(membership.role)
|
||||
)
|
||||
);
|
||||
}
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderMembers() {
|
||||
var container = document.getElementById('members-list');
|
||||
var label = document.getElementById('members-count-label');
|
||||
if (!container || !label) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
workspaceFormState.memberships.forEach(function (membership) {
|
||||
addMemberRow(container, membership);
|
||||
});
|
||||
label.textContent = window.tPlural(
|
||||
'workspace_setup.member_count',
|
||||
workspaceFormState.memberships.length,
|
||||
{ count: workspaceFormState.memberships.length }
|
||||
);
|
||||
}
|
||||
|
||||
function renderInvitations() {
|
||||
var pending = document.getElementById('pending-invites');
|
||||
var count = document.getElementById('pending-count');
|
||||
if (!pending || !count) {
|
||||
return;
|
||||
}
|
||||
|
||||
var items = workspaceFormState.invitations.filter(function (record) {
|
||||
return record.invitation.status === 'pending';
|
||||
});
|
||||
|
||||
count.textContent = String(items.length);
|
||||
if (!items.length) {
|
||||
pending.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
pending.hidden = false;
|
||||
pending.innerHTML = '';
|
||||
var heading = createElement('div', null, tKey('workspace_setup.members.pending') + ' ');
|
||||
heading.style.fontSize = '11px';
|
||||
heading.style.fontWeight = '600';
|
||||
heading.style.textTransform = 'uppercase';
|
||||
heading.style.letterSpacing = '0.06em';
|
||||
heading.style.color = 'var(--text-muted)';
|
||||
heading.style.marginBottom = '10px';
|
||||
var countPill = createElement('span', null, String(items.length));
|
||||
countPill.id = 'pending-count';
|
||||
countPill.style.background = 'rgba(139,148,158,0.15)';
|
||||
countPill.style.borderRadius = '10px';
|
||||
countPill.style.padding = '1px 7px';
|
||||
countPill.style.fontSize = '10px';
|
||||
heading.appendChild(countPill);
|
||||
pending.appendChild(heading);
|
||||
|
||||
items.forEach(function (record) {
|
||||
var invitation = record.invitation;
|
||||
var row = document.createElement('div');
|
||||
row.className = 'member-row';
|
||||
var inviteAvatar = createElement('div', 'member-avatar', '?');
|
||||
inviteAvatar.style.background = 'var(--bg-canvas)';
|
||||
inviteAvatar.style.border = '1.5px dashed var(--border)';
|
||||
inviteAvatar.style.color = 'var(--text-muted)';
|
||||
inviteAvatar.style.fontSize = '16px';
|
||||
row.appendChild(inviteAvatar);
|
||||
|
||||
var inviteInfo = createElement('div', 'member-info');
|
||||
inviteInfo.appendChild(createElement('div', 'member-name', invitation.email));
|
||||
inviteInfo.appendChild(
|
||||
createElement(
|
||||
'div',
|
||||
'member-email',
|
||||
tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) })
|
||||
)
|
||||
);
|
||||
row.appendChild(inviteInfo);
|
||||
row.appendChild(createElement('div', 'member-last-active', '—'));
|
||||
|
||||
var roleBadge = createElement('span', 'member-role-badge', roleLabel(invitation.role));
|
||||
roleBadge.style.background = 'rgba(139,148,158,0.12)';
|
||||
roleBadge.style.color = 'var(--text-muted)';
|
||||
roleBadge.style.border = '1px solid var(--border)';
|
||||
row.appendChild(roleBadge);
|
||||
|
||||
var revokeButton = createElement('button', 'member-remove-btn');
|
||||
revokeButton.type = 'button';
|
||||
revokeButton.title = tKey('workspace_setup.members.revoke');
|
||||
var revokeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
revokeSvg.setAttribute('width', '12');
|
||||
revokeSvg.setAttribute('height', '12');
|
||||
revokeSvg.setAttribute('viewBox', '0 0 16 16');
|
||||
revokeSvg.setAttribute('fill', 'none');
|
||||
revokeSvg.setAttribute('stroke', 'currentColor');
|
||||
revokeSvg.setAttribute('stroke-width', '1.8');
|
||||
revokeSvg.setAttribute('stroke-linecap', 'round');
|
||||
var lineA = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
lineA.setAttribute('x1', '2');
|
||||
lineA.setAttribute('y1', '2');
|
||||
lineA.setAttribute('x2', '14');
|
||||
lineA.setAttribute('y2', '14');
|
||||
var lineB = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
lineB.setAttribute('x1', '14');
|
||||
lineB.setAttribute('y1', '2');
|
||||
lineB.setAttribute('x2', '2');
|
||||
lineB.setAttribute('y2', '14');
|
||||
revokeSvg.appendChild(lineA);
|
||||
revokeSvg.appendChild(lineB);
|
||||
revokeButton.appendChild(revokeSvg);
|
||||
revokeButton.addEventListener('click', function() {
|
||||
revokeInviteById(invitation.id);
|
||||
});
|
||||
row.appendChild(revokeButton);
|
||||
pending.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWorkspaceAccessData() {
|
||||
if (workspaceFormState.isCreateMode || !window.CrankApi || !workspaceFormState.workspaceId) {
|
||||
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 : [];
|
||||
workspaceFormState.invitations = invitationsResponse && invitationsResponse.items ? invitationsResponse.items : [];
|
||||
renderMembers();
|
||||
renderInvitations();
|
||||
}
|
||||
|
||||
function updatePageMode() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
workspaceFormState.isCreateMode = params.get('mode') === 'create';
|
||||
|
||||
var submit = document.getElementById('submit-btn');
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
document.title = 'Crank — ' + tKey('workspace_setup.create.title');
|
||||
document.getElementById('page-title').textContent = tKey('workspace_setup.create.title');
|
||||
document.getElementById('page-subtitle').textContent = tKey('workspace_setup.create.subtitle');
|
||||
document.getElementById('section-invite').hidden = false;
|
||||
document.getElementById('footer-note').hidden = false;
|
||||
submit.textContent = tKey('workspace_setup.actions.create');
|
||||
submit.disabled = true;
|
||||
submit.classList.add('is-disabled');
|
||||
} else {
|
||||
document.getElementById('section-members').hidden = false;
|
||||
document.getElementById('section-danger').hidden = false;
|
||||
submit.textContent = tKey('workspace_setup.actions.save');
|
||||
submit.classList.remove('is-disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function onWsNameInput(value) {
|
||||
var elements = formElements();
|
||||
var normalized = value.trim();
|
||||
elements.avatar.textContent = normalized ? normalized.charAt(0).toUpperCase() : '?';
|
||||
if (!workspaceFormState.slugManual) {
|
||||
elements.slug.value = normalizeSlug(value);
|
||||
}
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
validateCreateForm();
|
||||
}
|
||||
setFormDirty(true);
|
||||
}
|
||||
|
||||
function onWsSlugInput(value) {
|
||||
formElements().slug.value = normalizeSlug(value);
|
||||
workspaceFormState.slugManual = true;
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
validateCreateForm();
|
||||
}
|
||||
setFormDirty(true);
|
||||
}
|
||||
|
||||
function pickColor(element) {
|
||||
workspaceFormState.selectedColor = element.dataset.color;
|
||||
document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) {
|
||||
swatch.classList.remove('active');
|
||||
});
|
||||
element.classList.add('active');
|
||||
formElements().avatar.style.background = workspaceFormState.selectedColor;
|
||||
setFormDirty(true);
|
||||
}
|
||||
|
||||
function validateCreateForm() {
|
||||
var elements = formElements();
|
||||
var valid = Boolean(elements.name.value.trim() && elements.slug.value.trim());
|
||||
elements.submit.disabled = !valid;
|
||||
elements.submit.classList.toggle('is-disabled', !valid);
|
||||
}
|
||||
|
||||
function inviteRows() {
|
||||
return Array.from(document.querySelectorAll('#invite-rows .invite-row')).map(function (row) {
|
||||
return {
|
||||
email: row.querySelector('input').value.trim(),
|
||||
role: uiRoleToApi(row.querySelector('select').value),
|
||||
};
|
||||
}).filter(function (row) {
|
||||
return row.email;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
var payload = workspacePayload();
|
||||
if (!payload.display_name || !payload.slug || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
var submit = document.getElementById('submit-btn');
|
||||
var originalLabel = submit.textContent;
|
||||
submit.disabled = true;
|
||||
submit.textContent = workspaceFormState.isCreateMode ? tKey('workspace_setup.saving_create') : tKey('workspace_setup.saving_update');
|
||||
|
||||
try {
|
||||
var response;
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
response = await window.CrankApi.createWorkspace(payload);
|
||||
} else {
|
||||
response = await window.CrankApi.updateWorkspace(workspaceFormState.workspaceId, payload);
|
||||
}
|
||||
|
||||
var record = response && response.workspace ? response : { workspace: response.workspace || response };
|
||||
var workspace = record.workspace;
|
||||
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
var invites = inviteRows();
|
||||
for (var index = 0; index < invites.length; index += 1) {
|
||||
await window.CrankApi.createInvitation(workspace.id, {
|
||||
email: invites[index].email,
|
||||
role: invites[index].role,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var workspaces = await window.refreshWorkspaces();
|
||||
var mapped = workspaces.find(function (item) { return item.id === workspace.id; }) || {
|
||||
id: workspace.id,
|
||||
slug: workspace.slug,
|
||||
name: workspace.display_name,
|
||||
role: tKey('workspace_setup.role.owner'),
|
||||
letter: (workspace.display_name || workspace.slug).charAt(0).toUpperCase(),
|
||||
color: payload.settings.color || '#0d9488',
|
||||
status: workspace.status,
|
||||
settings: payload.settings,
|
||||
};
|
||||
|
||||
await window.setCurrentWorkspace(mapped);
|
||||
workspaceFormState.workspaceId = workspace.id;
|
||||
workspaceFormState.workspaceRecord = { workspace: workspace };
|
||||
setFormDirty(false);
|
||||
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
|
||||
return;
|
||||
}
|
||||
|
||||
applyWorkspaceRecord({ workspace: workspace });
|
||||
await loadWorkspaceAccessData();
|
||||
submit.textContent = tKey('workspace_setup.saved');
|
||||
setTimeout(function () {
|
||||
submit.textContent = originalLabel;
|
||||
submit.disabled = false;
|
||||
}, 1400);
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.save_error'), tKey('workspace_setup.save_error_title'));
|
||||
}
|
||||
submit.disabled = false;
|
||||
submit.textContent = originalLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function addInviteRow() {
|
||||
var rows = document.getElementById('invite-rows');
|
||||
var node = document.createElement('div');
|
||||
node.className = 'invite-row';
|
||||
var emailInput = createElement('input', 'form-input');
|
||||
emailInput.type = 'email';
|
||||
emailInput.placeholder = 'colleague@company.com';
|
||||
emailInput.autocomplete = 'off';
|
||||
emailInput.style.flex = '1';
|
||||
emailInput.style.marginBottom = '0';
|
||||
node.appendChild(emailInput);
|
||||
|
||||
var roleSelect = createElement('select', 'form-input');
|
||||
roleSelect.style.width = '130px';
|
||||
roleSelect.style.marginBottom = '0';
|
||||
[
|
||||
['admin', tKey('workspace_setup.role.admin')],
|
||||
['developer', tKey('workspace_setup.role.operator')],
|
||||
['viewer', tKey('workspace_setup.role.viewer')]
|
||||
].forEach(function(entry) {
|
||||
var option = document.createElement('option');
|
||||
option.value = entry[0];
|
||||
option.textContent = entry[1];
|
||||
option.selected = entry[0] === 'developer';
|
||||
roleSelect.appendChild(option);
|
||||
});
|
||||
node.appendChild(roleSelect);
|
||||
|
||||
var removeButton = createElement('button', 'invite-row-remove');
|
||||
removeButton.type = 'button';
|
||||
removeButton.title = tKey('workspace_setup.members.remove');
|
||||
removeButton.appendChild(
|
||||
buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/close.svg#icon',
|
||||
12,
|
||||
12
|
||||
)
|
||||
);
|
||||
removeButton.addEventListener('click', function() {
|
||||
removeInviteRow(removeButton);
|
||||
});
|
||||
node.appendChild(removeButton);
|
||||
rows.appendChild(node);
|
||||
}
|
||||
|
||||
function removeInviteRow(button) {
|
||||
button.closest('.invite-row').remove();
|
||||
}
|
||||
|
||||
function toggleInviteForm() {
|
||||
var form = document.getElementById('invite-form');
|
||||
var success = document.getElementById('invite-success');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.hidden) {
|
||||
form.hidden = false;
|
||||
if (success) {
|
||||
success.hidden = true;
|
||||
}
|
||||
document.getElementById('invite-email').focus();
|
||||
} else {
|
||||
form.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendInvite() {
|
||||
if (!window.CrankApi || !workspaceFormState.workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
var email = document.getElementById('invite-email').value.trim();
|
||||
var role = uiRoleToApi(document.getElementById('invite-role').value);
|
||||
if (!email || email.indexOf('@') === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.CrankApi.createInvitation(workspaceFormState.workspaceId, { email: email, role: role });
|
||||
document.getElementById('invite-email').value = '';
|
||||
document.getElementById('invite-success').hidden = false;
|
||||
await loadWorkspaceAccessData();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.invite_error'), tKey('workspace_setup.invite_error_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.role_error'), tKey('workspace_setup.role_error_title'));
|
||||
}
|
||||
} finally {
|
||||
select.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(userId, name) {
|
||||
if (!workspaceFormState.workspaceId || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(tfKey('workspace_setup.remove_confirm', { name: name }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
|
||||
await loadWorkspaceAccessData();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.remove_error'), tKey('workspace_setup.remove_error_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeInviteById(invitationId) {
|
||||
if (!window.CrankApi || !workspaceFormState.workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId);
|
||||
await loadWorkspaceAccessData();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.revoke_error'), tKey('workspace_setup.revoke_error_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function revokeInvite(button) {
|
||||
var row = button.closest('.member-row');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
: tKey('settings.nav.workspace');
|
||||
downloadJsonFile(slug + '-snapshot.json', snapshot);
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.export_error'), tKey('workspace_setup.export_error_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkspaceAction() {
|
||||
if (!workspaceFormState.workspaceId || !window.CrankApi || !window.CrankAuth) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(tKey('workspace_setup.delete_confirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
|
||||
await window.CrankAuth.logout();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace_setup.delete_error'), tKey('workspace_setup.delete_error_title'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initPage() {
|
||||
updatePageMode();
|
||||
|
||||
var language = currentLanguage();
|
||||
document.querySelectorAll('.lang-btn').forEach(function (button) {
|
||||
button.classList.toggle('active', button.dataset.lang === language);
|
||||
});
|
||||
|
||||
document.getElementById('export-workspace-btn').addEventListener('click', exportWorkspaceSnapshot);
|
||||
document.getElementById('delete-workspace-btn').addEventListener('click', deleteWorkspaceAction);
|
||||
|
||||
if (workspaceFormState.isCreateMode) {
|
||||
document.getElementById('ws-name').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
await window.whenWorkspacesReady();
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
if (!workspace || !window.CrankApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var record = await window.CrankApi.getWorkspace(workspace.id);
|
||||
applyWorkspaceRecord(record);
|
||||
await loadWorkspaceAccessData();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || 'Failed to load workspace', 'Workspace load failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
if (workspaceFormState.formDirty) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,287 @@
|
||||
var WS_LIST = [
|
||||
{ id: 'ws_default', slug: 'default', name: 'Default workspace', role: 'Owner', letter: 'D', color: '#0d9488' }
|
||||
];
|
||||
|
||||
var workspaceLoadPromise = null;
|
||||
var workspaceLoadFailed = false;
|
||||
var lastWorkspaceId = null;
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function buildIconSvg(href, width, height) {
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', String(width));
|
||||
svg.setAttribute('height', String(height));
|
||||
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('href', href);
|
||||
svg.appendChild(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
function workspaceColor(index) {
|
||||
return ['#0d9488', '#7c3aed', '#0891b2', '#f59e0b', '#2563eb'][index % 5];
|
||||
}
|
||||
|
||||
function mapWorkspace(record, index) {
|
||||
var workspace = record && record.workspace ? record.workspace : record;
|
||||
var displayName = workspace.display_name || workspace.slug || workspace.id;
|
||||
var settings = workspace.settings || {};
|
||||
var role = record && record.role
|
||||
? String(record.role).replace(/^\w/, function(char) { return char.toUpperCase(); })
|
||||
: tKey('workspace_setup.role.owner');
|
||||
return {
|
||||
id: workspace.id,
|
||||
slug: workspace.slug,
|
||||
name: displayName,
|
||||
role: role,
|
||||
letter: displayName.charAt(0).toUpperCase(),
|
||||
color: settings.color || workspaceColor(index),
|
||||
status: workspace.status,
|
||||
settings: settings,
|
||||
};
|
||||
}
|
||||
|
||||
function cachedWorkspaceId() {
|
||||
try {
|
||||
return localStorage.getItem('crank_workspace_id');
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cachedWorkspaceSlug() {
|
||||
try {
|
||||
return localStorage.getItem('crank_workspace_slug');
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sessionWorkspaceId() {
|
||||
if (!window.CrankAuth || typeof window.CrankAuth.getCachedSession !== 'function') {
|
||||
return null;
|
||||
}
|
||||
var session = window.CrankAuth.getCachedSession();
|
||||
return session && session.current_workspace_id ? session.current_workspace_id : null;
|
||||
}
|
||||
|
||||
function cacheCurrentWorkspace(workspace) {
|
||||
if (!workspace) return;
|
||||
try {
|
||||
localStorage.setItem('crank_workspace_id', workspace.id);
|
||||
localStorage.setItem('crank_workspace_slug', workspace.slug);
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function resolveCurrentWorkspace() {
|
||||
var workspaceId = sessionWorkspaceId() || cachedWorkspaceId();
|
||||
var workspaceSlug = cachedWorkspaceSlug();
|
||||
return WS_LIST.find(function(item) { return item.id === workspaceId; })
|
||||
|| WS_LIST.find(function(item) { return item.slug === workspaceSlug; })
|
||||
|| WS_LIST[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
function emitWorkspaceChange(workspace) {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
lastWorkspaceId = workspace.id;
|
||||
window.dispatchEvent(new CustomEvent('crank:workspacechange', { detail: workspace }));
|
||||
}
|
||||
|
||||
function getCurrentWs() {
|
||||
var workspace = resolveCurrentWorkspace();
|
||||
|
||||
if (workspace) {
|
||||
cacheCurrentWorkspace(workspace);
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
function updateWorkspaceHeader(workspace) {
|
||||
var nameEl = document.getElementById('ws-current-name');
|
||||
var dotEl = document.getElementById('ws-dot');
|
||||
if (nameEl) nameEl.textContent = workspace ? workspace.name : 'No workspace';
|
||||
if (dotEl && workspace) {
|
||||
dotEl.textContent = workspace.letter;
|
||||
dotEl.style.background = workspace.color;
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkspaceList() {
|
||||
var current = getCurrentWs();
|
||||
updateWorkspaceHeader(current);
|
||||
|
||||
var list = document.getElementById('ws-dropdown-list');
|
||||
if (!list) return;
|
||||
window.CrankDom.clear(list);
|
||||
|
||||
WS_LIST.forEach(function(workspace) {
|
||||
var active = current && workspace.id === current.id;
|
||||
var item = document.createElement('div');
|
||||
item.className = 'ws-dropdown-item' + (active ? ' active' : '');
|
||||
item.addEventListener('click', function() {
|
||||
switchWorkspace(workspace.id);
|
||||
});
|
||||
|
||||
var dot = document.createElement('div');
|
||||
dot.className = 'ws-item-dot';
|
||||
dot.style.background = workspace.color;
|
||||
dot.textContent = workspace.letter;
|
||||
item.appendChild(dot);
|
||||
|
||||
var info = document.createElement('div');
|
||||
info.className = 'ws-item-info';
|
||||
var name = document.createElement('div');
|
||||
name.className = 'ws-item-name';
|
||||
name.textContent = workspace.name;
|
||||
var role = document.createElement('div');
|
||||
role.className = 'ws-item-role';
|
||||
role.textContent = workspace.role;
|
||||
info.appendChild(name);
|
||||
info.appendChild(role);
|
||||
item.appendChild(info);
|
||||
|
||||
if (active) {
|
||||
var check = buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/check.svg#icon',
|
||||
12,
|
||||
12
|
||||
);
|
||||
item.appendChild(check);
|
||||
}
|
||||
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
var divider = document.createElement('div');
|
||||
divider.className = 'ws-dropdown-divider';
|
||||
list.appendChild(divider);
|
||||
|
||||
var manageLink = document.createElement('a');
|
||||
manageLink.className = 'ws-dropdown-mgmt-link';
|
||||
manageLink.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
|
||||
manageLink.addEventListener('click', function() {
|
||||
var dd = document.getElementById('ws-dropdown');
|
||||
if (dd) dd.hidden = true;
|
||||
});
|
||||
manageLink.appendChild(
|
||||
buildIconSvg(
|
||||
(window.APP_BASE || '') + 'icons/general/settings.svg#icon',
|
||||
14,
|
||||
14
|
||||
)
|
||||
);
|
||||
manageLink.appendChild(document.createTextNode(tKey('settings.ws.title')));
|
||||
list.appendChild(manageLink);
|
||||
}
|
||||
|
||||
async function loadWorkspaces() {
|
||||
if (workspaceLoadPromise) return workspaceLoadPromise;
|
||||
|
||||
workspaceLoadPromise = (async function() {
|
||||
if (!window.CrankApi) {
|
||||
renderWorkspaceList();
|
||||
return WS_LIST;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listWorkspaces();
|
||||
var items = (response && response.items ? response.items : []).map(mapWorkspace);
|
||||
if (items.length > 0) {
|
||||
WS_LIST = items;
|
||||
}
|
||||
workspaceLoadFailed = false;
|
||||
} catch (_error) {
|
||||
workspaceLoadFailed = true;
|
||||
}
|
||||
|
||||
renderWorkspaceList();
|
||||
return WS_LIST;
|
||||
}());
|
||||
|
||||
return workspaceLoadPromise;
|
||||
}
|
||||
|
||||
async function refreshWorkspaces() {
|
||||
workspaceLoadPromise = null;
|
||||
return loadWorkspaces();
|
||||
}
|
||||
|
||||
function initWorkspaceSwitcher() {
|
||||
renderWorkspaceList();
|
||||
loadWorkspaces();
|
||||
}
|
||||
|
||||
function toggleWsSwitcher(e) {
|
||||
e.stopPropagation();
|
||||
var dd = document.getElementById('ws-dropdown');
|
||||
if (dd) dd.hidden = !dd.hidden;
|
||||
}
|
||||
|
||||
function switchWorkspace(workspaceId) {
|
||||
var workspace = WS_LIST.find(function(item) { return item.id === workspaceId; });
|
||||
if (!workspace) return;
|
||||
|
||||
var dd = document.getElementById('ws-dropdown');
|
||||
if (dd) dd.hidden = true;
|
||||
|
||||
if (!window.CrankApi || !window.CrankAuth || typeof window.CrankAuth.replaceSession !== 'function') {
|
||||
cacheCurrentWorkspace(workspace);
|
||||
renderWorkspaceList();
|
||||
emitWorkspaceChange(workspace);
|
||||
return;
|
||||
}
|
||||
|
||||
return window.CrankApi
|
||||
.setCurrentWorkspace(workspace.id)
|
||||
.then(function(session) {
|
||||
window.CrankAuth.replaceSession(session);
|
||||
cacheCurrentWorkspace(workspace);
|
||||
renderWorkspaceList();
|
||||
emitWorkspaceChange(workspace);
|
||||
return workspace;
|
||||
})
|
||||
.catch(function(error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('workspace.switch_error'), tKey('workspace.switch_error_title'));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentWorkspace(workspace) {
|
||||
if (!workspace) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.resolve(switchWorkspace(workspace.id));
|
||||
}
|
||||
|
||||
window.getCurrentWorkspace = getCurrentWs;
|
||||
window.whenWorkspacesReady = loadWorkspaces;
|
||||
window.refreshWorkspaces = refreshWorkspaces;
|
||||
window.setCurrentWorkspace = setCurrentWorkspace;
|
||||
window.hasWorkspaceApiFailure = function() { return workspaceLoadFailed; };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initWorkspaceSwitcher);
|
||||
|
||||
window.addEventListener('crank:sessionchange', function() {
|
||||
var workspace = getCurrentWs();
|
||||
renderWorkspaceList();
|
||||
if (workspace && workspace.id !== lastWorkspaceId) {
|
||||
emitWorkspaceChange(workspace);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('#ws-switcher')) {
|
||||
var dd = document.getElementById('ws-dropdown');
|
||||
if (dd) dd.hidden = true;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user