From e34df9b96d46afbd50af53db34fdd814e83dd338 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sat, 2 May 2026 15:09:28 +0000 Subject: [PATCH] ui: extract wizard upstream module --- TASKS.md | 9 +- apps/ui/js/wizard-upstreams.js | 562 ++++++++++++++++++++++++++++++++ apps/ui/js/wizard.js | 564 --------------------------------- apps/ui/scripts/build.js | 1 + 4 files changed, 568 insertions(+), 568 deletions(-) create mode 100644 apps/ui/js/wizard-upstreams.js diff --git a/TASKS.md b/TASKS.md index f42bc2d..5c10c24 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,14 +2,14 @@ ## Current -### `feat/frontend-performance-polish` +### `feat/frontend-wizard-modularization` Status: in_progress DoD: -- repeated derived views are cached or precomputed instead of recalculated per render path -- UI interaction handlers avoid unnecessary full rerenders for small state changes -- performance changes keep current page behavior and pass existing smoke checks +- `wizard.js` is split by responsibility into smaller files with stable ownership boundaries +- extracted modules keep existing wizard behavior and asset build flow +- modularization changes pass UI build and targeted wizard smoke checks ## Next @@ -27,6 +27,7 @@ DoD: - `feat/frontend-wizard-modularization` - `feat/frontend-css-state-cleanup` - `feat/frontend-performance-polish` +- `feat/frontend-wizard-modularization` - `feat/frontend-observability-and-testability` - `feat/id-display-support` - `feat/postgres-pool-config` diff --git a/apps/ui/js/wizard-upstreams.js b/apps/ui/js/wizard-upstreams.js new file mode 100644 index 0000000..e2063c6 --- /dev/null +++ b/apps/ui/js/wizard-upstreams.js @@ -0,0 +1,562 @@ +/* ── Upstream selector (Step 2) ── */ + +var upstreams = [ + { id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, + { id: 'countries-graphql', name: 'Countries GraphQL', url: 'https://countries.trevorblades.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, + { id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', 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'); + + var reflectName = document.getElementById('grpc-reflect-upstream-name'); + var reflectUrl = document.getElementById('grpc-reflect-upstream-url'); + if (reflectName) reflectName.textContent = u.name; + if (reflectUrl) reflectUrl.textContent = u.url; +} + +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') + ); + } +} diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js index 8b9d5dc..ac2e8b4 100644 --- a/apps/ui/js/wizard.js +++ b/apps/ui/js/wizard.js @@ -223,570 +223,6 @@ function selectGqlType(btn) { if (defaults[type]) editor.value = defaults[type]; } -/* ── Upstream selector (Step 2) ── */ - -var upstreams = [ - { id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, - { id: 'countries-graphql', name: 'Countries GraphQL', url: 'https://countries.trevorblades.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, - { id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null } -]; - -var dropdownOpen = false; - -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(); } - - // Close new-upstream form if open - 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; - - // Update combobox trigger value - 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)); - } - } - - // Show preview strip - 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; - } - - // Hide new-upstream form and reset trigger - 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'); - - // Update reflection panel upstream info strip - var reflectName = document.getElementById('grpc-reflect-upstream-name'); - var reflectUrl = document.getElementById('grpc-reflect-upstream-url'); - if (reflectName) reflectName.textContent = u.name; - if (reflectUrl) reflectUrl.textContent = u.url; -} - -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) { - // toggle off — restore previous state - if (form) form.hidden = true; - if (trigger) trigger.classList.remove('active'); - return; - } - - // Clear combobox trigger & hide preview - var val = document.getElementById('upstream-combobox-value'); - if (val) setUpstreamComboboxPlaceholder(val); - var preview = document.getElementById('upstream-preview'); - if (preview) preview.hidden = true; - selectedUpstreamId = null; - - // Mark trigger active and show form - 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') - ); - } -} - function escapeHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } diff --git a/apps/ui/scripts/build.js b/apps/ui/scripts/build.js index c93a3e5..b159993 100644 --- a/apps/ui/scripts/build.js +++ b/apps/ui/scripts/build.js @@ -103,6 +103,7 @@ const BUNDLES = { 'js/streaming-form.js', 'js/stream-test-run.js', 'js/wizard.js', + 'js/wizard-upstreams.js', 'js/nav.js', ], },