Files
gitops/ts_web/elements/views/secrets/index.ts
Juergen Kunz e3f67d12a3 fix(core): fix secrets scan upserts, connection health checks, and frontend improvements
- Add upsert pattern to SecretsScanService to prevent duplicate key errors on repeated scans
- Auto-test connection health on startup so status reflects reality
- Fix Actions view to read identity from appstate instead of broken localStorage hack
- Fetch both project and group secrets in parallel, add "All Scopes" filter to Secrets view
- Enable noCache on UtilityWebsiteServer to prevent stale browser cache
2026-02-24 22:50:26 +00:00

283 lines
9.1 KiB
TypeScript

import * as plugins from '../../../plugins.js';
import * as appstate from '../../../appstate.js';
import { viewHostCss } from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('gitops-view-secrets')
export class GitopsViewSecrets extends DeesElement {
@state()
accessor connectionsState: appstate.IConnectionsState = {
connections: [],
activeConnectionId: null,
};
@state()
accessor dataState: appstate.IDataState = {
projects: [],
groups: [],
secrets: [],
pipelines: [],
pipelineJobs: [],
currentJobLog: '',
};
@state()
accessor selectedConnectionId: string = '';
@state()
accessor selectedScope: 'all' | 'project' | 'group' = 'all';
@state()
accessor selectedScopeId: string = '__all__';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
.select((s) => s)
.subscribe((s) => { this.connectionsState = s; });
this.rxSubscriptions.push(connSub);
const dataSub = appstate.dataStatePart
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.loadSecrets();
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
];
private get filteredSecrets() {
let secrets = this.dataState.secrets;
// Filter by scope (unless "all")
if (this.selectedScope !== 'all') {
secrets = secrets.filter((s) => s.scope === this.selectedScope);
}
// Filter by entity if specific one selected
if (this.selectedScopeId !== '__all__') {
secrets = secrets.filter((s) => s.scopeId === this.selectedScopeId);
}
return secrets;
}
public render(): TemplateResult {
const connectionOptions = this.connectionsState.connections.map((c) => ({
option: `${c.name} (${c.providerType})`,
key: c.id,
}));
const scopeOptions = [
{ option: 'All Scopes', key: 'all' },
{ option: 'Project', key: 'project' },
{ option: 'Group', key: 'group' },
];
const entities = this.selectedScope === 'group'
? this.dataState.groups.map((g) => ({ option: g.fullPath || g.name, key: g.id }))
: this.selectedScope === 'project'
? this.dataState.projects.map((p) => ({ option: p.fullPath || p.name, key: p.id }))
: [];
const entityOptions = [
{ option: 'All', key: '__all__' },
...entities,
];
const isAllSelected = this.selectedScope === 'all' || this.selectedScopeId === '__all__';
return html`
<div class="view-title">Secrets</div>
<div class="view-description">Manage CI/CD secrets and variables</div>
<div class="toolbar">
<dees-input-dropdown
.label=${'Connection'}
.options=${connectionOptions}
.selectedOption=${connectionOptions.find((o) => o.key === this.selectedConnectionId) || connectionOptions[0]}
@selectedOption=${(e: CustomEvent) => {
this.selectedConnectionId = e.detail.key;
this.selectedScopeId = '__all__';
this.loadEntities();
this.loadSecrets();
}}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Scope'}
.options=${scopeOptions}
.selectedOption=${scopeOptions.find((o) => o.key === this.selectedScope)}
@selectedOption=${(e: CustomEvent) => {
this.selectedScope = e.detail.key as 'all' | 'project' | 'group';
this.selectedScopeId = '__all__';
this.loadEntities();
this.loadSecrets();
}}
></dees-input-dropdown>
${this.selectedScope !== 'all' ? html`
<dees-input-dropdown
.label=${this.selectedScope === 'project' ? 'Project' : 'Group'}
.options=${entityOptions}
.selectedOption=${entityOptions.find((o) => o.key === this.selectedScopeId) || entityOptions[0]}
@selectedOption=${(e: CustomEvent) => {
this.selectedScopeId = e.detail.key;
}}
></dees-input-dropdown>
` : ''}
<dees-button
.disabled=${isAllSelected}
@click=${() => this.addSecret()}
>Add Secret</dees-button>
<dees-button @click=${() => this.loadSecrets()}>Refresh</dees-button>
</div>
<dees-table
.heading1=${'Secrets'}
.heading2=${'CI/CD variables for the selected entity'}
.data=${this.filteredSecrets}
.displayFunction=${(item: any) => ({
Key: item.key,
Scope: item.scopeName || item.scopeId,
Value: item.masked ? '******' : item.value,
Protected: item.protected ? 'Yes' : 'No',
Environment: item.environment || '*',
})}
.dataActions=${[
{
name: 'Edit',
iconName: 'lucide:edit',
action: async (item: any) => { await this.editSecret(item); },
},
{
name: 'Delete',
iconName: 'lucide:trash2',
action: async (item: any) => {
await appstate.dataStatePart.dispatchAction(appstate.deleteSecretAction, {
connectionId: this.selectedConnectionId,
scope: item.scope,
scopeId: item.scopeId,
key: item.key,
});
},
},
]}
></dees-table>
`;
}
async firstUpdated() {
await appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
const conns = appstate.connectionsStatePart.getState().connections;
if (conns.length > 0 && !this.selectedConnectionId) {
this.selectedConnectionId = conns[0].id;
await this.loadEntities();
await this.loadSecrets();
}
}
private async loadEntities() {
if (!this.selectedConnectionId) return;
if (this.selectedScope === 'all') return;
if (this.selectedScope === 'project') {
await appstate.dataStatePart.dispatchAction(appstate.fetchProjectsAction, {
connectionId: this.selectedConnectionId,
});
} else {
await appstate.dataStatePart.dispatchAction(appstate.fetchGroupsAction, {
connectionId: this.selectedConnectionId,
});
}
}
private async loadSecrets() {
if (!this.selectedConnectionId) return;
// Always fetch both scopes — client-side filtering handles the rest
await appstate.dataStatePart.dispatchAction(appstate.fetchAllSecretsAction, {
connectionId: this.selectedConnectionId,
});
}
private async addSecret() {
if (this.selectedScope === 'all' || this.selectedScopeId === '__all__') return;
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Secret',
content: html`
<style>.form-row { margin-bottom: 16px; }</style>
<div class="form-row">
<dees-input-text .label=${'Key'} .key=${'key'}></dees-input-text>
</div>
<div class="form-row">
<dees-input-text .label=${'Value'} .key=${'value'} type="password"></dees-input-text>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal: any) => { modal.destroy(); } },
{
name: 'Create',
action: async (modal: any) => {
const inputs = modal.shadowRoot.querySelectorAll('dees-input-text');
const data: any = {};
for (const input of inputs) { data[input.key] = input.value || ''; }
await appstate.dataStatePart.dispatchAction(appstate.createSecretAction, {
connectionId: this.selectedConnectionId,
scope: this.selectedScope,
scopeId: this.selectedScopeId,
key: data.key,
value: data.value,
});
modal.destroy();
},
},
],
});
}
private async editSecret(item: any) {
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit Secret: ${item.key}`,
content: html`
<style>.form-row { margin-bottom: 16px; }</style>
<div class="form-row">
<dees-input-text .label=${'Value'} .key=${'value'} type="password"></dees-input-text>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal: any) => { modal.destroy(); } },
{
name: 'Update',
action: async (modal: any) => {
const input = modal.shadowRoot.querySelector('dees-input-text');
await appstate.dataStatePart.dispatchAction(appstate.updateSecretAction, {
connectionId: this.selectedConnectionId,
scope: item.scope,
scopeId: item.scopeId,
key: item.key,
value: input?.value || '',
});
modal.destroy();
},
},
],
});
}
}