feat: connect alpine operations and wizard to admin api
This commit is contained in:
+574
-189
@@ -3,6 +3,10 @@ var TOTAL_STEPS = 5;
|
||||
var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc'
|
||||
var wizardMode = 'create'; // 'create' | 'edit'
|
||||
var wizardEditId = null;
|
||||
var wizardWorkspaceId = null;
|
||||
var wizardCurrentOperation = null;
|
||||
var wizardCurrentVersion = null;
|
||||
var wizardProtoUpload = null;
|
||||
|
||||
/* ── Dynamic step loading ── */
|
||||
function _stepFile(n) {
|
||||
@@ -115,9 +119,54 @@ function _doGoToStep(n) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
function loadWizardPanels(steps) {
|
||||
return steps.reduce(function(chain, step) {
|
||||
return chain.then(function() {
|
||||
return new Promise(function(resolve) {
|
||||
loadStep(step, resolve);
|
||||
});
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
function bindProtocolCards() {
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
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 proto = card.dataset.protocol
|
||||
|| (card.classList.contains('rest')
|
||||
? 'rest'
|
||||
: card.classList.contains('graphql')
|
||||
? 'graphql'
|
||||
: card.classList.contains('grpc')
|
||||
? 'grpc'
|
||||
: 'rest');
|
||||
|
||||
wizardProtocol = proto;
|
||||
var s3name = document.getElementById('sidebar-step-3-name');
|
||||
if (s3name) s3name.textContent = STEP3_LABELS[proto] || 'Protocol config';
|
||||
|
||||
if (proto === 'graphql') {
|
||||
var pathInput = document.getElementById('endpoint-path');
|
||||
if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql';
|
||||
}
|
||||
});
|
||||
card.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
card.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
document.querySelector('.btn-continue').addEventListener('click', function() {
|
||||
if (currentStep < TOTAL_STEPS) {
|
||||
goToStep(currentStep + 1);
|
||||
@@ -130,12 +179,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!this.disabled && currentStep > 1) goToStep(currentStep - 1);
|
||||
});
|
||||
|
||||
// Sidebar step clicks
|
||||
document.querySelectorAll('.step-item').forEach(function(item, i) {
|
||||
item.addEventListener('click', function() { goToStep(i + 1); });
|
||||
});
|
||||
|
||||
// Back to catalog / close
|
||||
var backToCatalog = document.getElementById('back-to-catalog');
|
||||
if (backToCatalog) {
|
||||
backToCatalog.addEventListener('click', function() {
|
||||
@@ -150,98 +197,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Protocol card selection
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
document.querySelectorAll('.protocol-card').forEach(function(c) {
|
||||
c.classList.remove('selected');
|
||||
c.setAttribute('aria-checked', 'false');
|
||||
});
|
||||
card.classList.add('selected');
|
||||
card.setAttribute('aria-checked', 'true');
|
||||
|
||||
// Detect protocol from card data-protocol or class
|
||||
var proto = card.dataset.protocol ||
|
||||
(card.classList.contains('rest') ? 'rest' :
|
||||
card.classList.contains('graphql') ? 'graphql' :
|
||||
card.classList.contains('grpc') ? 'grpc' : 'rest');
|
||||
wizardProtocol = proto;
|
||||
|
||||
// Update sidebar step-3 label
|
||||
var s3name = document.getElementById('sidebar-step-3-name');
|
||||
if (s3name) s3name.textContent = STEP3_LABELS[proto] || 'Protocol config';
|
||||
|
||||
// GraphQL: default endpoint path to /graphql and show hint
|
||||
if (proto === 'graphql') {
|
||||
var pathInput = document.getElementById('endpoint-path');
|
||||
if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql';
|
||||
}
|
||||
});
|
||||
card.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); }
|
||||
});
|
||||
});
|
||||
|
||||
// Method button selection
|
||||
document.querySelectorAll('.method-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var cls = ['selected-get','selected-post','selected-put','selected-patch','selected-delete'];
|
||||
document.querySelectorAll('.method-btn').forEach(function(b) {
|
||||
cls.forEach(function(c) { b.classList.remove(c); });
|
||||
});
|
||||
btn.classList.add('selected-' + btn.querySelector('.method-verb').textContent.toLowerCase());
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle switches
|
||||
document.querySelectorAll('.toggle-row').forEach(function(row) {
|
||||
row.addEventListener('click', function() {
|
||||
var t = row.querySelector('.toggle');
|
||||
if (t) t.classList.toggle('on');
|
||||
});
|
||||
});
|
||||
|
||||
// Save draft button
|
||||
var saveDraftBtn = document.querySelector('.btn-save-draft');
|
||||
if (saveDraftBtn) {
|
||||
saveDraftBtn.addEventListener('click', function() {
|
||||
var data = collectWizardData();
|
||||
if (!data.name) return; // nothing to save yet
|
||||
try {
|
||||
var overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]');
|
||||
if (wizardMode === 'edit' && wizardEditId) {
|
||||
overrides = overrides.map(function(o) {
|
||||
return o.id === wizardEditId ? Object.assign({}, o, data, { id: wizardEditId }) : o;
|
||||
});
|
||||
} else {
|
||||
var draftId = 'op_draft_' + Date.now();
|
||||
overrides.push(Object.assign({ id: draftId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data));
|
||||
}
|
||||
localStorage.setItem('crank_ops_overrides', JSON.stringify(overrides));
|
||||
} catch (e) {}
|
||||
// Visual feedback
|
||||
saveDraftBtn.textContent = 'Saved ✓';
|
||||
setTimeout(function() { saveDraftBtn.textContent = 'Save draft'; }, 1800);
|
||||
saveOperation(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Detect edit mode from URL param and sessionStorage
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
wizardWorkspaceId = workspace ? workspace.id : null;
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
bindProtocolCards();
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('mode') === 'edit') {
|
||||
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
||||
wizardMode = 'edit';
|
||||
wizardEditId = params.get('operationId');
|
||||
document.title = 'Crank — Edit Operation';
|
||||
try {
|
||||
var editData = JSON.parse(sessionStorage.getItem('wizard_edit') || 'null');
|
||||
if (editData) {
|
||||
wizardEditId = editData.id;
|
||||
prefillWizardFromEdit(editData);
|
||||
}
|
||||
} catch (e) {}
|
||||
await loadOperationForEdit();
|
||||
}
|
||||
|
||||
// Init: load step 1 fragment first, then render
|
||||
loadStep(1, function() { _doGoToStep(1); });
|
||||
|
||||
_doGoToStep(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -723,6 +701,7 @@ function handleProtoFile(event) {
|
||||
if (!file) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
wizardProtoUpload = { fileName: file.name, content: e.target.result };
|
||||
showProtoFileInfo(file.name, file.size);
|
||||
var parsed = parseProto(e.target.result);
|
||||
renderParsedProto(parsed);
|
||||
@@ -738,6 +717,7 @@ function handleProtoDrop(event) {
|
||||
if (!file || !file.name.endsWith('.proto')) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
wizardProtoUpload = { fileName: file.name, content: e.target.result };
|
||||
showProtoFileInfo(file.name, file.size);
|
||||
renderParsedProto(parseProto(e.target.result));
|
||||
};
|
||||
@@ -769,6 +749,7 @@ function resetProto() {
|
||||
if (fileInput) fileInput.value = '';
|
||||
protoParsed = null;
|
||||
selectedRpcMethod = null;
|
||||
wizardProtoUpload = null;
|
||||
}
|
||||
|
||||
function toggleProtoPaste() {
|
||||
@@ -795,6 +776,7 @@ function parseProtoPasted() {
|
||||
var ta = document.getElementById('proto-paste-input');
|
||||
if (!ta || !ta.value.trim()) return;
|
||||
var parsed = parseProto(ta.value);
|
||||
wizardProtoUpload = { fileName: 'pasted.proto', content: ta.value };
|
||||
document.getElementById('proto-paste-area').style.display = 'none';
|
||||
var dropzone = document.getElementById('proto-dropzone');
|
||||
if (dropzone) dropzone.style.display = 'none';
|
||||
@@ -909,133 +891,536 @@ function grpcManualTypeInput() {
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Wizard edit mode — collect, save, prefill
|
||||
══════════════════════════════════════════════ */
|
||||
function textValue(id) {
|
||||
var element = document.getElementById(id);
|
||||
return element ? element.value.trim() : '';
|
||||
}
|
||||
|
||||
function collectWizardData() {
|
||||
var protocol = wizardProtocol;
|
||||
function setValue(id, value) {
|
||||
var element = document.getElementById(id);
|
||||
if (element) element.value = value || '';
|
||||
}
|
||||
|
||||
// Upstream base URL
|
||||
var upstream = upstreams.find(function(u) { return u.id === selectedUpstreamId; });
|
||||
var baseUrl = upstream ? upstream.url : '';
|
||||
function parseStructuredText(text) {
|
||||
if (!text.trim()) return {};
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (_error) {
|
||||
if (!window.jsyaml) throw new Error('YAML parser is not available');
|
||||
return window.jsyaml.load(text) || {};
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol-specific path & method
|
||||
var method = '';
|
||||
var targetUrl = baseUrl;
|
||||
if (protocol === 'rest') {
|
||||
var activeMethod = document.querySelector('.method-card.active');
|
||||
method = activeMethod ? activeMethod.dataset.method : 'POST';
|
||||
var pathEl = document.getElementById('endpoint-path');
|
||||
var path = pathEl ? pathEl.value.trim() : '';
|
||||
targetUrl = baseUrl + path;
|
||||
} else if (protocol === 'graphql') {
|
||||
method = 'POST';
|
||||
targetUrl = baseUrl + '/graphql';
|
||||
} else if (protocol === 'grpc') {
|
||||
var pathValEl = document.getElementById('grpc-path-value');
|
||||
var grpcPath = pathValEl ? pathValEl.textContent.trim() : '';
|
||||
targetUrl = baseUrl + grpcPath;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
var val = function(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; };
|
||||
if (kind === 'array' && schemaNode.items) {
|
||||
schema.items = convertJsonSchemaToCrankSchema(schemaNode.items, []);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function mappingRootForProtocol(protocol) {
|
||||
if (protocol === 'graphql') return '$.request.variables';
|
||||
if (protocol === 'grpc') return '$.request.grpc';
|
||||
var activeMethod = document.querySelector('.method-card.active');
|
||||
var method = activeMethod ? activeMethod.dataset.method : 'POST';
|
||||
return method === 'GET' || method === 'DELETE' ? '$.request.query' : '$.request.body';
|
||||
}
|
||||
|
||||
function normalizeInputSource(path) {
|
||||
if (typeof path !== 'string') return '$.mcp';
|
||||
if (path.indexOf('$.input.') === 0) return '$.mcp.' + path.slice('$.input.'.length);
|
||||
if (path === '$.input') return '$.mcp';
|
||||
return path;
|
||||
}
|
||||
|
||||
function normalizeOutputSource(path) {
|
||||
if (typeof path !== 'string') return '$.response.data';
|
||||
return path;
|
||||
}
|
||||
|
||||
function buildMappingSet(rawValue, mode) {
|
||||
if (rawValue && Array.isArray(rawValue.rules)) {
|
||||
return { rules: rawValue.rules };
|
||||
}
|
||||
|
||||
var rules = [];
|
||||
if (!rawValue || typeof rawValue !== 'object') {
|
||||
return { rules: rules };
|
||||
}
|
||||
|
||||
var root = mappingRootForProtocol(wizardProtocol);
|
||||
|
||||
Object.keys(rawValue).forEach(function(key) {
|
||||
var source = rawValue[key];
|
||||
if (typeof source !== 'string') return;
|
||||
if (mode === 'input') {
|
||||
rules.push({
|
||||
source: normalizeInputSource(source),
|
||||
target: root + '.' + key,
|
||||
required: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
rules.push({
|
||||
source: normalizeOutputSource(source),
|
||||
target: '$.output.' + key,
|
||||
required: false,
|
||||
});
|
||||
});
|
||||
|
||||
return { rules: rules };
|
||||
}
|
||||
|
||||
function parseExecutionConfig(text) {
|
||||
var value = parseStructuredText(text);
|
||||
var retry = value.retry || value.retry_policy || null;
|
||||
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
||||
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: value.auth && value.auth.profile ? value.auth.profile : null,
|
||||
headers: headers,
|
||||
protocol_options: null,
|
||||
};
|
||||
|
||||
if (wizardProtocol === 'grpc') {
|
||||
var useTls = false;
|
||||
if (value.protocol_options && value.protocol_options.grpc) {
|
||||
useTls = !!value.protocol_options.grpc.use_tls;
|
||||
} else if (value.tls) {
|
||||
useTls = !!value.tls.verify;
|
||||
}
|
||||
config.protocol_options = { grpc: { use_tls: useTls } };
|
||||
}
|
||||
|
||||
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 authHeaders = textValue('new-upstream-auth-headers');
|
||||
return {
|
||||
name: val('tool-name'),
|
||||
display_name: val('tool-display-name') || val('tool-name'),
|
||||
description: val('tool-description'),
|
||||
protocol: protocol,
|
||||
method: method,
|
||||
target_url: targetUrl,
|
||||
input_schema: val('tool-input-schema'),
|
||||
output_schema: val('tool-output-schema'),
|
||||
input_mapping: val('tool-input-mapping'),
|
||||
output_mapping: val('tool-output-mapping'),
|
||||
exec_config: val('tool-exec-config'),
|
||||
id: 'custom',
|
||||
name: customName || 'custom-upstream',
|
||||
url: customUrl,
|
||||
authHeaders: authHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
function saveOperation() {
|
||||
var data = collectWizardData();
|
||||
if (!data.name) { goToStep(4); return; }
|
||||
|
||||
try {
|
||||
var overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]');
|
||||
|
||||
if (wizardMode === 'edit' && wizardEditId) {
|
||||
var found = false;
|
||||
overrides = overrides.map(function(o) {
|
||||
if (o.id === wizardEditId) {
|
||||
found = true;
|
||||
return Object.assign({}, o, data, { id: wizardEditId });
|
||||
}
|
||||
return o;
|
||||
});
|
||||
if (!found) {
|
||||
overrides.push(Object.assign({ id: wizardEditId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data));
|
||||
}
|
||||
} else {
|
||||
var newId = 'op_' + Date.now();
|
||||
overrides = overrides.filter(function(o) { return o.id !== newId; });
|
||||
overrides.push(Object.assign({ id: newId, status: 'draft', created_at: new Date().toISOString(), calls_today: 0, last_called_at: null, category: 'general' }, data));
|
||||
}
|
||||
|
||||
localStorage.setItem('crank_ops_overrides', JSON.stringify(overrides));
|
||||
} catch (e) {}
|
||||
|
||||
setTimeout(function() { window.location.href = (window.APP_BASE || '') + 'index.html'; }, 300);
|
||||
function joinUrl(baseUrl, path) {
|
||||
if (!baseUrl) return path || '';
|
||||
if (!path) return baseUrl;
|
||||
return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function prefillWizardFromEdit(op) {
|
||||
if (!op) return;
|
||||
function selectedGraphqlOperationType() {
|
||||
var active = document.querySelector('.gql-type-card.active');
|
||||
return active ? active.dataset.gqlType : 'query';
|
||||
}
|
||||
|
||||
// Update wizard header labels for edit mode
|
||||
function graphqlTopLevelField(queryText) {
|
||||
var match = queryText.replace(/#[^\n]*/g, '').match(/\{\s*([A-Za-z_][A-Za-z0-9_]*)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function graphqlOperationName(queryText) {
|
||||
var match = queryText.match(/\b(query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
||||
return match ? match[2] : textValue('tool-name');
|
||||
}
|
||||
|
||||
function buildTarget() {
|
||||
var upstream = currentUpstream();
|
||||
if (!upstream || !upstream.url) {
|
||||
throw new Error('Select or register an upstream first');
|
||||
}
|
||||
|
||||
var pathTemplate = textValue('endpoint-path') || '/';
|
||||
|
||||
if (wizardProtocol === 'rest') {
|
||||
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.authHeaders),
|
||||
};
|
||||
}
|
||||
|
||||
if (wizardProtocol === 'graphql') {
|
||||
var queryTemplate = textValue('gql-query-editor');
|
||||
var topField = graphqlTopLevelField(queryTemplate);
|
||||
return {
|
||||
kind: 'graphql',
|
||||
endpoint: joinUrl(upstream.url, pathTemplate),
|
||||
operation_type: selectedGraphqlOperationType(),
|
||||
operation_name: graphqlOperationName(queryTemplate),
|
||||
query_template: queryTemplate,
|
||||
response_path: topField ? '$.response.body.data.' + topField : '$.response.body.data',
|
||||
};
|
||||
}
|
||||
|
||||
var route = textValue('grpc-manual-route') || (document.getElementById('grpc-path-value') ? document.getElementById('grpc-path-value').textContent.trim() : '');
|
||||
var descriptorRef = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc'
|
||||
? wizardCurrentVersion.target.descriptor_ref
|
||||
: 'desc_pending';
|
||||
var descriptorSetB64 = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc'
|
||||
? (wizardCurrentVersion.target.descriptor_set_b64 || '')
|
||||
: '';
|
||||
var parsedRoute = parseGrpcRoute(route);
|
||||
|
||||
return {
|
||||
kind: 'grpc',
|
||||
server_addr: upstream.url,
|
||||
package: parsedRoute.package,
|
||||
service: parsedRoute.service,
|
||||
method: parsedRoute.method,
|
||||
descriptor_ref: descriptorRef,
|
||||
descriptor_set_b64: descriptorSetB64,
|
||||
};
|
||||
}
|
||||
|
||||
function parseGrpcRoute(route) {
|
||||
var normalized = route.replace(/^\/+/, '');
|
||||
var parts = normalized.split('/');
|
||||
if (parts.length !== 2) {
|
||||
return {
|
||||
package: '',
|
||||
service: '',
|
||||
method: '',
|
||||
};
|
||||
}
|
||||
|
||||
var servicePart = parts[0];
|
||||
var method = parts[1];
|
||||
var serviceSegments = servicePart.split('.');
|
||||
var service = serviceSegments.pop() || '';
|
||||
var packageName = serviceSegments.join('.');
|
||||
|
||||
return {
|
||||
package: packageName,
|
||||
service: service,
|
||||
method: method,
|
||||
};
|
||||
}
|
||||
|
||||
function parseHeaderMap(text) {
|
||||
if (!text || !text.trim()) return {};
|
||||
var value = parseStructuredText(text);
|
||||
return value && typeof value === 'object' ? value : {};
|
||||
}
|
||||
|
||||
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('Tool name is required');
|
||||
|
||||
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 {
|
||||
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';
|
||||
} catch (error) {
|
||||
alert(error.message || 'Failed to save operation');
|
||||
}
|
||||
}
|
||||
|
||||
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 = 'Edit operation';
|
||||
var sidebarBrand = document.querySelector('.step-sidebar-brand');
|
||||
if (sidebarBrand) sidebarBrand.innerHTML = 'Edit <span>operation</span>';
|
||||
}
|
||||
|
||||
// Step 1: protocol
|
||||
wizardProtocol = op.protocol || 'rest';
|
||||
function selectProtocol(protocol) {
|
||||
wizardProtocol = protocol || 'rest';
|
||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||
var proto = card.dataset.protocol ||
|
||||
(card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : 'grpc');
|
||||
var sel = proto === wizardProtocol;
|
||||
card.classList.toggle('selected', sel);
|
||||
card.setAttribute('aria-checked', sel ? 'true' : 'false');
|
||||
var proto = card.dataset.protocol
|
||||
|| (card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : 'grpc');
|
||||
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) s3name.textContent = STEP3_LABELS[wizardProtocol] || 'Protocol config';
|
||||
}
|
||||
|
||||
// Step 3: endpoint path & method
|
||||
if (op.protocol === 'rest') {
|
||||
var pathEl = document.getElementById('endpoint-path');
|
||||
if (pathEl && op.target_url) {
|
||||
try {
|
||||
var url = new URL(op.target_url);
|
||||
pathEl.value = url.pathname + url.search;
|
||||
} catch (e) {
|
||||
pathEl.value = op.target_url;
|
||||
}
|
||||
}
|
||||
if (op.method) {
|
||||
document.querySelectorAll('.method-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.method === op.method);
|
||||
});
|
||||
}
|
||||
} else if (op.protocol === 'graphql') {
|
||||
var gqlEl = document.getElementById('gql-query-editor');
|
||||
if (gqlEl && op.input_mapping) gqlEl.value = op.input_mapping;
|
||||
function prefillUpstream(baseUrl, headers) {
|
||||
var known = upstreams.find(function(item) { return item.url === baseUrl; });
|
||||
if (known) {
|
||||
pickUpstream(known.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Steps 4 & 5: form fields
|
||||
var set = function(id, val) { var el = document.getElementById(id); if (el) el.value = val || ''; };
|
||||
set('tool-name', op.name);
|
||||
set('tool-display-name', op.display_name);
|
||||
set('tool-description', op.description);
|
||||
set('tool-input-schema', op.input_schema);
|
||||
set('tool-output-schema', op.output_schema);
|
||||
set('tool-input-mapping', op.input_mapping);
|
||||
set('tool-output-mapping',op.output_mapping);
|
||||
set('tool-exec-config', op.exec_config);
|
||||
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.style.display = '';
|
||||
setValue('new-upstream-name', 'imported-upstream');
|
||||
setValue('new-upstream-url', baseUrl);
|
||||
setValue('new-upstream-auth-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
|
||||
|
||||
var valueEl = document.getElementById('upstream-combobox-value');
|
||||
if (valueEl) {
|
||||
valueEl.innerHTML =
|
||||
'<div class="upstream-trigger-name">imported-upstream</div>' +
|
||||
'<div class="upstream-trigger-url">' + escapeHtml(baseUrl) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (config.protocol_options && config.protocol_options.grpc) {
|
||||
value.tls = { verify: !!config.protocol_options.grpc.use_tls };
|
||||
}
|
||||
return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function prefillWizardFromEdit(detail, versionDocument) {
|
||||
if (!detail || !versionDocument) return;
|
||||
|
||||
setEditModePresentation();
|
||||
selectProtocol(detail.protocol);
|
||||
|
||||
var target = versionDocument.target || {};
|
||||
if (target.kind === 'rest') {
|
||||
prefillUpstream(target.base_url, target.static_headers);
|
||||
setValue('endpoint-path', target.path_template);
|
||||
document.querySelectorAll('.method-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.method === target.method);
|
||||
});
|
||||
} else if (target.kind === 'graphql') {
|
||||
var endpoint = target.endpoint || '';
|
||||
var parsedEndpoint = new URL(endpoint, window.location.origin);
|
||||
prefillUpstream(parsedEndpoint.origin, {});
|
||||
setValue('endpoint-path', parsedEndpoint.pathname + parsedEndpoint.search);
|
||||
setValue('gql-query-editor', target.query_template);
|
||||
document.querySelectorAll('.gql-type-card').forEach(function(card) {
|
||||
card.classList.toggle('active', card.dataset.gqlType === target.operation_type);
|
||||
});
|
||||
} else if (target.kind === 'grpc') {
|
||||
prefillUpstream(target.server_addr, {});
|
||||
var route = '/' + (target.package ? target.package + '.' : '') + target.service + '/' + target.method;
|
||||
setValue('grpc-manual-route', route);
|
||||
setValue('grpc-manual-req-type', '');
|
||||
setValue('grpc-manual-res-type', '');
|
||||
grpcManualRouteInput(route);
|
||||
grpcManualTypeInput();
|
||||
}
|
||||
|
||||
setValue('tool-name', detail.name);
|
||||
setValue('tool-display-name', detail.display_name);
|
||||
setValue('tool-title', versionDocument.tool_description ? versionDocument.tool_description.title : detail.display_name);
|
||||
setValue('tool-description', versionDocument.tool_description ? versionDocument.tool_description.description : '');
|
||||
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.input_schema), null, 2));
|
||||
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.output_schema), null, 2));
|
||||
setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol));
|
||||
setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol));
|
||||
setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config));
|
||||
|
||||
var toolNameInput = document.getElementById('tool-name');
|
||||
if (toolNameInput) toolNameInput.disabled = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user