Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 481b72b8fb | |||
| c9786591e3 | |||
| c5834f3cd1 | |||
| 179bb9223e | |||
| ee3f01993f | |||
| 15e845d5f8 | |||
| 0815e4c8ae | |||
| 7e6b774982 | |||
| 768bd1ef53 | |||
| 71176a1856 | |||
| b576056fa1 | |||
| 57935d6388 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,6 +7,8 @@ node_modules/
|
|||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
# ts_bundled/ is committed (embedded frontend bundle)
|
# ts_bundled/ is committed (embedded frontend bundle)
|
||||||
|
ts_bundled/bundle.js
|
||||||
|
ts_bundled/bundle.js.map
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
.nogit/
|
.nogit/
|
||||||
|
|||||||
47
changelog.md
47
changelog.md
@@ -1,5 +1,52 @@
|
|||||||
# 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)
|
## 2026-02-24 - 2.4.0 - feat(opsserver)
|
||||||
serve embedded frontend bundle from committed ts_bundled instead of using external dist_serve directory
|
serve embedded frontend bundle from committed ts_bundled instead of using external dist_serve directory
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/gitops",
|
"name": "@serve.zone/gitops",
|
||||||
"version": "2.4.0",
|
"version": "2.7.1",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/gitops",
|
"name": "@serve.zone/gitops",
|
||||||
"version": "2.4.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.4",
|
"@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,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/gitops',
|
name: '@serve.zone/gitops',
|
||||||
version: '2.4.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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +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) {
|
||||||
|
// 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,
|
||||||
bundledContent: bundledFiles,
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,13 +18,45 @@ export class GiteaProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
|
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
|
||||||
const repos = await this.client.getRepos(opts);
|
// If caller explicitly requests a specific page, respect it (no auto-pagination)
|
||||||
return repos.map((r) => this.mapProject(r));
|
if (opts?.page) {
|
||||||
|
const repos = await this.client.getRepos(opts);
|
||||||
|
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[]> {
|
||||||
const orgs = await this.client.getOrgs(opts);
|
// If caller explicitly requests a specific page, respect it (no auto-pagination)
|
||||||
return orgs.map((o) => this.mapGroup(o));
|
if (opts?.page) {
|
||||||
|
const orgs = await this.client.getOrgs(opts);
|
||||||
|
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 ---
|
||||||
@@ -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
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.4.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