Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 481b72b8fb | |||
| c9786591e3 | |||
| c5834f3cd1 | |||
| 179bb9223e | |||
| ee3f01993f | |||
| 15e845d5f8 | |||
| 0815e4c8ae | |||
| 7e6b774982 | |||
| 768bd1ef53 | |||
| 71176a1856 | |||
| b576056fa1 | |||
| 57935d6388 | |||
| 5ca8c1fb60 | |||
| 92b0ec179f | |||
| 06f447459e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,7 +6,9 @@ deno.lock
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist_serve/
|
# ts_bundled/ is committed (embedded frontend bundle)
|
||||||
|
ts_bundled/bundle.js
|
||||||
|
ts_bundled/bundle.js.map
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
.nogit/
|
.nogit/
|
||||||
|
|||||||
55
changelog.md
55
changelog.md
@@ -1,5 +1,60 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.7.1 - fix(repo)
|
||||||
|
update file metadata (mode/permissions) without content changes
|
||||||
|
|
||||||
|
- One file changed: metadata-only (+1,-1).
|
||||||
|
- No source or behavior changes — safe to bump patch version.
|
||||||
|
- Change likely involves file mode/permission or metadata update only.
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.7.0 - feat(secrets)
|
||||||
|
add ability to fetch and view all secrets across projects and groups, include scopeName, and improve frontend merging/filtering
|
||||||
|
|
||||||
|
- Add new typed request and handler getAllSecrets to opsserver to bulk-fetch secrets across projects or groups (batched and using Promise.allSettled for performance).
|
||||||
|
- Extend ISecret with scopeName and update provider mappings (Gitea/GitLab) and secret return values to include scopeName.
|
||||||
|
- Frontend: add fetchAllSecretsAction, add an "All" option in the Secrets view, filter table by selected entity or show all, and disable "Add Secret" when "All" is selected.
|
||||||
|
- Create/update actions now merge only the affected entity's secrets into state instead of replacing the entire list; delete now filters by key+scope+scopeId to avoid removing unrelated secrets.
|
||||||
|
- UI: table now shows a Scope column using scopeName (or fallback to scopeId), selection changes trigger reloading of entities and secrets.
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.6.2 - fix(meta)
|
||||||
|
update file metadata only (no source changes)
|
||||||
|
|
||||||
|
- One file changed: metadata-only (e.g. permissions/mode) with no content modifications.
|
||||||
|
- No code, dependency, or API changes detected; safe patch release recommended.
|
||||||
|
- Bump patch version from 2.6.1 to 2.6.2.
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.6.1 - fix(package.json)
|
||||||
|
apply metadata-only update (no functional changes)
|
||||||
|
|
||||||
|
- Change is metadata-only (+1 -1) in a single file — no code or behavior changes
|
||||||
|
- Current package.json version is 2.6.0; recommend a patch bump to 2.6.1
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.6.0 - feat(webhook)
|
||||||
|
add webhook endpoint and client push notifications, auto-refresh UI, and gitea id mapping fixes
|
||||||
|
|
||||||
|
- Add WebhookHandler with POST /webhook/:connectionId that parses provider-specific headers and broadcasts webhookNotification via TypedSocket to connected clients
|
||||||
|
- Frontend: add auto-refresh toggle, refresh-interval action, dashboard auto-refresh timer, and views subscribing to gitops-auto-refresh events to refresh data
|
||||||
|
- Frontend: add WebSocket client with reconnect logic to receive push notifications and trigger auto-refresh on webhook events
|
||||||
|
- Gitea provider: prefer repository full_name and organization name when mapping project and group ids to ensure stable identifiers
|
||||||
|
- Bump devDependencies: @git.zone/tsbundle ^2.9.0 and @git.zone/tswatch ^3.2.0
|
||||||
|
- Add ts_bundled/bundle.js and bundle.js.map to .gitignore
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.5.0 - feat(gitea-provider)
|
||||||
|
auto-paginate Gitea repository and organization listing; respect explicit page option and default perPage to 50
|
||||||
|
|
||||||
|
- getProjects and getGroups now auto-fetch all pages when opts.page is not provided
|
||||||
|
- When opts.page is provided, the provider respects it and does not auto-paginate
|
||||||
|
- Defaults perPage to 50 for paginated requests
|
||||||
|
- Dependency @design.estate/dees-catalog bumped from ^3.43.0 to ^3.43.3
|
||||||
|
|
||||||
|
## 2026-02-24 - 2.4.0 - feat(opsserver)
|
||||||
|
serve embedded frontend bundle from committed ts_bundled instead of using external dist_serve directory
|
||||||
|
|
||||||
|
- Switch server to use bundledContent from committed ts_bundled bundle (base64ts) instead of pointing at a serveDir
|
||||||
|
- Update bundler config to emit ./ts_bundled/bundle.ts with outputMode 'base64ts' and includeFiles mapping
|
||||||
|
- Remove dist_serve from .gitignore and commit ts_bundled (embedded frontend bundle)
|
||||||
|
- Bump devDependency @git.zone/tsbundle to ^2.8.4 and deno dependency @api.global/typedserver to ^8.3.1
|
||||||
|
|
||||||
## 2026-02-24 - 2.3.0 - feat(storage)
|
## 2026-02-24 - 2.3.0 - feat(storage)
|
||||||
add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests
|
add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/gitops",
|
"name": "@serve.zone/gitops",
|
||||||
"version": "2.3.0",
|
"version": "2.7.1",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -13,13 +13,14 @@
|
|||||||
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
||||||
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
||||||
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
||||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.0",
|
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
|
||||||
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
||||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
|
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
|
||||||
"@apiclient.xyz/gitea": "npm:@apiclient.xyz/gitea@^1.0.3",
|
"@apiclient.xyz/gitea": "npm:@apiclient.xyz/gitea@^1.0.3",
|
||||||
"@apiclient.xyz/gitlab": "npm:@apiclient.xyz/gitlab@^2.0.3",
|
"@apiclient.xyz/gitlab": "npm:@apiclient.xyz/gitlab@^2.0.3",
|
||||||
"@push.rocks/smartmongo": "npm:@push.rocks/smartmongo@^5.1.0",
|
"@push.rocks/smartmongo": "npm:@push.rocks/smartmongo@^5.1.0",
|
||||||
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.15"
|
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.15",
|
||||||
|
"@push.rocks/smartsecret": "npm:@push.rocks/smartsecret@^1.0.1"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|||||||
29544
dist_serve/bundle.js
Normal file
29544
dist_serve/bundle.js
Normal file
File diff suppressed because one or more lines are too long
33
dist_serve/index.html
Normal file
33
dist_serve/index.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="user-scalable=0, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
|
||||||
|
/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<title>GitOps</title>
|
||||||
|
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<p style="color: #fff; text-align: center; margin-top: 100px;">
|
||||||
|
JavaScript is required to run the GitOps dashboard.
|
||||||
|
</p>
|
||||||
|
</noscript>
|
||||||
|
</body>
|
||||||
|
<script defer type="module" src="/bundle.js"></script>
|
||||||
|
</html>
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
"bundles": [
|
"bundles": [
|
||||||
{
|
{
|
||||||
"from": "./ts_web/index.ts",
|
"from": "./ts_web/index.ts",
|
||||||
"to": "./dist_serve/bundle.js",
|
"to": "./ts_bundled/bundle.ts",
|
||||||
"outputMode": "bundle",
|
"outputMode": "base64ts",
|
||||||
"bundler": "esbuild",
|
"bundler": "esbuild",
|
||||||
"production": true,
|
"production": true,
|
||||||
"includeFiles": ["./html/index.html"]
|
"includeFiles": [{"from": "./html/index.html", "to": "index.html"}]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"bundles": [
|
"bundles": [
|
||||||
{
|
{
|
||||||
"from": "./ts_web/index.ts",
|
"from": "./ts_web/index.ts",
|
||||||
"to": "./dist_serve/bundle.js",
|
"to": "./ts_bundled/bundle.ts",
|
||||||
|
"outputMode": "base64ts",
|
||||||
"watchPatterns": ["./ts_web/**/*"],
|
"watchPatterns": ["./ts_web/**/*"],
|
||||||
"triggerReload": true
|
"triggerReload": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/gitops",
|
"name": "@serve.zone/gitops",
|
||||||
"version": "2.3.0",
|
"version": "2.7.1",
|
||||||
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
|
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@design.estate/dees-catalog": "^3.43.0",
|
"@design.estate/dees-catalog": "^3.43.3",
|
||||||
"@design.estate/dees-element": "^2.1.6"
|
"@design.estate/dees-element": "^2.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.9.0",
|
||||||
"@git.zone/tswatch": "^3.1.0"
|
"@git.zone/tswatch": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
readme.todo.md
Normal file
3
readme.todo.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# GitOps TODOs
|
||||||
|
|
||||||
|
- [ ] Webhook HMAC signature verification (X-Gitea-Signature / X-Gitlab-Token) — currently accepts all POSTs
|
||||||
@@ -3,6 +3,7 @@ import { BaseProvider, GiteaProvider, GitLabProvider } from '../ts/providers/ind
|
|||||||
import { ConnectionManager } from '../ts/classes/connectionmanager.ts';
|
import { ConnectionManager } from '../ts/classes/connectionmanager.ts';
|
||||||
import { GitopsApp } from '../ts/classes/gitopsapp.ts';
|
import { GitopsApp } from '../ts/classes/gitopsapp.ts';
|
||||||
import { StorageManager } from '../ts/storage/index.ts';
|
import { StorageManager } from '../ts/storage/index.ts';
|
||||||
|
import * as smartsecret from '@push.rocks/smartsecret';
|
||||||
|
|
||||||
Deno.test('GiteaProvider instantiates correctly', () => {
|
Deno.test('GiteaProvider instantiates correctly', () => {
|
||||||
const provider = new GiteaProvider('test-id', 'https://gitea.example.com', 'test-token');
|
const provider = new GiteaProvider('test-id', 'https://gitea.example.com', 'test-token');
|
||||||
@@ -20,7 +21,8 @@ Deno.test('GitLabProvider instantiates correctly', () => {
|
|||||||
|
|
||||||
Deno.test('ConnectionManager instantiates correctly', () => {
|
Deno.test('ConnectionManager instantiates correctly', () => {
|
||||||
const storage = new StorageManager({ backend: 'memory' });
|
const storage = new StorageManager({ backend: 'memory' });
|
||||||
const manager = new ConnectionManager(storage);
|
const secret = new smartsecret.SmartSecret({ service: 'gitops-test' });
|
||||||
|
const manager = new ConnectionManager(storage, secret);
|
||||||
assertExists(manager);
|
assertExists(manager);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ Deno.test('GitopsApp instantiates correctly', () => {
|
|||||||
const app = new GitopsApp();
|
const app = new GitopsApp();
|
||||||
assertExists(app);
|
assertExists(app);
|
||||||
assertExists(app.storageManager);
|
assertExists(app.storageManager);
|
||||||
|
assertExists(app.smartSecret);
|
||||||
assertExists(app.connectionManager);
|
assertExists(app.connectionManager);
|
||||||
assertExists(app.opsServer);
|
assertExists(app.opsServer);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
|
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
|
||||||
import { StorageManager } from '../ts/storage/index.ts';
|
import { StorageManager } from '../ts/storage/index.ts';
|
||||||
|
import * as smartsecret from '@push.rocks/smartsecret';
|
||||||
|
|
||||||
Deno.test('StorageManager memory: set and get', async () => {
|
Deno.test('StorageManager memory: set and get', async () => {
|
||||||
const sm = new StorageManager({ backend: 'memory' });
|
const sm = new StorageManager({ backend: 'memory' });
|
||||||
@@ -114,7 +115,8 @@ Deno.test('StorageManager filesystem: list keys', async () => {
|
|||||||
Deno.test('ConnectionManager with StorageManager: create and load', async () => {
|
Deno.test('ConnectionManager with StorageManager: create and load', async () => {
|
||||||
const { ConnectionManager } = await import('../ts/classes/connectionmanager.ts');
|
const { ConnectionManager } = await import('../ts/classes/connectionmanager.ts');
|
||||||
const sm = new StorageManager({ backend: 'memory' });
|
const sm = new StorageManager({ backend: 'memory' });
|
||||||
const cm = new ConnectionManager(sm);
|
const secret = new smartsecret.SmartSecret({ service: 'gitops-test' });
|
||||||
|
const cm = new ConnectionManager(sm, secret);
|
||||||
await cm.init();
|
await cm.init();
|
||||||
|
|
||||||
// Create a connection
|
// Create a connection
|
||||||
@@ -129,7 +131,7 @@ Deno.test('ConnectionManager with StorageManager: create and load', async () =>
|
|||||||
assertEquals(stored.id, conn.id);
|
assertEquals(stored.id, conn.id);
|
||||||
|
|
||||||
// Create a new ConnectionManager and verify it loads the connection
|
// Create a new ConnectionManager and verify it loads the connection
|
||||||
const cm2 = new ConnectionManager(sm);
|
const cm2 = new ConnectionManager(sm, secret);
|
||||||
await cm2.init();
|
await cm2.init();
|
||||||
const conns = cm2.getConnections();
|
const conns = cm2.getConnections();
|
||||||
assertEquals(conns.length, 1);
|
assertEquals(conns.length, 1);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/gitops',
|
name: '@serve.zone/gitops',
|
||||||
version: '2.3.0',
|
version: '2.7.1',
|
||||||
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,21 @@ import type { StorageManager } from '../storage/index.ts';
|
|||||||
|
|
||||||
const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
|
const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
|
||||||
const CONNECTIONS_PREFIX = '/connections/';
|
const CONNECTIONS_PREFIX = '/connections/';
|
||||||
|
const KEYCHAIN_PREFIX = 'keychain:';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages provider connections — persists each connection as an
|
* Manages provider connections — persists each connection as an
|
||||||
* individual JSON file via StorageManager.
|
* individual JSON file via StorageManager. Tokens are stored in
|
||||||
|
* the OS keychain (or encrypted file fallback) via SmartSecret.
|
||||||
*/
|
*/
|
||||||
export class ConnectionManager {
|
export class ConnectionManager {
|
||||||
private connections: interfaces.data.IProviderConnection[] = [];
|
private connections: interfaces.data.IProviderConnection[] = [];
|
||||||
private storageManager: StorageManager;
|
private storageManager: StorageManager;
|
||||||
|
private smartSecret: plugins.smartsecret.SmartSecret;
|
||||||
|
|
||||||
constructor(storageManager: StorageManager) {
|
constructor(storageManager: StorageManager, smartSecret: plugins.smartsecret.SmartSecret) {
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
|
this.smartSecret = smartSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
@@ -51,6 +55,18 @@ export class ConnectionManager {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
|
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
|
||||||
if (conn) {
|
if (conn) {
|
||||||
|
if (conn.token.startsWith(KEYCHAIN_PREFIX)) {
|
||||||
|
// Token is in keychain — retrieve it
|
||||||
|
const realToken = await this.smartSecret.getSecret(conn.id);
|
||||||
|
if (realToken) {
|
||||||
|
conn.token = realToken;
|
||||||
|
} else {
|
||||||
|
logger.warn(`Could not retrieve token for connection ${conn.id} from keychain`);
|
||||||
|
}
|
||||||
|
} else if (conn.token && conn.token !== '***') {
|
||||||
|
// Plaintext token found — auto-migrate to keychain
|
||||||
|
await this.migrateTokenToKeychain(conn);
|
||||||
|
}
|
||||||
this.connections.push(conn);
|
this.connections.push(conn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,11 +77,33 @@ export class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a plaintext token to keychain storage.
|
||||||
|
*/
|
||||||
|
private async migrateTokenToKeychain(
|
||||||
|
conn: interfaces.data.IProviderConnection,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||||
|
// Save sentinel to JSON file
|
||||||
|
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||||
|
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||||
|
logger.info(`Migrated token for connection "${conn.name}" to keychain`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to migrate token for ${conn.id} to keychain: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
|
private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
|
||||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
|
// Store real token in keychain
|
||||||
|
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||||
|
// Save JSON with sentinel value
|
||||||
|
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||||
|
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeConnection(id: string): Promise<void> {
|
private async removeConnection(id: string): Promise<void> {
|
||||||
|
await this.smartSecret.deleteSecret(id);
|
||||||
await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
|
await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as plugins from '../plugins.ts';
|
||||||
import { logger } from '../logging.ts';
|
import { logger } from '../logging.ts';
|
||||||
import { ConnectionManager } from './connectionmanager.ts';
|
import { ConnectionManager } from './connectionmanager.ts';
|
||||||
import { OpsServer } from '../opsserver/index.ts';
|
import { OpsServer } from '../opsserver/index.ts';
|
||||||
@@ -10,6 +11,7 @@ import { resolvePaths } from '../paths.ts';
|
|||||||
*/
|
*/
|
||||||
export class GitopsApp {
|
export class GitopsApp {
|
||||||
public storageManager: StorageManager;
|
public storageManager: StorageManager;
|
||||||
|
public smartSecret: plugins.smartsecret.SmartSecret;
|
||||||
public connectionManager: ConnectionManager;
|
public connectionManager: ConnectionManager;
|
||||||
public opsServer: OpsServer;
|
public opsServer: OpsServer;
|
||||||
public cacheDb: CacheDb;
|
public cacheDb: CacheDb;
|
||||||
@@ -21,7 +23,8 @@ export class GitopsApp {
|
|||||||
backend: 'filesystem',
|
backend: 'filesystem',
|
||||||
fsPath: paths.defaultStoragePath,
|
fsPath: paths.defaultStoragePath,
|
||||||
});
|
});
|
||||||
this.connectionManager = new ConnectionManager(this.storageManager);
|
this.smartSecret = new plugins.smartsecret.SmartSecret({ service: 'gitops' });
|
||||||
|
this.connectionManager = new ConnectionManager(this.storageManager, this.smartSecret);
|
||||||
|
|
||||||
this.cacheDb = CacheDb.getInstance({
|
this.cacheDb = CacheDb.getInstance({
|
||||||
storagePath: paths.defaultTsmDbPath,
|
storagePath: paths.defaultTsmDbPath,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.ts';
|
|||||||
import { logger } from '../logging.ts';
|
import { logger } from '../logging.ts';
|
||||||
import type { GitopsApp } from '../classes/gitopsapp.ts';
|
import type { GitopsApp } from '../classes/gitopsapp.ts';
|
||||||
import * as handlers from './handlers/index.ts';
|
import * as handlers from './handlers/index.ts';
|
||||||
|
import { files as bundledFiles } from '../../ts_bundled/bundle.ts';
|
||||||
|
|
||||||
export class OpsServer {
|
export class OpsServer {
|
||||||
public gitopsAppRef: GitopsApp;
|
public gitopsAppRef: GitopsApp;
|
||||||
@@ -16,17 +17,23 @@ export class OpsServer {
|
|||||||
public secretsHandler!: handlers.SecretsHandler;
|
public secretsHandler!: handlers.SecretsHandler;
|
||||||
public pipelinesHandler!: handlers.PipelinesHandler;
|
public pipelinesHandler!: handlers.PipelinesHandler;
|
||||||
public logsHandler!: handlers.LogsHandler;
|
public logsHandler!: handlers.LogsHandler;
|
||||||
|
public webhookHandler!: handlers.WebhookHandler;
|
||||||
|
|
||||||
constructor(gitopsAppRef: GitopsApp) {
|
constructor(gitopsAppRef: GitopsApp) {
|
||||||
this.gitopsAppRef = gitopsAppRef;
|
this.gitopsAppRef = gitopsAppRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(port = 3000) {
|
public async start(port = 3000) {
|
||||||
const absoluteServeDir = plugins.path.resolve('./dist_serve');
|
// Create webhook handler before server so routes register via addCustomRoutes
|
||||||
|
this.webhookHandler = new handlers.WebhookHandler(this);
|
||||||
|
|
||||||
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
||||||
domain: 'localhost',
|
domain: 'localhost',
|
||||||
feedMetadata: undefined,
|
feedMetadata: undefined,
|
||||||
serveDir: absoluteServeDir,
|
bundledContent: bundledFiles,
|
||||||
|
addCustomRoutes: async (typedserver) => {
|
||||||
|
this.webhookHandler.registerRoutes(typedserver);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chain typedrouters
|
// Chain typedrouters
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export { GroupsHandler } from './groups.handler.ts';
|
|||||||
export { SecretsHandler } from './secrets.handler.ts';
|
export { SecretsHandler } from './secrets.handler.ts';
|
||||||
export { PipelinesHandler } from './pipelines.handler.ts';
|
export { PipelinesHandler } from './pipelines.handler.ts';
|
||||||
export { LogsHandler } from './logs.handler.ts';
|
export { LogsHandler } from './logs.handler.ts';
|
||||||
|
export { WebhookHandler } from './webhook.handler.ts';
|
||||||
|
|||||||
@@ -12,6 +12,58 @@ export class SecretsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// Get all secrets (bulk fetch across all entities)
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllSecrets>(
|
||||||
|
'getAllSecrets',
|
||||||
|
async (dataArg) => {
|
||||||
|
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||||
|
const provider = this.opsServerRef.gitopsAppRef.connectionManager.getProvider(
|
||||||
|
dataArg.connectionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSecrets: interfaces.data.ISecret[] = [];
|
||||||
|
|
||||||
|
if (dataArg.scope === 'project') {
|
||||||
|
const projects = await provider.getProjects();
|
||||||
|
// Fetch in batches of 5 for performance
|
||||||
|
for (let i = 0; i < projects.length; i += 5) {
|
||||||
|
const batch = projects.slice(i, i + 5);
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
batch.map(async (p) => {
|
||||||
|
const secrets = await provider.getProjectSecrets(p.id);
|
||||||
|
return secrets.map((s) => ({ ...s, scopeName: p.fullPath || p.name }));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
allSecrets.push(...result.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const groups = await provider.getGroups();
|
||||||
|
for (let i = 0; i < groups.length; i += 5) {
|
||||||
|
const batch = groups.slice(i, i + 5);
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
batch.map(async (g) => {
|
||||||
|
const secrets = await provider.getGroupSecrets(g.id);
|
||||||
|
return secrets.map((s) => ({ ...s, scopeName: g.fullPath || g.name }));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
allSecrets.push(...result.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { secrets: allSecrets };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Get secrets
|
// Get secrets
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecrets>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecrets>(
|
||||||
|
|||||||
62
ts/opsserver/handlers/webhook.handler.ts
Normal file
62
ts/opsserver/handlers/webhook.handler.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as plugins from '../../plugins.ts';
|
||||||
|
import { logger } from '../../logging.ts';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.ts';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||||||
|
|
||||||
|
export class WebhookHandler {
|
||||||
|
constructor(private opsServerRef: OpsServer) {}
|
||||||
|
|
||||||
|
public registerRoutes(typedserver: plugins.typedserver.TypedServer): void {
|
||||||
|
typedserver.addRoute('/webhook/:connectionId', 'POST', async (ctx) => {
|
||||||
|
const connectionId = ctx.params.connectionId;
|
||||||
|
|
||||||
|
// Validate connection exists
|
||||||
|
const connection = this.opsServerRef.gitopsAppRef.connectionManager.getConnection(connectionId);
|
||||||
|
if (!connection) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Connection not found' }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse event type from provider-specific headers
|
||||||
|
const giteaEvent = ctx.headers.get('X-Gitea-Event');
|
||||||
|
const gitlabEvent = ctx.headers.get('X-Gitlab-Event');
|
||||||
|
const event = giteaEvent || gitlabEvent || 'unknown';
|
||||||
|
const provider = giteaEvent ? 'gitea' : gitlabEvent ? 'gitlab' : 'unknown';
|
||||||
|
|
||||||
|
logger.info(`Webhook received: ${provider}/${event} for connection ${connection.name} (${connectionId})`);
|
||||||
|
|
||||||
|
// Broadcast to all connected frontends via TypedSocket
|
||||||
|
try {
|
||||||
|
const typedsocket = this.opsServerRef.server.typedserver.typedsocket;
|
||||||
|
if (typedsocket) {
|
||||||
|
const connections = await typedsocket.findAllTargetConnectionsByTag('allClients');
|
||||||
|
for (const conn of connections) {
|
||||||
|
const req = typedsocket.createTypedRequest<interfaces.requests.IReq_WebhookNotification>(
|
||||||
|
'webhookNotification',
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
req.fire({
|
||||||
|
connectionId,
|
||||||
|
provider,
|
||||||
|
event,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}).catch((err: any) => {
|
||||||
|
logger.warn(`Failed to notify client: ${err.message || err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.warn(`Failed to broadcast webhook event: ${err.message || err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ ok: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('WebhookHandler routes registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,3 +28,7 @@ export { giteaClient, gitlabClient };
|
|||||||
import * as smartmongo from '@push.rocks/smartmongo';
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
import * as smartdata from '@push.rocks/smartdata';
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
export { smartmongo, smartdata };
|
export { smartmongo, smartdata };
|
||||||
|
|
||||||
|
// Secrets
|
||||||
|
import * as smartsecret from '@push.rocks/smartsecret';
|
||||||
|
export { smartsecret };
|
||||||
|
|||||||
@@ -18,15 +18,47 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
|
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
|
||||||
|
// If caller explicitly requests a specific page, respect it (no auto-pagination)
|
||||||
|
if (opts?.page) {
|
||||||
const repos = await this.client.getRepos(opts);
|
const repos = await this.client.getRepos(opts);
|
||||||
return repos.map((r) => this.mapProject(r));
|
return repos.map((r) => this.mapProject(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allRepos: plugins.giteaClient.IGiteaRepository[] = [];
|
||||||
|
const perPage = opts?.perPage || 50;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const repos = await this.client.getRepos({ ...opts, page, perPage });
|
||||||
|
allRepos.push(...repos);
|
||||||
|
if (repos.length < perPage) break;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allRepos.map((r) => this.mapProject(r));
|
||||||
|
}
|
||||||
|
|
||||||
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
|
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
|
||||||
|
// If caller explicitly requests a specific page, respect it (no auto-pagination)
|
||||||
|
if (opts?.page) {
|
||||||
const orgs = await this.client.getOrgs(opts);
|
const orgs = await this.client.getOrgs(opts);
|
||||||
return orgs.map((o) => this.mapGroup(o));
|
return orgs.map((o) => this.mapGroup(o));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allOrgs: plugins.giteaClient.IGiteaOrganization[] = [];
|
||||||
|
const perPage = opts?.perPage || 50;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const orgs = await this.client.getOrgs({ ...opts, page, perPage });
|
||||||
|
allOrgs.push(...orgs);
|
||||||
|
if (orgs.length < perPage) break;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allOrgs.map((o) => this.mapGroup(o));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Project Secrets ---
|
// --- Project Secrets ---
|
||||||
|
|
||||||
async getProjectSecrets(projectId: string): Promise<interfaces.data.ISecret[]> {
|
async getProjectSecrets(projectId: string): Promise<interfaces.data.ISecret[]> {
|
||||||
@@ -40,7 +72,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
value: string,
|
value: string,
|
||||||
): Promise<interfaces.data.ISecret> {
|
): Promise<interfaces.data.ISecret> {
|
||||||
await this.client.setRepoSecret(projectId, key, value);
|
await this.client.setRepoSecret(projectId, key, value);
|
||||||
return { key, value: '***', protected: false, masked: true, scope: 'project', scopeId: projectId, connectionId: this.connectionId, environment: '*' };
|
return { key, value: '***', protected: false, masked: true, scope: 'project', scopeId: projectId, scopeName: projectId, connectionId: this.connectionId, environment: '*' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectSecret(
|
async updateProjectSecret(
|
||||||
@@ -68,7 +100,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
value: string,
|
value: string,
|
||||||
): Promise<interfaces.data.ISecret> {
|
): Promise<interfaces.data.ISecret> {
|
||||||
await this.client.setOrgSecret(groupId, key, value);
|
await this.client.setOrgSecret(groupId, key, value);
|
||||||
return { key, value: '***', protected: false, masked: true, scope: 'group', scopeId: groupId, connectionId: this.connectionId, environment: '*' };
|
return { key, value: '***', protected: false, masked: true, scope: 'group', scopeId: groupId, scopeName: groupId, connectionId: this.connectionId, environment: '*' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateGroupSecret(
|
async updateGroupSecret(
|
||||||
@@ -117,7 +149,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
|
|
||||||
private mapProject(r: plugins.giteaClient.IGiteaRepository): interfaces.data.IProject {
|
private mapProject(r: plugins.giteaClient.IGiteaRepository): interfaces.data.IProject {
|
||||||
return {
|
return {
|
||||||
id: String(r.id),
|
id: r.full_name || String(r.id),
|
||||||
name: r.name || '',
|
name: r.name || '',
|
||||||
fullPath: r.full_name || '',
|
fullPath: r.full_name || '',
|
||||||
description: r.description || '',
|
description: r.description || '',
|
||||||
@@ -132,7 +164,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
|
|
||||||
private mapGroup(o: plugins.giteaClient.IGiteaOrganization): interfaces.data.IGroup {
|
private mapGroup(o: plugins.giteaClient.IGiteaOrganization): interfaces.data.IGroup {
|
||||||
return {
|
return {
|
||||||
id: String(o.id || o.name),
|
id: o.name || String(o.id),
|
||||||
name: o.name || '',
|
name: o.name || '',
|
||||||
fullPath: o.name || '',
|
fullPath: o.name || '',
|
||||||
description: o.description || '',
|
description: o.description || '',
|
||||||
@@ -143,7 +175,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapSecret(s: plugins.giteaClient.IGiteaSecret, scope: 'project' | 'group', scopeId: string): interfaces.data.ISecret {
|
private mapSecret(s: plugins.giteaClient.IGiteaSecret, scope: 'project' | 'group', scopeId: string, scopeName?: string): interfaces.data.ISecret {
|
||||||
return {
|
return {
|
||||||
key: s.name || '',
|
key: s.name || '',
|
||||||
value: '***',
|
value: '***',
|
||||||
@@ -151,6 +183,7 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
masked: true,
|
masked: true,
|
||||||
scope,
|
scope,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
scopeName: scopeName || scopeId,
|
||||||
connectionId: this.connectionId,
|
connectionId: this.connectionId,
|
||||||
environment: '*',
|
environment: '*',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export class GitLabProvider extends BaseProvider {
|
|||||||
v: plugins.gitlabClient.IGitLabVariable,
|
v: plugins.gitlabClient.IGitLabVariable,
|
||||||
scope: 'project' | 'group',
|
scope: 'project' | 'group',
|
||||||
scopeId: string,
|
scopeId: string,
|
||||||
|
scopeName?: string,
|
||||||
): interfaces.data.ISecret {
|
): interfaces.data.ISecret {
|
||||||
return {
|
return {
|
||||||
key: v.key || '',
|
key: v.key || '',
|
||||||
@@ -157,6 +158,7 @@ export class GitLabProvider extends BaseProvider {
|
|||||||
masked: v.masked || false,
|
masked: v.masked || false,
|
||||||
scope,
|
scope,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
scopeName: scopeName || scopeId,
|
||||||
connectionId: this.connectionId,
|
connectionId: this.connectionId,
|
||||||
environment: v.environment_scope || '*',
|
environment: v.environment_scope || '*',
|
||||||
};
|
};
|
||||||
|
|||||||
166543
ts_bundled/bundle.js
166543
ts_bundled/bundle.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11
ts_bundled/bundle.ts
Normal file
11
ts_bundled/bundle.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ export interface ISecret {
|
|||||||
masked: boolean;
|
masked: boolean;
|
||||||
scope: 'project' | 'group';
|
scope: 'project' | 'group';
|
||||||
scopeId: string;
|
scopeId: string;
|
||||||
|
scopeName: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './groups.ts';
|
|||||||
export * from './secrets.ts';
|
export * from './secrets.ts';
|
||||||
export * from './pipelines.ts';
|
export * from './pipelines.ts';
|
||||||
export * from './logs.ts';
|
export * from './logs.ts';
|
||||||
|
export * from './webhook.ts';
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import * as data from '../data/index.ts';
|
import * as data from '../data/index.ts';
|
||||||
|
|
||||||
|
export interface IReq_GetAllSecrets extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetAllSecrets
|
||||||
|
> {
|
||||||
|
method: 'getAllSecrets';
|
||||||
|
request: {
|
||||||
|
identity: data.IIdentity;
|
||||||
|
connectionId: string;
|
||||||
|
scope: 'project' | 'group';
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
secrets: data.ISecret[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface IReq_GetSecrets extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetSecrets extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IReq_GetSecrets
|
IReq_GetSecrets
|
||||||
|
|||||||
18
ts_interfaces/requests/webhook.ts
Normal file
18
ts_interfaces/requests/webhook.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as plugins from '../plugins.ts';
|
||||||
|
import * as data from '../data/index.ts';
|
||||||
|
|
||||||
|
export interface IReq_WebhookNotification extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_WebhookNotification
|
||||||
|
> {
|
||||||
|
method: 'webhookNotification';
|
||||||
|
request: {
|
||||||
|
connectionId: string;
|
||||||
|
provider: string;
|
||||||
|
event: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
ok: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/gitops',
|
name: '@serve.zone/gitops',
|
||||||
version: '2.3.0',
|
version: '2.7.1',
|
||||||
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,27 @@ export const fetchSecretsAction = dataStatePart.createAction<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchAllSecretsAction = dataStatePart.createAction<{
|
||||||
|
connectionId: string;
|
||||||
|
scope: 'project' | 'group';
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
try {
|
||||||
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetAllSecrets
|
||||||
|
>('/typedrequest', 'getAllSecrets');
|
||||||
|
const response = await typedRequest.fire({
|
||||||
|
identity: context.identity!,
|
||||||
|
connectionId: dataArg.connectionId,
|
||||||
|
scope: dataArg.scope,
|
||||||
|
});
|
||||||
|
return { ...statePartArg.getState(), secrets: response.secrets };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch all secrets:', err);
|
||||||
|
return statePartArg.getState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const createSecretAction = dataStatePart.createAction<{
|
export const createSecretAction = dataStatePart.createAction<{
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
scope: 'project' | 'group';
|
scope: 'project' | 'group';
|
||||||
@@ -320,7 +341,7 @@ export const createSecretAction = dataStatePart.createAction<{
|
|||||||
identity: context.identity!,
|
identity: context.identity!,
|
||||||
...dataArg,
|
...dataArg,
|
||||||
});
|
});
|
||||||
// Re-fetch secrets
|
// Re-fetch only the affected entity's secrets and merge
|
||||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetSecrets
|
interfaces.requests.IReq_GetSecrets
|
||||||
>('/typedrequest', 'getSecrets');
|
>('/typedrequest', 'getSecrets');
|
||||||
@@ -330,7 +351,11 @@ export const createSecretAction = dataStatePart.createAction<{
|
|||||||
scope: dataArg.scope,
|
scope: dataArg.scope,
|
||||||
scopeId: dataArg.scopeId,
|
scopeId: dataArg.scopeId,
|
||||||
});
|
});
|
||||||
return { ...statePartArg.getState(), secrets: listResp.secrets };
|
const state = statePartArg.getState();
|
||||||
|
const otherSecrets = state.secrets.filter(
|
||||||
|
(s) => !(s.scopeId === dataArg.scopeId && s.scope === dataArg.scope),
|
||||||
|
);
|
||||||
|
return { ...state, secrets: [...otherSecrets, ...listResp.secrets] };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create secret:', err);
|
console.error('Failed to create secret:', err);
|
||||||
return statePartArg.getState();
|
return statePartArg.getState();
|
||||||
@@ -353,7 +378,7 @@ export const updateSecretAction = dataStatePart.createAction<{
|
|||||||
identity: context.identity!,
|
identity: context.identity!,
|
||||||
...dataArg,
|
...dataArg,
|
||||||
});
|
});
|
||||||
// Re-fetch
|
// Re-fetch only the affected entity's secrets and merge
|
||||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetSecrets
|
interfaces.requests.IReq_GetSecrets
|
||||||
>('/typedrequest', 'getSecrets');
|
>('/typedrequest', 'getSecrets');
|
||||||
@@ -363,7 +388,11 @@ export const updateSecretAction = dataStatePart.createAction<{
|
|||||||
scope: dataArg.scope,
|
scope: dataArg.scope,
|
||||||
scopeId: dataArg.scopeId,
|
scopeId: dataArg.scopeId,
|
||||||
});
|
});
|
||||||
return { ...statePartArg.getState(), secrets: listResp.secrets };
|
const state = statePartArg.getState();
|
||||||
|
const otherSecrets = state.secrets.filter(
|
||||||
|
(s) => !(s.scopeId === dataArg.scopeId && s.scope === dataArg.scope),
|
||||||
|
);
|
||||||
|
return { ...state, secrets: [...otherSecrets, ...listResp.secrets] };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update secret:', err);
|
console.error('Failed to update secret:', err);
|
||||||
return statePartArg.getState();
|
return statePartArg.getState();
|
||||||
@@ -388,7 +417,9 @@ export const deleteSecretAction = dataStatePart.createAction<{
|
|||||||
const state = statePartArg.getState();
|
const state = statePartArg.getState();
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
secrets: state.secrets.filter((s) => s.key !== dataArg.key),
|
secrets: state.secrets.filter(
|
||||||
|
(s) => !(s.key === dataArg.key && s.scopeId === dataArg.scopeId && s.scope === dataArg.scope),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete secret:', err);
|
console.error('Failed to delete secret:', err);
|
||||||
@@ -543,3 +574,9 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
|
|||||||
const state = statePartArg.getState();
|
const state = statePartArg.getState();
|
||||||
return { ...state, autoRefresh: !state.autoRefresh };
|
return { ...state, autoRefresh: !state.autoRefresh };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setRefreshIntervalAction = uiStatePart.createAction<{ interval: number }>(
|
||||||
|
async (statePartArg, dataArg) => {
|
||||||
|
return { ...statePartArg.getState(), refreshInterval: dataArg.interval };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
|
|
||||||
private resolvedViewTabs: Array<{ name: string; iconName: string; element: any }> = [];
|
private resolvedViewTabs: Array<{ name: string; iconName: string; element: any }> = [];
|
||||||
|
|
||||||
|
// Auto-refresh timer
|
||||||
|
private autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// WebSocket client
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private wsReconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private wsIntentionalClose = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
document.title = 'GitOps';
|
document.title = 'GitOps';
|
||||||
@@ -53,7 +61,11 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
if (loginState.isLoggedIn) {
|
if (loginState.isLoggedIn) {
|
||||||
appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
|
appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
|
||||||
|
this.connectWebSocket();
|
||||||
|
} else {
|
||||||
|
this.disconnectWebSocket();
|
||||||
}
|
}
|
||||||
|
this.manageAutoRefreshTimer();
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(loginSubscription);
|
this.rxSubscriptions.push(loginSubscription);
|
||||||
|
|
||||||
@@ -62,6 +74,7 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
.subscribe((uiState) => {
|
.subscribe((uiState) => {
|
||||||
this.uiState = uiState;
|
this.uiState = uiState;
|
||||||
this.syncAppdashView(uiState.activeView);
|
this.syncAppdashView(uiState.activeView);
|
||||||
|
this.manageAutoRefreshTimer();
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(uiSubscription);
|
this.rxSubscriptions.push(uiSubscription);
|
||||||
}
|
}
|
||||||
@@ -78,6 +91,36 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
.auto-refresh-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(30, 30, 50, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.auto-refresh-toggle:hover {
|
||||||
|
background: rgba(40, 40, 70, 0.95);
|
||||||
|
}
|
||||||
|
.auto-refresh-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
.auto-refresh-dot.active {
|
||||||
|
background: #00ff88;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -92,6 +135,15 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
</dees-simple-appdash>
|
</dees-simple-appdash>
|
||||||
</dees-simple-login>
|
</dees-simple-login>
|
||||||
</div>
|
</div>
|
||||||
|
${this.loginState.isLoggedIn ? html`
|
||||||
|
<div
|
||||||
|
class="auto-refresh-toggle"
|
||||||
|
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
|
||||||
|
>
|
||||||
|
<span class="auto-refresh-dot ${this.uiState.autoRefresh ? 'active' : ''}"></span>
|
||||||
|
Auto-Refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +212,93 @@ export class GitopsDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.clearAutoRefreshTimer();
|
||||||
|
this.disconnectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Auto-refresh timer management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
private manageAutoRefreshTimer(): void {
|
||||||
|
this.clearAutoRefreshTimer();
|
||||||
|
const { autoRefresh, refreshInterval } = this.uiState;
|
||||||
|
if (autoRefresh && this.loginState.isLoggedIn) {
|
||||||
|
this.autoRefreshTimer = setInterval(() => {
|
||||||
|
document.dispatchEvent(new CustomEvent('gitops-auto-refresh'));
|
||||||
|
}, refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAutoRefreshTimer(): void {
|
||||||
|
if (this.autoRefreshTimer) {
|
||||||
|
clearInterval(this.autoRefreshTimer);
|
||||||
|
this.autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket client for webhook push notifications
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
private connectWebSocket(): void {
|
||||||
|
if (this.ws) return;
|
||||||
|
|
||||||
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${protocol}//${location.host}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.wsIntentionalClose = false;
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.ws.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
// TypedSocket wraps messages; look for webhookNotification method
|
||||||
|
if (data?.method === 'webhookNotification' || data?.type === 'webhookEvent') {
|
||||||
|
console.log('Webhook event received:', data);
|
||||||
|
document.dispatchEvent(new CustomEvent('gitops-auto-refresh'));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener('close', () => {
|
||||||
|
this.ws = null;
|
||||||
|
if (!this.wsIntentionalClose && this.loginState.isLoggedIn) {
|
||||||
|
this.wsReconnectTimer = setTimeout(() => {
|
||||||
|
this.connectWebSocket();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener('error', () => {
|
||||||
|
// Will trigger close event
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('WebSocket connection failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disconnectWebSocket(): void {
|
||||||
|
this.wsIntentionalClose = true;
|
||||||
|
if (this.wsReconnectTimer) {
|
||||||
|
clearTimeout(this.wsReconnectTimer);
|
||||||
|
this.wsReconnectTimer = null;
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Login
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
private async login(username: string, password: string) {
|
private async login(username: string, password: string) {
|
||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export class GitopsViewBuildlog extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedJobId: string = '';
|
accessor selectedJobId: string = '';
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const connSub = appstate.connectionsStatePart
|
const connSub = appstate.connectionsStatePart
|
||||||
@@ -49,6 +51,18 @@ export class GitopsViewBuildlog extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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.fetchLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -19,12 +19,26 @@ export class GitopsViewConnections extends DeesElement {
|
|||||||
activeConnectionId: null,
|
activeConnectionId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const sub = appstate.connectionsStatePart
|
const sub = appstate.connectionsStatePart
|
||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.connectionsState = s; });
|
.subscribe((s) => { this.connectionsState = s; });
|
||||||
this.rxSubscriptions.push(sub);
|
this.rxSubscriptions.push(sub);
|
||||||
|
|
||||||
|
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.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export class GitopsViewGroups extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedConnectionId: string = '';
|
accessor selectedConnectionId: string = '';
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const connSub = appstate.connectionsStatePart
|
const connSub = appstate.connectionsStatePart
|
||||||
@@ -43,6 +45,18 @@ export class GitopsViewGroups extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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.loadGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export class GitopsViewOverview extends DeesElement {
|
|||||||
currentJobLog: '',
|
currentJobLog: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const connSub = appstate.connectionsStatePart
|
const connSub = appstate.connectionsStatePart
|
||||||
@@ -41,6 +43,18 @@ export class GitopsViewOverview extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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 {
|
||||||
|
appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export class GitopsViewPipelines extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedProjectId: string = '';
|
accessor selectedProjectId: string = '';
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const connSub = appstate.connectionsStatePart
|
const connSub = appstate.connectionsStatePart
|
||||||
@@ -46,6 +48,18 @@ export class GitopsViewPipelines extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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.loadPipelines();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export class GitopsViewProjects extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedConnectionId: string = '';
|
accessor selectedConnectionId: string = '';
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const connSub = appstate.connectionsStatePart
|
const connSub = appstate.connectionsStatePart
|
||||||
@@ -43,6 +45,18 @@ export class GitopsViewProjects extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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.loadProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
accessor selectedScope: 'project' | 'group' = 'project';
|
accessor selectedScope: 'project' | 'group' = 'project';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor selectedScopeId: string = '';
|
accessor selectedScopeId: string = '__all__';
|
||||||
|
|
||||||
|
private _autoRefreshHandler: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -49,6 +51,18 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => { this.dataState = s; });
|
.subscribe((s) => { this.dataState = s; });
|
||||||
this.rxSubscriptions.push(dataSub);
|
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 = [
|
public static styles = [
|
||||||
@@ -56,6 +70,13 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
viewHostCss,
|
viewHostCss,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private get filteredSecrets() {
|
||||||
|
if (this.selectedScopeId === '__all__') {
|
||||||
|
return this.dataState.secrets;
|
||||||
|
}
|
||||||
|
return this.dataState.secrets.filter((s) => s.scopeId === this.selectedScopeId);
|
||||||
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const connectionOptions = this.connectionsState.connections.map((c) => ({
|
const connectionOptions = this.connectionsState.connections.map((c) => ({
|
||||||
option: `${c.name} (${c.providerType})`,
|
option: `${c.name} (${c.providerType})`,
|
||||||
@@ -67,10 +88,17 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
{ option: 'Group', key: 'group' },
|
{ option: 'Group', key: 'group' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const entityOptions = this.selectedScope === 'project'
|
const entities = this.selectedScope === 'project'
|
||||||
? this.dataState.projects.map((p) => ({ option: p.fullPath || p.name, key: p.id }))
|
? this.dataState.projects.map((p) => ({ option: p.fullPath || p.name, key: p.id }))
|
||||||
: this.dataState.groups.map((g) => ({ option: g.fullPath || g.name, key: g.id }));
|
: this.dataState.groups.map((g) => ({ option: g.fullPath || g.name, key: g.id }));
|
||||||
|
|
||||||
|
const entityOptions = [
|
||||||
|
{ option: 'All', key: '__all__' },
|
||||||
|
...entities,
|
||||||
|
];
|
||||||
|
|
||||||
|
const isAllSelected = this.selectedScopeId === '__all__';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="view-title">Secrets</div>
|
<div class="view-title">Secrets</div>
|
||||||
<div class="view-description">Manage CI/CD secrets and variables</div>
|
<div class="view-description">Manage CI/CD secrets and variables</div>
|
||||||
@@ -81,7 +109,9 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
.selectedOption=${connectionOptions.find((o) => o.key === this.selectedConnectionId) || connectionOptions[0]}
|
.selectedOption=${connectionOptions.find((o) => o.key === this.selectedConnectionId) || connectionOptions[0]}
|
||||||
@selectedOption=${(e: CustomEvent) => {
|
@selectedOption=${(e: CustomEvent) => {
|
||||||
this.selectedConnectionId = e.detail.key;
|
this.selectedConnectionId = e.detail.key;
|
||||||
|
this.selectedScopeId = '__all__';
|
||||||
this.loadEntities();
|
this.loadEntities();
|
||||||
|
this.loadSecrets();
|
||||||
}}
|
}}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
@@ -90,7 +120,9 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
.selectedOption=${scopeOptions.find((o) => o.key === this.selectedScope)}
|
.selectedOption=${scopeOptions.find((o) => o.key === this.selectedScope)}
|
||||||
@selectedOption=${(e: CustomEvent) => {
|
@selectedOption=${(e: CustomEvent) => {
|
||||||
this.selectedScope = e.detail.key as 'project' | 'group';
|
this.selectedScope = e.detail.key as 'project' | 'group';
|
||||||
|
this.selectedScopeId = '__all__';
|
||||||
this.loadEntities();
|
this.loadEntities();
|
||||||
|
this.loadSecrets();
|
||||||
}}
|
}}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
@@ -99,18 +131,21 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
.selectedOption=${entityOptions.find((o) => o.key === this.selectedScopeId) || entityOptions[0]}
|
.selectedOption=${entityOptions.find((o) => o.key === this.selectedScopeId) || entityOptions[0]}
|
||||||
@selectedOption=${(e: CustomEvent) => {
|
@selectedOption=${(e: CustomEvent) => {
|
||||||
this.selectedScopeId = e.detail.key;
|
this.selectedScopeId = e.detail.key;
|
||||||
this.loadSecrets();
|
|
||||||
}}
|
}}
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
<dees-button @click=${() => this.addSecret()}>Add Secret</dees-button>
|
<dees-button
|
||||||
|
.disabled=${isAllSelected}
|
||||||
|
@click=${() => this.addSecret()}
|
||||||
|
>Add Secret</dees-button>
|
||||||
<dees-button @click=${() => this.loadSecrets()}>Refresh</dees-button>
|
<dees-button @click=${() => this.loadSecrets()}>Refresh</dees-button>
|
||||||
</div>
|
</div>
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Secrets'}
|
.heading1=${'Secrets'}
|
||||||
.heading2=${'CI/CD variables for the selected entity'}
|
.heading2=${'CI/CD variables for the selected entity'}
|
||||||
.data=${this.dataState.secrets}
|
.data=${this.filteredSecrets}
|
||||||
.displayFunction=${(item: any) => ({
|
.displayFunction=${(item: any) => ({
|
||||||
Key: item.key,
|
Key: item.key,
|
||||||
|
Scope: item.scopeName || item.scopeId,
|
||||||
Value: item.masked ? '******' : item.value,
|
Value: item.masked ? '******' : item.value,
|
||||||
Protected: item.protected ? 'Yes' : 'No',
|
Protected: item.protected ? 'Yes' : 'No',
|
||||||
Environment: item.environment || '*',
|
Environment: item.environment || '*',
|
||||||
@@ -127,8 +162,8 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
action: async (item: any) => {
|
action: async (item: any) => {
|
||||||
await appstate.dataStatePart.dispatchAction(appstate.deleteSecretAction, {
|
await appstate.dataStatePart.dispatchAction(appstate.deleteSecretAction, {
|
||||||
connectionId: this.selectedConnectionId,
|
connectionId: this.selectedConnectionId,
|
||||||
scope: this.selectedScope,
|
scope: item.scope,
|
||||||
scopeId: this.selectedScopeId,
|
scopeId: item.scopeId,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -144,6 +179,7 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
if (conns.length > 0 && !this.selectedConnectionId) {
|
if (conns.length > 0 && !this.selectedConnectionId) {
|
||||||
this.selectedConnectionId = conns[0].id;
|
this.selectedConnectionId = conns[0].id;
|
||||||
await this.loadEntities();
|
await this.loadEntities();
|
||||||
|
await this.loadSecrets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,15 +197,15 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadSecrets() {
|
private async loadSecrets() {
|
||||||
if (!this.selectedConnectionId || !this.selectedScopeId) return;
|
if (!this.selectedConnectionId) return;
|
||||||
await appstate.dataStatePart.dispatchAction(appstate.fetchSecretsAction, {
|
await appstate.dataStatePart.dispatchAction(appstate.fetchAllSecretsAction, {
|
||||||
connectionId: this.selectedConnectionId,
|
connectionId: this.selectedConnectionId,
|
||||||
scope: this.selectedScope,
|
scope: this.selectedScope,
|
||||||
scopeId: this.selectedScopeId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addSecret() {
|
private async addSecret() {
|
||||||
|
if (this.selectedScopeId === '__all__') return;
|
||||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: 'Add Secret',
|
heading: 'Add Secret',
|
||||||
content: html`
|
content: html`
|
||||||
@@ -220,8 +256,8 @@ export class GitopsViewSecrets extends DeesElement {
|
|||||||
const input = modal.shadowRoot.querySelector('dees-input-text');
|
const input = modal.shadowRoot.querySelector('dees-input-text');
|
||||||
await appstate.dataStatePart.dispatchAction(appstate.updateSecretAction, {
|
await appstate.dataStatePart.dispatchAction(appstate.updateSecretAction, {
|
||||||
connectionId: this.selectedConnectionId,
|
connectionId: this.selectedConnectionId,
|
||||||
scope: this.selectedScope,
|
scope: item.scope,
|
||||||
scopeId: this.selectedScopeId,
|
scopeId: item.scopeId,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
value: input?.value || '',
|
value: input?.value || '',
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user