- 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
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import * as plugins from '../../../plugins.js';
|
|
import * as interfaces from '../../../../ts_interfaces/index.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-actions')
|
|
export class GitopsViewActions extends DeesElement {
|
|
@state()
|
|
accessor lastScanTimestamp: number = 0;
|
|
|
|
@state()
|
|
accessor isScanning: boolean = false;
|
|
|
|
@state()
|
|
accessor lastResult: {
|
|
connectionsScanned: number;
|
|
secretsFound: number;
|
|
errors: string[];
|
|
durationMs: number;
|
|
} | null = null;
|
|
|
|
@state()
|
|
accessor statusError: string = '';
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
viewHostCss,
|
|
css`
|
|
.action-cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 24px;
|
|
max-width: 720px;
|
|
}
|
|
.action-card {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 12px;
|
|
padding: 28px;
|
|
}
|
|
.action-card-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
.action-card-description {
|
|
font-size: 13px;
|
|
color: #999;
|
|
line-height: 1.5;
|
|
margin-bottom: 20px;
|
|
}
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: 140px 1fr;
|
|
gap: 8px 16px;
|
|
margin-bottom: 20px;
|
|
font-size: 13px;
|
|
}
|
|
.info-label {
|
|
color: #888;
|
|
}
|
|
.info-value {
|
|
color: #ddd;
|
|
font-family: monospace;
|
|
}
|
|
.info-value.scanning {
|
|
color: #f0c040;
|
|
}
|
|
.info-value.error {
|
|
color: #ff6060;
|
|
}
|
|
.button-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
.errors-list {
|
|
margin-top: 12px;
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
font-size: 12px;
|
|
color: #ff8080;
|
|
font-family: monospace;
|
|
line-height: 1.6;
|
|
background: rgba(255, 0, 0, 0.05);
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
const lastScanFormatted = this.lastScanTimestamp
|
|
? new Date(this.lastScanTimestamp).toLocaleString()
|
|
: 'Never';
|
|
|
|
return html`
|
|
<div class="view-title">Actions</div>
|
|
<div class="view-description">System actions and maintenance tasks</div>
|
|
<div class="action-cards">
|
|
<div class="action-card">
|
|
<div class="action-card-title">Secrets Cache Scan</div>
|
|
<div class="action-card-description">
|
|
Secrets are automatically scanned and cached every 24 hours.
|
|
Use "Force Full Scan" to trigger an immediate refresh of all secrets
|
|
across all connections, projects, and groups.
|
|
</div>
|
|
<div class="info-grid">
|
|
<div class="info-label">Status</div>
|
|
<div class="info-value ${this.isScanning ? 'scanning' : ''}">
|
|
${this.isScanning ? 'Scanning...' : 'Idle'}
|
|
</div>
|
|
<div class="info-label">Last Scan</div>
|
|
<div class="info-value">${lastScanFormatted}</div>
|
|
${this.lastResult ? html`
|
|
<div class="info-label">Connections</div>
|
|
<div class="info-value">${this.lastResult.connectionsScanned}</div>
|
|
<div class="info-label">Secrets Found</div>
|
|
<div class="info-value">${this.lastResult.secretsFound}</div>
|
|
<div class="info-label">Duration</div>
|
|
<div class="info-value">${(this.lastResult.durationMs / 1000).toFixed(1)}s</div>
|
|
${this.lastResult.errors.length > 0 ? html`
|
|
<div class="info-label">Errors</div>
|
|
<div class="info-value error">${this.lastResult.errors.length}</div>
|
|
` : ''}
|
|
` : ''}
|
|
</div>
|
|
${this.statusError ? html`
|
|
<div class="errors-list">${this.statusError}</div>
|
|
` : ''}
|
|
${this.lastResult?.errors?.length ? html`
|
|
<div class="errors-list">
|
|
${this.lastResult.errors.map((e) => html`<div>${e}</div>`)}
|
|
</div>
|
|
` : ''}
|
|
<div class="button-row">
|
|
<dees-button
|
|
.disabled=${this.isScanning}
|
|
@click=${() => this.forceScan()}
|
|
>Force Full Scan</dees-button>
|
|
<dees-button
|
|
@click=${() => this.refreshStatus()}
|
|
>Refresh Status</dees-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async firstUpdated() {
|
|
await this.refreshStatus();
|
|
}
|
|
|
|
private getIdentity(): interfaces.data.IIdentity | null {
|
|
return appstate.loginStatePart.getState().identity;
|
|
}
|
|
|
|
private async refreshStatus(): Promise<void> {
|
|
const identity = this.getIdentity();
|
|
if (!identity) return;
|
|
|
|
try {
|
|
this.statusError = '';
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetScanStatus
|
|
>('/typedrequest', 'getScanStatus');
|
|
const response = await typedRequest.fire({ identity });
|
|
this.lastScanTimestamp = response.lastScanTimestamp;
|
|
this.isScanning = response.isScanning;
|
|
this.lastResult = response.lastResult;
|
|
} catch (err) {
|
|
this.statusError = `Failed to get status: ${err}`;
|
|
}
|
|
}
|
|
|
|
private async forceScan(): Promise<void> {
|
|
const identity = this.getIdentity();
|
|
if (!identity) return;
|
|
|
|
try {
|
|
this.statusError = '';
|
|
this.isScanning = true;
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ForceScanSecrets
|
|
>('/typedrequest', 'forceScanSecrets');
|
|
const response = await typedRequest.fire({ identity });
|
|
this.lastResult = {
|
|
connectionsScanned: response.connectionsScanned,
|
|
secretsFound: response.secretsFound,
|
|
errors: response.errors,
|
|
durationMs: response.durationMs,
|
|
};
|
|
this.lastScanTimestamp = Date.now();
|
|
this.isScanning = false;
|
|
} catch (err) {
|
|
this.statusError = `Scan failed: ${err}`;
|
|
this.isScanning = false;
|
|
}
|
|
}
|
|
}
|