feat: connect wizard live flow to admin api

This commit is contained in:
a.tolmachev
2026-03-31 02:11:55 +03:00
parent 04bfc84c19
commit dcc4401043
11 changed files with 736 additions and 118 deletions
+83
View File
@@ -47,6 +47,41 @@
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);
}
@@ -155,9 +190,23 @@
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);
},
uploadProtoFile: function(workspaceId, operationId, content, fileName) {
return postBytes(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/descriptors/proto',
@@ -165,6 +214,40 @@
fileName
);
},
uploadDescriptorSet: function(workspaceId, operationId, content, fileName) {
return postBytes(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/descriptors/descriptor-set',
content,
fileName
);
},
listGrpcServices: function(workspaceId, operationId, version) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/grpc/services'
+ query({ version: version })
);
},
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');
},
File diff suppressed because one or more lines are too long
+434 -89
View File
@@ -7,6 +7,9 @@ var wizardWorkspaceId = null;
var wizardCurrentOperation = null;
var wizardCurrentVersion = null;
var wizardProtoUpload = null;
var wizardDescriptorSetUpload = null;
var wizardTestResponsePreview = null;
var grpcDescriptorServices = [];
/* ── Dynamic step loading ── */
function _stepFile(n) {
@@ -210,6 +213,7 @@ document.addEventListener('DOMContentLoaded', async function() {
await loadWizardPanels([1, 2, 3, 4, 5]);
bindProtocolCards();
bindWizardLiveActions();
var params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'edit' && params.get('operationId')) {
@@ -219,6 +223,7 @@ document.addEventListener('DOMContentLoaded', async function() {
await loadOperationForEdit();
}
updateWizardProtocolVisibility();
_doGoToStep(1);
});
@@ -824,30 +829,11 @@ var REFLECTION_MOCK = {
};
function startReflection() {
var idle = document.getElementById('grpc-reflect-idle');
var loading = document.getElementById('grpc-reflect-loading');
var error = document.getElementById('grpc-reflect-error');
var view = document.getElementById('reflect-parsed-view');
if (idle) idle.style.display = 'none';
if (loading) loading.style.display = '';
if (error) error.style.display = 'none';
if (view) view.style.display = 'none';
document.getElementById('grpc-method-detail').style.display = 'none';
var statusEl = document.getElementById('grpc-reflect-status-text');
var steps = ['Connecting…', 'Negotiating TLS…', 'Sending reflection request…', 'Parsing service descriptors…'];
var i = 0;
var iv = setInterval(function() {
if (statusEl && i < steps.length) { statusEl.textContent = steps[i++]; }
}, 380);
setTimeout(function() {
clearInterval(iv);
if (loading) loading.style.display = 'none';
if (idle) idle.style.display = '';
renderReflectionResult(REFLECTION_MOCK);
}, 1700);
goToStep(5);
showWizardLiveStatus(
'gRPC discovery moved to live descriptor flow',
'Server reflection is not wired in this deployment. Save the draft, then use the descriptor-set tools in Step 5 to upload a real descriptor set and discover unary methods from the backend.'
);
}
@@ -1191,71 +1177,7 @@ function collectWizardPayload() {
async function saveOperation(stayOnPage) {
try {
var payload = collectWizardPayload();
var response;
if (!wizardWorkspaceId) {
throw new Error('No workspace selected');
}
if (wizardMode === 'edit' && wizardEditId) {
response = await window.CrankApi.updateOperation(wizardWorkspaceId, wizardEditId, {
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,
});
} else {
response = await window.CrankApi.createOperation(wizardWorkspaceId, payload);
wizardEditId = response.operation_id;
wizardMode = 'edit';
history.replaceState({}, '', window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(wizardEditId));
setEditModePresentation();
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;
}
if (wizardProtocol === 'grpc' && wizardProtoUpload && wizardEditId) {
var descriptor = await window.CrankApi.uploadProtoFile(
wizardWorkspaceId,
wizardEditId,
new TextEncoder().encode(wizardProtoUpload.content),
wizardProtoUpload.fileName
);
payload.target.descriptor_ref = descriptor.descriptor_id;
wizardCurrentVersion = wizardCurrentVersion || {};
wizardCurrentVersion.target = payload.target;
await window.CrankApi.updateOperation(wizardWorkspaceId, wizardEditId, {
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,
});
}
if (stayOnPage) {
var saveDraftBtn = document.querySelector('.btn-save-draft');
if (saveDraftBtn) {
var label = saveDraftBtn.textContent;
saveDraftBtn.textContent = 'Saved ✓';
setTimeout(function() {
saveDraftBtn.textContent = label;
}, 1400);
}
return;
}
window.location.href = (window.APP_BASE || '') + 'index.html';
await persistCurrentDraft(stayOnPage);
} catch (error) {
alert(error.message || 'Failed to save operation');
}
@@ -1294,6 +1216,7 @@ function selectProtocol(protocol) {
});
var s3name = document.getElementById('sidebar-step-3-name');
if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || 'Protocol config';
updateWizardProtocolVisibility();
}
function prefillUpstream(baseUrl, headers) {
@@ -1423,4 +1346,426 @@ function prefillWizardFromEdit(detail, versionDocument) {
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;
updateWizardProtocolVisibility();
}
function bindWizardLiveActions() {
bindClick('wizard-upload-input-sample', uploadInputSampleFromWizard);
bindClick('wizard-upload-output-sample', uploadOutputSampleFromWizard);
bindClick('wizard-generate-draft', generateDraftFromWizard);
bindClick('wizard-run-test', runWizardTest);
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
bindClick('wizard-export-yaml', exportWizardYaml);
bindClick('wizard-import-yaml', importWizardYaml);
bindClick('wizard-publish-operation', publishWizardOperation);
bindClick('wizard-select-descriptor-set', function() {
var input = document.getElementById('wizard-descriptor-set-input');
if (input) input.click();
});
bindClick('wizard-upload-descriptor-set', uploadDescriptorSetAndDiscover);
bindClick('wizard-import-yaml-file-trigger', function() {
var input = document.getElementById('wizard-import-yaml-file');
if (input) input.click();
});
var descriptorInput = document.getElementById('wizard-descriptor-set-input');
if (descriptorInput) descriptorInput.addEventListener('change', handleDescriptorSetSelection);
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 updateWizardProtocolVisibility() {
var grpcTools = document.getElementById('wizard-grpc-live-tools');
if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none';
}
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.style.display = '';
root.style.borderColor = isError ? 'rgba(248, 113, 113, 0.35)' : '';
}
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 uploadPendingGrpcArtifacts(payload) {
if (wizardProtocol !== 'grpc' || !wizardEditId) return;
var descriptorResponse = null;
if (wizardProtoUpload) {
descriptorResponse = await window.CrankApi.uploadProtoFile(
wizardWorkspaceId,
wizardEditId,
new TextEncoder().encode(wizardProtoUpload.content),
wizardProtoUpload.fileName
);
wizardProtoUpload = null;
}
if (wizardDescriptorSetUpload) {
descriptorResponse = await window.CrankApi.uploadDescriptorSet(
wizardWorkspaceId,
wizardEditId,
wizardDescriptorSetUpload.bytes,
wizardDescriptorSetUpload.fileName
);
wizardDescriptorSetUpload = null;
var descriptorName = document.getElementById('wizard-descriptor-set-name');
if (descriptorName) descriptorName.textContent = 'Descriptor set uploaded';
}
if (!descriptorResponse) return;
payload.target.descriptor_ref = descriptorResponse.descriptor_id;
payload.target.descriptor_set_b64 = '';
await window.CrankApi.updateOperation(
wizardWorkspaceId,
wizardEditId,
updateOperationPayload(payload)
);
}
async function persistCurrentDraft(stayOnPage) {
if (!wizardWorkspaceId) {
throw new Error('No workspace selected');
}
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 uploadPendingGrpcArtifacts(payload);
await refreshCurrentOperationState();
if (stayOnPage) {
var saveDraftBtn = document.querySelector('.btn-save-draft');
if (saveDraftBtn) {
var label = saveDraftBtn.textContent;
saveDraftBtn.textContent = 'Saved ✓';
setTimeout(function() {
saveDraftBtn.textContent = label;
}, 1400);
}
}
showWizardLiveStatus(
createdNow ? 'Draft created' : 'Draft updated',
'The current draft is saved on the backend and ready for live actions in this wizard.'
);
return response;
}
function safeStringify(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function setTextareaValue(id, value) {
var element = document.getElementById(id);
if (!element) return;
element.value = safeStringify(value);
}
async function uploadInputSampleFromWizard() {
await persistCurrentDraft(true);
await window.CrankApi.uploadInputSample(
wizardWorkspaceId,
wizardEditId,
parseStructuredText(textValue('wizard-input-sample'))
);
showWizardLiveStatus('Input sample saved', 'The input sample is now stored against the current draft version.');
}
async function uploadOutputSampleFromWizard() {
await persistCurrentDraft(true);
await window.CrankApi.uploadOutputSample(
wizardWorkspaceId,
wizardEditId,
parseStructuredText(textValue('wizard-output-sample'))
);
showWizardLiveStatus('Output sample saved', 'The output sample is now stored against the current draft version.');
}
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('Draft generated', 'Schemas and mappings were regenerated from the saved samples.');
}
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 : []);
showWizardLiveStatus(
result.ok ? 'Test run completed' : 'Test run returned errors',
result.ok
? 'The current draft executed successfully against the upstream.'
: 'The draft executed, but the backend returned validation or runtime errors.',
!result.ok
);
}
function copyTestResponseToOutputSample() {
if (!wizardTestResponsePreview) {
showWizardLiveStatus('No test response yet', 'Run a test first, then copy the response preview into the output sample editor.', true);
return;
}
setTextareaValue('wizard-output-sample', wizardTestResponsePreview);
showWizardLiveStatus('Response copied', 'The latest test response was copied into the output sample editor.');
}
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);
}
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('YAML exported', 'The current draft YAML was downloaded from the backend.');
}
async function importWizardYaml() {
if (!wizardWorkspaceId) {
throw new Error('No workspace selected');
}
var yamlDocument = textValue('wizard-import-yaml-text');
if (!yamlDocument) {
showWizardLiveStatus('No YAML provided', 'Paste a YAML document or load it from a file before importing.', true);
return;
}
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
showWizardLiveStatus('YAML imported', 'The imported operation draft was stored. The wizard will reopen it now.');
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(
'Operation published',
'Version ' + published.published_version + ' is now published and can be bound into agents.'
);
}
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('YAML loaded', 'The YAML document was loaded into the import editor.');
};
reader.readAsText(file);
}
function handleDescriptorSetSelection(event) {
var file = event.target.files && event.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(loadEvent) {
wizardDescriptorSetUpload = {
fileName: file.name,
bytes: new Uint8Array(loadEvent.target.result),
};
var fileNameEl = document.getElementById('wizard-descriptor-set-name');
if (fileNameEl) fileNameEl.textContent = file.name;
showWizardLiveStatus('Descriptor set selected', 'Upload the descriptor set to discover unary gRPC methods from the backend.');
};
reader.readAsArrayBuffer(file);
}
function renderDescriptorServiceList(services) {
grpcDescriptorServices = services || [];
var summaryEl = document.getElementById('wizard-descriptor-services-summary');
var listEl = document.getElementById('wizard-descriptor-services-list');
if (!summaryEl || !listEl) return;
var methodCount = grpcDescriptorServices.reduce(function(total, service) {
return total + ((service.methods || []).length);
}, 0);
summaryEl.textContent = grpcDescriptorServices.length
? (grpcDescriptorServices.length + ' services · ' + methodCount + ' unary methods discovered')
: 'No unary methods discovered yet';
listEl.innerHTML = '';
grpcDescriptorServices.forEach(function(service, serviceIndex) {
var group = document.createElement('div');
group.className = 'config-card';
group.style.marginBottom = '0';
var body = document.createElement('div');
body.className = 'config-card-body';
body.style.padding = '16px';
body.style.gap = '10px';
var title = document.createElement('div');
title.className = 'config-card-title';
title.textContent = (service.package ? service.package + '.' : '') + service.service;
body.appendChild(title);
(service.methods || []).forEach(function(method, methodIndex) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'btn-ghost-sm';
button.style.width = '100%';
button.style.justifyContent = 'space-between';
button.textContent = method.name;
button.addEventListener('click', function() {
applyDiscoveredGrpcMethod(serviceIndex, methodIndex);
});
body.appendChild(button);
});
group.appendChild(body);
listEl.appendChild(group);
});
}
function applyDiscoveredGrpcMethod(serviceIndex, methodIndex) {
var service = grpcDescriptorServices[serviceIndex];
var method = service && service.methods ? service.methods[methodIndex] : null;
if (!service || !method) return;
var route = '/' + (service.package ? service.package + '.' : '') + service.service + '/' + method.name;
selectGrpcSource('manual');
setValue('grpc-manual-route', route);
setValue('grpc-manual-req-type', method.name + 'Request');
setValue('grpc-manual-res-type', method.name + 'Response');
grpcManualRouteInput(route);
grpcManualTypeInput();
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(method.input_schema), null, 2));
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(method.output_schema), null, 2));
showWizardLiveStatus('gRPC method applied', 'The route and schemas were filled from the uploaded descriptor set.');
}
async function uploadDescriptorSetAndDiscover() {
var hasSavedDescriptor = !!(
wizardCurrentVersion
&& wizardCurrentVersion.target
&& wizardCurrentVersion.target.kind === 'grpc'
&& wizardCurrentVersion.target.descriptor_ref
);
if (!wizardDescriptorSetUpload && !hasSavedDescriptor) {
showWizardLiveStatus('No descriptor set selected', 'Choose a descriptor-set file before uploading it, or save a draft that already has one attached.', true);
return;
}
await persistCurrentDraft(true);
var services = await window.CrankApi.listGrpcServices(
wizardWorkspaceId,
wizardEditId,
currentDraftVersion()
);
renderDescriptorServiceList(services.services || []);
showWizardLiveStatus('Descriptor services discovered', 'The backend parsed the descriptor set. Pick a method to apply its route and schemas.');
}