feat: add global secrets view
This commit is contained in:
parent
dd75335727
commit
dfc53f1a5f
|
|
@ -23,6 +23,7 @@
|
||||||
'.secrets-panel{min-height:0;overflow:auto;border-right:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629)}',
|
'.secrets-panel{min-height:0;overflow:auto;border-right:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629)}',
|
||||||
'.secrets-main{min-width:0;min-height:0;overflow:auto;padding:1rem;background:var(--vt-color-background,#101020)}',
|
'.secrets-main{min-width:0;min-height:0;overflow:auto;padding:1rem;background:var(--vt-color-background,#101020)}',
|
||||||
'.secrets-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.65rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
'.secrets-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.65rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
||||||
|
'.secrets-filters{display:grid;grid-template-columns:minmax(0,1fr) minmax(8rem,.8fr);gap:.4rem;padding:.5rem .55rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
||||||
'.secrets-title{font-weight:600;font-size:.88rem}',
|
'.secrets-title{font-weight:600;font-size:.88rem}',
|
||||||
'.secrets-count{color:var(--vt-color-text-muted,#7f8aa3);font-size:.76rem}',
|
'.secrets-count{color:var(--vt-color-text-muted,#7f8aa3);font-size:.76rem}',
|
||||||
'.secrets-spacer{flex:1}',
|
'.secrets-spacer{flex:1}',
|
||||||
|
|
@ -44,10 +45,11 @@
|
||||||
'.secrets-form{display:grid;gap:.65rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);padding:.9rem;background:var(--vt-color-surface,#15152c)}',
|
'.secrets-form{display:grid;gap:.65rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);padding:.9rem;background:var(--vt-color-surface,#15152c)}',
|
||||||
'.secrets-row{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.65rem;align-items:center}',
|
'.secrets-row{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.65rem;align-items:center}',
|
||||||
'.secrets-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
'.secrets-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
'.secrets-input,.secrets-textarea,.secrets-select{width:100%;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.84rem;padding:.45rem .55rem;outline:none}',
|
'.secrets-input,.secrets-textarea,.secrets-select,.secrets-search{width:100%;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.84rem;padding:.45rem .55rem;outline:none}',
|
||||||
|
'.secrets-search{font-size:.76rem}',
|
||||||
'.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}.secrets-select option{background:#0d1117;color:var(--vt-color-text-primary,#f4f7fb)}',
|
'.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}.secrets-select option{background:#0d1117;color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
'.secrets-textarea{min-height:6rem;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}',
|
'.secrets-textarea{min-height:6rem;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}',
|
||||||
'.secrets-input:focus,.secrets-textarea:focus,.secrets-select:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
'.secrets-input:focus,.secrets-textarea:focus,.secrets-select:focus,.secrets-search:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
'.secrets-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
'.secrets-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
||||||
'.secrets-status{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3);min-height:1rem}',
|
'.secrets-status{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3);min-height:1rem}',
|
||||||
'.secrets-status.error{color:#ffc6ce}',
|
'.secrets-status.error{color:#ffc6ce}',
|
||||||
|
|
@ -58,7 +60,7 @@
|
||||||
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
||||||
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
||||||
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
||||||
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row{grid-template-columns:1fr}}'
|
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row,.secrets-filters{grid-template-columns:1fr}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
|
|
@ -97,7 +99,7 @@
|
||||||
|
|
||||||
function scopeLabel(record) {
|
function scopeLabel(record) {
|
||||||
var scope = record && record.scope || {};
|
var scope = record && record.scope || {};
|
||||||
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Workspace';
|
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Deal';
|
||||||
return 'Global';
|
return 'Global';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,6 +147,9 @@
|
||||||
var workspaceRoot = workspaceFromProps(props || {});
|
var workspaceRoot = workspaceFromProps(props || {});
|
||||||
var selectedID = selectedIDFromProps(props || {});
|
var selectedID = selectedIDFromProps(props || {});
|
||||||
var records = [];
|
var records = [];
|
||||||
|
var workspaceOptions = [];
|
||||||
|
var scopeFilter = 'all';
|
||||||
|
var searchQuery = '';
|
||||||
var selectedRecord = null;
|
var selectedRecord = null;
|
||||||
var selectedValue = '';
|
var selectedValue = '';
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
|
|
@ -162,6 +167,59 @@
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordWorkspaceRoot(record) {
|
||||||
|
var scope = record && record.scope || {};
|
||||||
|
return scope.kind === ScopeWorkspace ? cleanWorkspace(scope.workspaceRootPath) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceRoots() {
|
||||||
|
var seen = {};
|
||||||
|
var values = [];
|
||||||
|
workspaceOptions.concat(records.map(recordWorkspaceRoot)).forEach(function (value) {
|
||||||
|
value = cleanWorkspace(value);
|
||||||
|
if (!value || seen[value]) return;
|
||||||
|
seen[value] = true;
|
||||||
|
values.push(value);
|
||||||
|
});
|
||||||
|
return values.sort(function (a, b) { return a.localeCompare(b); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredRecords() {
|
||||||
|
var query = text(searchQuery).trim().toLowerCase();
|
||||||
|
return records.filter(function (record) {
|
||||||
|
var recordWorkspace = recordWorkspaceRoot(record);
|
||||||
|
if (scopeFilter === ScopeGlobal && recordWorkspace) return false;
|
||||||
|
if (scopeFilter.indexOf(ScopeWorkspace + ':') === 0 && recordWorkspace !== scopeFilter.slice((ScopeWorkspace + ':').length)) return false;
|
||||||
|
if (!query) return true;
|
||||||
|
return [record.title, record.id, record.username, recordWorkspace].some(function (value) {
|
||||||
|
return text(value).toLowerCase().indexOf(query) !== -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHiddenSelection() {
|
||||||
|
if (!selectedRecord) return;
|
||||||
|
var visible = filteredRecords();
|
||||||
|
if (visible.some(function (record) { return record.id === selectedRecord.id; })) return;
|
||||||
|
selectedRecord = null;
|
||||||
|
selectedValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWorkspaceOptions() {
|
||||||
|
if (!api || !api.files || typeof api.files.list !== 'function') return Promise.resolve();
|
||||||
|
return api.files.list('').then(function (items) {
|
||||||
|
workspaceOptions = (Array.isArray(items) ? items : []).filter(function (item) {
|
||||||
|
return text(item && item.type).toLowerCase() === 'folder';
|
||||||
|
}).map(function (item) {
|
||||||
|
return cleanWorkspace(item.relativePath || item.name);
|
||||||
|
}).filter(function (value) {
|
||||||
|
return value && value.indexOf('/') === -1;
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
workspaceOptions = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderLocked() {
|
function renderLocked() {
|
||||||
var passwordInput = el('input', {
|
var passwordInput = el('input', {
|
||||||
className: 'secrets-input',
|
className: 'secrets-input',
|
||||||
|
|
@ -227,19 +285,48 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
|
var visibleRecords = filteredRecords();
|
||||||
|
var scopeSelect = el('select', {
|
||||||
|
className: 'secrets-select',
|
||||||
|
'data-secret-scope-filter': '',
|
||||||
|
onChange: function (event) {
|
||||||
|
scopeFilter = text(event.target.value) || 'all';
|
||||||
|
clearHiddenSelection();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
el('option', { value: 'all' }, [tr('ui.scopeAll', null, 'All scopes')]),
|
||||||
|
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')])
|
||||||
|
].concat(workspaceRoots().map(function (workspace) {
|
||||||
|
return el('option', { value: ScopeWorkspace + ':' + workspace }, [workspace]);
|
||||||
|
})));
|
||||||
|
scopeSelect.value = scopeFilter;
|
||||||
|
var searchInput = el('input', {
|
||||||
|
className: 'secrets-search',
|
||||||
|
type: 'search',
|
||||||
|
'data-secret-search': '',
|
||||||
|
placeholder: tr('ui.search', null, 'Search secrets'),
|
||||||
|
value: searchQuery,
|
||||||
|
onInput: function (event) {
|
||||||
|
searchQuery = text(event.target.value);
|
||||||
|
clearHiddenSelection();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
});
|
||||||
var children = [
|
var children = [
|
||||||
el('div', { className: 'secrets-toolbar' }, [
|
el('div', { className: 'secrets-toolbar' }, [
|
||||||
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]),
|
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]),
|
||||||
el('span', { className: 'secrets-count' }, [String(records.length)]),
|
el('span', { className: 'secrets-count' }, [String(visibleRecords.length)]),
|
||||||
el('span', { className: 'secrets-spacer' }),
|
el('span', { className: 'secrets-spacer' }),
|
||||||
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')])
|
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')])
|
||||||
])
|
]),
|
||||||
|
el('div', { className: 'secrets-filters' }, [searchInput, scopeSelect])
|
||||||
];
|
];
|
||||||
if (!records.length) {
|
if (!visibleRecords.length) {
|
||||||
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
groupRecords(records).forEach(function (group) {
|
groupRecords(visibleRecords).forEach(function (group) {
|
||||||
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
||||||
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
||||||
var active = selectedRecord && selectedRecord.id === record.id;
|
var active = selectedRecord && selectedRecord.id === record.id;
|
||||||
|
|
@ -318,9 +405,26 @@
|
||||||
value.value = isEdit ? selectedValue : '';
|
value.value = isEdit ? selectedValue : '';
|
||||||
var scope = el('select', { className: 'secrets-select' }, [
|
var scope = el('select', { className: 'secrets-select' }, [
|
||||||
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]),
|
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]),
|
||||||
el('option', { value: ScopeWorkspace }, [workspaceRoot || tr('ui.workspace', null, 'Workspace')])
|
el('option', { value: ScopeWorkspace }, [tr('ui.deal', null, 'Deal')])
|
||||||
]);
|
]);
|
||||||
|
scope.setAttribute('data-secret-scope', '');
|
||||||
|
var existingWorkspace = existing ? recordWorkspaceRoot(existing) : '';
|
||||||
|
var defaultWorkspace = existingWorkspace || workspaceRoot || workspaceRoots()[0] || '';
|
||||||
|
var workspace = el('select', { className: 'secrets-select', 'data-secret-workspace': '' }, [
|
||||||
|
el('option', { value: '' }, [tr('ui.chooseWorkspace', null, 'Choose a Deal')])
|
||||||
|
].concat(workspaceRoots().map(function (item) {
|
||||||
|
return el('option', { value: item }, [item]);
|
||||||
|
})));
|
||||||
|
workspace.value = defaultWorkspace;
|
||||||
|
var workspaceRow = el('div', { className: 'secrets-row' }, [
|
||||||
|
el('label', { className: 'secrets-label' }, [tr('ui.deal', null, 'Deal')]), workspace
|
||||||
|
]);
|
||||||
|
function updateWorkspaceVisibility() {
|
||||||
|
workspaceRow.hidden = scope.value !== ScopeWorkspace;
|
||||||
|
}
|
||||||
|
scope.addEventListener('change', updateWorkspaceVisibility);
|
||||||
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
||||||
|
updateWorkspaceVisibility();
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]),
|
el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]),
|
||||||
el('div', { className: 'secrets-form' }, [
|
el('div', { className: 'secrets-form' }, [
|
||||||
|
|
@ -328,6 +432,7 @@
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]),
|
||||||
|
workspaceRow,
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]),
|
||||||
el('div', { className: 'secrets-actions' }, [
|
el('div', { className: 'secrets-actions' }, [
|
||||||
el('button', {
|
el('button', {
|
||||||
|
|
@ -336,12 +441,17 @@
|
||||||
'data-secret-save': '',
|
'data-secret-save': '',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
var nextID = text(id.value).trim() || text(title.value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '.').replace(/^\.+|\.+$/g, '');
|
var nextID = text(id.value).trim() || text(title.value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '.').replace(/^\.+|\.+$/g, '');
|
||||||
|
var targetWorkspace = cleanWorkspace(workspace.value);
|
||||||
|
if (scope.value === ScopeWorkspace && !targetWorkspace) {
|
||||||
|
setStatus(tr('ui.workspaceRequired', null, 'Choose a Deal for this secret.'), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
api.secrets.write({
|
api.secrets.write({
|
||||||
id: nextID,
|
id: nextID,
|
||||||
title: text(title.value).trim() || nextID,
|
title: text(title.value).trim() || nextID,
|
||||||
username: text(username.value).trim(),
|
username: text(username.value).trim(),
|
||||||
value: text(value.value),
|
value: text(value.value),
|
||||||
scope: scope.value === ScopeWorkspace ? { kind: ScopeWorkspace, workspaceRootPath: workspaceRoot } : { kind: ScopeGlobal }
|
scope: scope.value === ScopeWorkspace ? { kind: ScopeWorkspace, workspaceRootPath: targetWorkspace } : { kind: ScopeGlobal }
|
||||||
}).then(function (record) {
|
}).then(function (record) {
|
||||||
selectedID = record.id;
|
selectedID = record.id;
|
||||||
selectedRecord = record;
|
selectedRecord = record;
|
||||||
|
|
@ -456,7 +566,7 @@
|
||||||
api.secrets.status().then(function (status) {
|
api.secrets.status().then(function (status) {
|
||||||
initialized = !!(status && status.initialized);
|
initialized = !!(status && status.initialized);
|
||||||
unlocked = !!(status && status.unlocked);
|
unlocked = !!(status && status.unlocked);
|
||||||
if (unlocked) return loadRecords();
|
if (unlocked) return loadWorkspaceOptions().then(loadRecords);
|
||||||
render();
|
render();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = (err && err.message) ? err.message : String(err);
|
statusText = (err && err.message) ? err.message : String(err);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Secrets",
|
"manifest.name": "Secrets",
|
||||||
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
||||||
|
"contributions.views.verstak.secrets.view.title": "Secrets",
|
||||||
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
||||||
"contributions.settingsPanels.verstak.secrets.settings.title": "Secrets",
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Secrets",
|
||||||
"contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets",
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets",
|
||||||
|
|
@ -31,6 +33,11 @@
|
||||||
"ui.editSecret": "Edit secret",
|
"ui.editSecret": "Edit secret",
|
||||||
"ui.newSecret": "New secret",
|
"ui.newSecret": "New secret",
|
||||||
"ui.scope": "Scope",
|
"ui.scope": "Scope",
|
||||||
|
"ui.deal": "Deal",
|
||||||
|
"ui.scopeAll": "All scopes",
|
||||||
|
"ui.search": "Search secrets",
|
||||||
|
"ui.chooseWorkspace": "Choose a Deal",
|
||||||
|
"ui.workspaceRequired": "Choose a Deal for this secret.",
|
||||||
"ui.value": "Value",
|
"ui.value": "Value",
|
||||||
"ui.save": "Save",
|
"ui.save": "Save",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Секреты",
|
"manifest.name": "Секреты",
|
||||||
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
||||||
|
"contributions.views.verstak.secrets.view.title": "Секреты",
|
||||||
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
||||||
"contributions.settingsPanels.verstak.secrets.settings.title": "Секреты",
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Секреты",
|
||||||
"contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты",
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты",
|
||||||
|
|
@ -31,6 +33,11 @@
|
||||||
"ui.editSecret": "Изменить секрет",
|
"ui.editSecret": "Изменить секрет",
|
||||||
"ui.newSecret": "Новый секрет",
|
"ui.newSecret": "Новый секрет",
|
||||||
"ui.scope": "Область",
|
"ui.scope": "Область",
|
||||||
|
"ui.deal": "Дело",
|
||||||
|
"ui.scopeAll": "Все области",
|
||||||
|
"ui.search": "Поиск секретов",
|
||||||
|
"ui.chooseWorkspace": "Выберите дело",
|
||||||
|
"ui.workspaceRequired": "Выберите дело для этого секрета.",
|
||||||
"ui.value": "Значение",
|
"ui.value": "Значение",
|
||||||
"ui.save": "Сохранить",
|
"ui.save": "Сохранить",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"secrets.write-ui"
|
"secrets.write-ui"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"files.read",
|
||||||
"secrets.read",
|
"secrets.read",
|
||||||
"secrets.write",
|
"secrets.write",
|
||||||
"ui.register"
|
"ui.register"
|
||||||
|
|
@ -22,6 +23,23 @@
|
||||||
"entry": "frontend/src/index.js"
|
"entry": "frontend/src/index.js"
|
||||||
},
|
},
|
||||||
"contributes": {
|
"contributes": {
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"id": "verstak.secrets.view",
|
||||||
|
"title": "Secrets",
|
||||||
|
"icon": "key-round",
|
||||||
|
"component": "SecretsView"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sidebarItems": [
|
||||||
|
{
|
||||||
|
"id": "verstak.secrets.sidebar",
|
||||||
|
"title": "Secrets",
|
||||||
|
"icon": "key-round",
|
||||||
|
"view": "verstak.secrets.view",
|
||||||
|
"position": 45
|
||||||
|
}
|
||||||
|
],
|
||||||
"openProviders": [
|
"openProviders": [
|
||||||
{
|
{
|
||||||
"id": "verstak.secrets.secret",
|
"id": "verstak.secrets.secret",
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,11 @@ async function flush() {
|
||||||
if (!manifest.provides.includes('secrets.write-ui')) throw new Error('secrets manifest must provide secrets.write-ui');
|
if (!manifest.provides.includes('secrets.write-ui')) throw new Error('secrets manifest must provide secrets.write-ui');
|
||||||
if (!manifest.permissions.includes('secrets.read')) throw new Error('secrets manifest must request secrets.read');
|
if (!manifest.permissions.includes('secrets.read')) throw new Error('secrets manifest must request secrets.read');
|
||||||
if (!manifest.permissions.includes('secrets.write')) throw new Error('secrets manifest must request secrets.write');
|
if (!manifest.permissions.includes('secrets.write')) throw new Error('secrets manifest must request secrets.write');
|
||||||
|
if (!manifest.permissions.includes('files.read')) throw new Error('secrets manifest must request files.read for the Deal selector');
|
||||||
if (!manifest.permissions.includes('ui.register')) throw new Error('secrets manifest must request ui.register');
|
if (!manifest.permissions.includes('ui.register')) throw new Error('secrets manifest must request ui.register');
|
||||||
if (!(manifest.contributes.openProviders || []).some((item) => (item.supports || []).some((support) => support.kind === 'secret'))) throw new Error('secrets secret open provider missing');
|
if (!(manifest.contributes.openProviders || []).some((item) => (item.supports || []).some((support) => support.kind === 'secret'))) throw new Error('secrets secret open provider missing');
|
||||||
|
if (!(manifest.contributes.views || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets global view missing');
|
||||||
|
if (!(manifest.contributes.sidebarItems || []).some((item) => item.view === 'verstak.secrets.view')) throw new Error('secrets global sidebar item missing');
|
||||||
if (!(manifest.contributes.workspaceItems || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets workspace item missing');
|
if (!(manifest.contributes.workspaceItems || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets workspace item missing');
|
||||||
if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing');
|
if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing');
|
||||||
|
|
||||||
|
|
@ -177,6 +180,12 @@ async function flush() {
|
||||||
clipboard: {
|
clipboard: {
|
||||||
writeText: async (text) => copied.push(text),
|
writeText: async (text) => copied.push(text),
|
||||||
},
|
},
|
||||||
|
files: {
|
||||||
|
list: async () => [
|
||||||
|
{ type: 'folder', relativePath: 'ClientA' },
|
||||||
|
{ type: 'folder', relativePath: 'ClientB' },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
@ -228,6 +237,53 @@ async function flush() {
|
||||||
await flush();
|
await flush();
|
||||||
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
||||||
|
|
||||||
|
records.push({ id: 'client-a.global-view', title: 'Client A Global View', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' });
|
||||||
|
const globalContainer = document.createElement('div');
|
||||||
|
component.mount(globalContainer, {}, api);
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
if (!globalContainer.textContent.includes('Global Server') || !globalContainer.textContent.includes('Client A Global View')) {
|
||||||
|
throw new Error('global secrets view did not show global and Deal-scoped records');
|
||||||
|
}
|
||||||
|
const scopeFilter = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-scope-filter') === '');
|
||||||
|
if (!scopeFilter) throw new Error('global secrets scope filter missing');
|
||||||
|
scopeFilter.value = 'workspace:ClientA';
|
||||||
|
scopeFilter.dispatchEvent('change');
|
||||||
|
await flush();
|
||||||
|
if (!globalContainer.textContent.includes('Client A Global View') || globalContainer.textContent.includes('Global Server')) {
|
||||||
|
throw new Error('global secrets scope filter did not isolate the selected Deal');
|
||||||
|
}
|
||||||
|
const searchInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-search') === '');
|
||||||
|
if (!searchInput) throw new Error('global secrets search missing');
|
||||||
|
searchInput.value = 'no-match';
|
||||||
|
searchInput.dispatchEvent('input');
|
||||||
|
await flush();
|
||||||
|
if (!globalContainer.textContent.includes('No secrets')) throw new Error('global secrets search did not filter records');
|
||||||
|
|
||||||
|
scopeFilter.value = 'all';
|
||||||
|
scopeFilter.dispatchEvent('change');
|
||||||
|
await flush();
|
||||||
|
const newButton = walk(globalContainer, (node) => node.tagName === 'BUTTON' && node.textContent === 'New');
|
||||||
|
if (!newButton) throw new Error('global secrets new button missing');
|
||||||
|
newButton.click();
|
||||||
|
await flush();
|
||||||
|
const globalTitleInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-title') === '');
|
||||||
|
const globalValueInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-value') === '');
|
||||||
|
const scopeInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-scope') === '');
|
||||||
|
const workspaceInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-workspace') === '');
|
||||||
|
const globalSaveButton = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-save') === '');
|
||||||
|
if (!globalTitleInput || !globalValueInput || !scopeInput || !workspaceInput || !globalSaveButton) throw new Error('global Deal-scoped secret form controls missing');
|
||||||
|
globalTitleInput.value = 'Client B API';
|
||||||
|
globalValueInput.value = 'client-b-api-value';
|
||||||
|
scopeInput.value = 'workspace';
|
||||||
|
scopeInput.dispatchEvent('change');
|
||||||
|
workspaceInput.value = 'ClientB';
|
||||||
|
globalSaveButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!records.some((record) => record.title === 'Client B API' && record.scope && record.scope.workspaceRootPath === 'ClientB')) {
|
||||||
|
throw new Error('global secrets form did not create a Deal-scoped secret');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('secrets plugin smoke passed');
|
console.log('secrets plugin smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue