Files
crank/apps/ui/js/secrets.js
T
github-ops 5f8149d0d1
Deploy / deploy (push) Failing after 1m43s
CI / Rust Checks (push) Successful in 5m54s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m24s
Fix workspace selection and secret modal layout
2026-06-22 18:30:56 +00:00

557 lines
19 KiB
JavaScript

function initSecretsPage() {
var state = {
workspaceId: null,
secrets: [],
profiles: [],
derived: {
activeSecretCount: 0,
secretsById: {},
usageBySecret: {},
},
search: '',
loading: false,
error: '',
modalMode: 'create',
modalSecretId: null,
};
var modal = document.getElementById('secret-modal');
var tbody = document.getElementById('secrets-tbody');
var cardList = document.getElementById('secrets-card-list');
var summary = document.getElementById('secrets-summary');
var searchInput = document.getElementById('secret-search');
var modalTitle = document.getElementById('secret-modal-title');
var modalName = document.getElementById('secret-name');
var modalKind = document.getElementById('secret-kind');
var modalNote = document.getElementById('secret-modal-note');
var modalSubmit = document.getElementById('secret-modal-submit-btn');
var kindHint = document.getElementById('secret-kind-hint');
var stringLabel = document.getElementById('secret-string-label');
var stringField = document.getElementById('secret-string-value');
var usernameField = document.getElementById('secret-username');
var passwordField = document.getElementById('secret-password');
var jsonField = document.getElementById('secret-json-value');
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : tKey(key);
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function currentWorkspace() {
return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
}
function secretKindLabel(kind) {
return tKey('secrets.kind.' + String(kind || '').toLowerCase());
}
function formatDate(value) {
if (!value) return '—';
return new Date(value).toLocaleDateString();
}
function secretIdsForProfile(profile) {
var config = profile && profile.config ? profile.config : {};
if (config.bearer) return [config.bearer.secret_id];
if (config.basic) return [config.basic.username_secret_id, config.basic.password_secret_id];
if (config.api_key_header) return [config.api_key_header.secret_id];
if (config.api_key_query) return [config.api_key_query.secret_id];
return [];
}
function buildUsageBySecret() {
var usage = {};
state.profiles.forEach(function (profile) {
secretIdsForProfile(profile).forEach(function (secretId) {
if (!secretId) return;
if (!usage[secretId]) usage[secretId] = [];
usage[secretId].push(profile);
});
});
return usage;
}
function recomputeDerivedState() {
var secretsById = {};
var activeSecretCount = 0;
state.secrets.forEach(function (secret) {
secretsById[secret.id] = secret;
if (secret.status === 'active') {
activeSecretCount += 1;
}
});
state.derived = {
activeSecretCount: activeSecretCount,
secretsById: secretsById,
usageBySecret: buildUsageBySecret(),
};
}
function resetModalFields() {
modalName.value = '';
modalKind.value = 'token';
stringField.value = '';
usernameField.value = '';
passwordField.value = '';
jsonField.value = '{\n "value": ""\n}';
}
function openModal(mode, secret) {
state.modalMode = mode;
state.modalSecretId = secret ? secret.id : null;
resetModalFields();
if (mode === 'rotate' && secret) {
modalTitle.textContent = tKey('secrets.modal.rotate_title');
modalName.value = secret.name;
modalName.disabled = true;
modalKind.value = secret.kind;
modalKind.disabled = true;
modalSubmit.textContent = tKey('secrets.modal.rotate_action');
modalNote.textContent = tKey('secrets.modal.rotate_note');
} else {
modalTitle.textContent = tKey('secrets.modal.create_title');
modalName.disabled = false;
modalKind.disabled = false;
modalSubmit.textContent = tKey('secrets.modal.create_action');
modalNote.textContent = tKey('secrets.modal.create_note');
}
updateKindFields();
modal.classList.add('open');
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureTestId === 'function') {
window.CrankDiagnostics.ensureTestId(modal, mode === 'rotate' ? 'secret-rotate-modal' : 'secret-create-modal');
}
setTimeout(function () {
if (mode === 'rotate') {
stringField.focus();
} else {
modalName.focus();
}
}, 30);
}
function closeModal() {
modal.classList.remove('open');
state.modalMode = 'create';
state.modalSecretId = null;
}
function updateKindFields() {
var kind = modalKind.value;
document.querySelectorAll('[data-kind-field="string"]').forEach(function (element) {
element.hidden = !(kind === 'token' || kind === 'header');
});
document.querySelectorAll('[data-kind-field="basic"]').forEach(function (element) {
element.hidden = kind !== 'username_password';
});
document.querySelectorAll('[data-kind-field="json"]').forEach(function (element) {
element.hidden = kind !== 'generic';
});
if (kind === 'token') {
stringLabel.textContent = tKey('secrets.modal.value');
} else if (kind === 'header') {
stringLabel.textContent = tKey('secrets.modal.header_value');
} else {
stringLabel.textContent = tKey('secrets.modal.value');
}
kindHint.textContent = tKey('secrets.hint.' + kind);
}
function buildSecretValue() {
var kind = modalKind.value;
if (kind === 'token' || kind === 'header') {
var value = stringField.value.trim();
if (!value) {
throw new Error(tKey('secrets.validation.value_required'));
}
return value;
}
if (kind === 'username_password') {
var username = usernameField.value.trim();
var password = passwordField.value;
if (!username || !password) {
throw new Error(tKey('secrets.validation.username_password_required'));
}
return {
username: username,
password: password,
};
}
try {
return JSON.parse(jsonField.value);
} catch (_error) {
throw new Error(tKey('secrets.validation.json_invalid'));
}
}
function renderSecrets() {
var usage = state.derived.usageBySecret;
var query = state.search.toLowerCase();
var rows = state.secrets.filter(function (secret) {
return !query || secret.name.toLowerCase().includes(query) || String(secret.kind || '').toLowerCase().includes(query);
});
summary.textContent = tfKey('secrets.active.subtitle', {
active: state.derived.activeSecretCount,
total: state.secrets.length,
});
window.CrankDom.clear(tbody);
if (cardList) {
window.CrankDom.clear(cardList);
}
if (state.loading && state.secrets.length === 0) {
var loadingRow = document.createElement('tr');
var loadingCell = document.createElement('td');
loadingCell.colSpan = 7;
loadingCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
loadingCell.textContent = tKey('secrets.loading');
loadingRow.appendChild(loadingCell);
tbody.appendChild(loadingRow);
renderSecretCards([], tKey('secrets.loading'));
return;
}
if (state.error) {
var errorRow = document.createElement('tr');
var errorCell = document.createElement('td');
errorCell.colSpan = 7;
errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
errorCell.textContent = state.error;
errorRow.appendChild(errorCell);
tbody.appendChild(errorRow);
renderSecretCards([], state.error);
return;
}
if (!rows.length) {
var emptyRow = document.createElement('tr');
var emptyCell = document.createElement('td');
emptyCell.colSpan = 7;
emptyCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
renderSecretCards(rows);
return;
}
rows.forEach(function (secret) {
var tr = document.createElement('tr');
var references = usage[secret.id] || [];
var nameCell = document.createElement('td');
nameCell.className = 'col-name';
nameCell.textContent = secret.name;
tr.appendChild(nameCell);
var kindCell = document.createElement('td');
kindCell.textContent = secretKindLabel(secret.kind);
tr.appendChild(kindCell);
var versionCell = document.createElement('td');
versionCell.className = 'col-mono';
versionCell.textContent = 'v' + secret.current_version;
tr.appendChild(versionCell);
var lastUsedCell = document.createElement('td');
lastUsedCell.textContent = secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never');
tr.appendChild(lastUsedCell);
var usedByCell = document.createElement('td');
usedByCell.textContent = window.tPlural('secrets.used_by_count', references.length, { count: references.length });
tr.appendChild(usedByCell);
var statusCell = document.createElement('td');
var badge = document.createElement('span');
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
badge.textContent = tKey('secrets.status.' + secret.status);
statusCell.appendChild(badge);
tr.appendChild(statusCell);
var actions = document.createElement('td');
actions.className = 'col-actions';
var rotateButton = document.createElement('button');
rotateButton.type = 'button';
rotateButton.className = 'btn-secondary table-action-btn';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.setAttribute('data-testid', 'secret-rotate-action');
rotateButton.setAttribute('data-secret-id', secret.id);
rotateButton.addEventListener('click', function () {
openModal('rotate', secret);
});
actions.appendChild(rotateButton);
var deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'btn-danger table-action-btn';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.setAttribute('data-testid', 'secret-delete-action');
deleteButton.setAttribute('data-secret-id', secret.id);
deleteButton.addEventListener('click', async function () {
await deleteSecret(secret);
});
actions.appendChild(deleteButton);
tr.appendChild(actions);
tbody.appendChild(tr);
});
renderSecretCards(rows);
}
function renderSecretCards(rows, statusText) {
if (!cardList) {
return;
}
window.CrankDom.clear(cardList);
if (statusText && !rows.length) {
var messageCard = document.createElement('div');
messageCard.className = 'resource-card';
var message = document.createElement('div');
message.className = 'resource-meta-value';
message.textContent = statusText;
message.style.color = state.error ? 'var(--danger,#f85149)' : 'var(--text-muted)';
messageCard.appendChild(message);
cardList.appendChild(messageCard);
return;
}
rows.forEach(function(secret) {
var references = state.derived.usageBySecret[secret.id] || [];
var card = document.createElement('div');
card.className = 'resource-card';
var header = document.createElement('div');
header.className = 'resource-card-header';
var headerBody = document.createElement('div');
var title = document.createElement('div');
title.className = 'resource-card-title';
title.textContent = secret.name;
var subtitle = document.createElement('div');
subtitle.className = 'resource-card-subtitle';
subtitle.textContent = secretKindLabel(secret.kind);
headerBody.appendChild(title);
headerBody.appendChild(subtitle);
var actions = document.createElement('div');
actions.className = 'resource-card-actions';
var badge = document.createElement('span');
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
badge.textContent = tKey('secrets.status.' + secret.status);
actions.appendChild(badge);
header.appendChild(headerBody);
header.appendChild(actions);
card.appendChild(header);
var metaGrid = document.createElement('div');
metaGrid.className = 'resource-meta-grid';
metaGrid.appendChild(buildSecretMetaItem('secrets.th.version', 'v' + secret.current_version));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.last_used',
secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')
));
metaGrid.appendChild(buildSecretMetaItem(
'secrets.th.used_by',
window.tPlural('secrets.used_by_count', references.length, { count: references.length })
));
card.appendChild(metaGrid);
var actionRow = document.createElement('div');
actionRow.className = 'resource-card-actions';
var rotateButton = document.createElement('button');
rotateButton.className = 'btn-secondary';
rotateButton.type = 'button';
rotateButton.textContent = tKey('secrets.action.rotate');
rotateButton.addEventListener('click', function() {
openModal('rotate', secret);
});
actionRow.appendChild(rotateButton);
var deleteButton = document.createElement('button');
deleteButton.className = 'btn-secondary';
deleteButton.type = 'button';
deleteButton.textContent = tKey('secrets.action.delete');
deleteButton.addEventListener('click', async function() {
await deleteSecret(secret);
});
actionRow.appendChild(deleteButton);
card.appendChild(actionRow);
cardList.appendChild(card);
});
}
function buildSecretMetaItem(labelKey, valueText) {
var item = document.createElement('div');
item.className = 'resource-meta-item';
var label = document.createElement('div');
label.className = 'resource-meta-label';
label.textContent = tKey(labelKey);
var value = document.createElement('div');
value.className = 'resource-meta-value';
value.textContent = valueText;
item.appendChild(label);
item.appendChild(value);
return item;
}
async function load() {
state.workspaceId = currentWorkspaceId();
state.loading = true;
state.error = '';
renderSecrets();
if (!state.workspaceId) {
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
state.loading = false;
state.error = tKey('secrets.error.workspace');
renderSecrets();
return;
}
try {
var results = await Promise.all([
window.CrankApi.listSecrets(state.workspaceId),
window.CrankApi.listAuthProfiles(state.workspaceId),
]);
state.secrets = (results[0] && results[0].items) || [];
state.profiles = (results[1] && results[1].items) || [];
recomputeDerivedState();
} catch (error) {
state.error = error.message || tKey('secrets.error.load');
state.secrets = [];
state.profiles = [];
recomputeDerivedState();
} finally {
state.loading = false;
renderSecrets();
}
}
async function deleteSecret(secret) {
if (!confirm(tfKey('secrets.confirm.delete', { name: secret.name }))) {
return;
}
try {
await window.CrankApi.deleteSecret(state.workspaceId, secret.id);
await load();
if (window.CrankUi) {
window.CrankUi.success(
tfKey('secrets.toast.delete_message', { name: secret.name }),
tKey('secrets.toast.delete_title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey('secrets.toast.delete_error_message'),
tKey('secrets.toast.delete_error_title')
);
}
}
}
async function submitModal() {
var kind = modalKind.value;
var value = buildSecretValue();
modalSubmit.disabled = true;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotating')
: tKey('secrets.modal.creating');
try {
if (state.modalMode === 'rotate') {
await window.CrankApi.rotateSecret(state.workspaceId, state.modalSecretId, { value: value });
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.rotate_message'),
tKey('secrets.toast.rotate_title')
);
}
} else {
var name = modalName.value.trim();
if (!name) {
throw new Error(tKey('secrets.validation.name_required'));
}
await window.CrankApi.createSecret(state.workspaceId, {
name: name,
kind: kind,
value: value,
});
if (window.CrankUi) {
window.CrankUi.success(
tKey('secrets.toast.create_message'),
tKey('secrets.toast.create_title')
);
}
}
closeModal();
await load();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(
error.message || tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_message' : 'secrets.toast.create_error_message'),
tKey(state.modalMode === 'rotate' ? 'secrets.toast.rotate_error_title' : 'secrets.toast.create_error_title')
);
}
} finally {
modalSubmit.disabled = false;
modalSubmit.textContent = state.modalMode === 'rotate'
? tKey('secrets.modal.rotate_action')
: tKey('secrets.modal.create_action');
}
}
document.getElementById('btn-create-secret').addEventListener('click', function () {
openModal('create');
});
document.getElementById('secret-modal-close-btn').addEventListener('click', closeModal);
document.getElementById('secret-modal-cancel-btn').addEventListener('click', closeModal);
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeModal();
}
});
modalKind.addEventListener('change', updateKindFields);
modalSubmit.addEventListener('click', function () {
void submitModal();
});
searchInput.addEventListener('input', function (event) {
state.search = event.target.value || '';
renderSecrets();
});
window.addEventListener('crank:workspacechange', function () {
void load();
});
updateKindFields();
void (async function bootSecretsPage() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
await load();
}());
}
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
window.CrankDiagnostics.bootstrap('secrets', initSecretsPage);
} else {
document.addEventListener('DOMContentLoaded', initSecretsPage);
}