import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as interfaces from '../../ts_interfaces/index.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; import type { GitopsViewOverview } from './views/overview/index.js'; import type { GitopsViewConnections } from './views/connections/index.js'; import type { GitopsViewProjects } from './views/projects/index.js'; import type { GitopsViewGroups } from './views/groups/index.js'; import type { GitopsViewSecrets } from './views/secrets/index.js'; import type { GitopsViewPipelines } from './views/pipelines/index.js'; import type { GitopsViewBuildlog } from './views/buildlog/index.js'; import type { GitopsViewActions } from './views/actions/index.js'; import type { GitopsViewActionlog } from './views/actionlog/index.js'; @customElement('gitops-dashboard') export class GitopsDashboard extends DeesElement { @state() accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false }; @state() accessor uiState: appstate.IUiState = { activeView: 'overview', autoRefresh: true, refreshInterval: 30000, }; private viewTabs = [ { name: 'Overview', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./views/overview/index.js')).GitopsViewOverview)() }, { name: 'Connections', iconName: 'lucide:plug', element: (async () => (await import('./views/connections/index.js')).GitopsViewConnections)() }, { name: 'Projects', iconName: 'lucide:folderGit2', element: (async () => (await import('./views/projects/index.js')).GitopsViewProjects)() }, { name: 'Groups', iconName: 'lucide:users', element: (async () => (await import('./views/groups/index.js')).GitopsViewGroups)() }, { name: 'Secrets', iconName: 'lucide:key', element: (async () => (await import('./views/secrets/index.js')).GitopsViewSecrets)() }, { name: 'Pipelines', iconName: 'lucide:play', element: (async () => (await import('./views/pipelines/index.js')).GitopsViewPipelines)() }, { name: 'Build Log', iconName: 'lucide:scrollText', element: (async () => (await import('./views/buildlog/index.js')).GitopsViewBuildlog)() }, { name: 'Actions', iconName: 'lucide:zap', element: (async () => (await import('./views/actions/index.js')).GitopsViewActions)() }, { name: 'Action Log', iconName: 'lucide:scroll', element: (async () => (await import('./views/actionlog/index.js')).GitopsViewActionlog)() }, ]; private resolvedViewTabs: Array<{ name: string; iconName: string; element: any }> = []; // Auto-refresh timer private autoRefreshTimer: ReturnType | null = null; // WebSocket client private ws: WebSocket | null = null; private wsReconnectTimer: ReturnType | null = null; private wsIntentionalClose = false; constructor() { super(); document.title = 'GitOps'; const loginSubscription = appstate.loginStatePart .select((stateArg) => stateArg) .subscribe((loginState) => { this.loginState = loginState; if (loginState.isLoggedIn) { appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null); this.connectWebSocket(); } else { this.disconnectWebSocket(); } this.manageAutoRefreshTimer(); }); this.rxSubscriptions.push(loginSubscription); const uiSubscription = appstate.uiStatePart .select((stateArg) => stateArg) .subscribe((uiState) => { this.uiState = uiState; this.syncAppdashView(uiState.activeView); this.manageAutoRefreshTimer(); }); this.rxSubscriptions.push(uiSubscription); } public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; } .maincontainer { width: 100%; 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; } `, ]; public render(): TemplateResult { return html`
${this.loginState.isLoggedIn ? html`
appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)} > Auto-Refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
` : ''} `; } public async firstUpdated() { // Resolve async view tab imports this.resolvedViewTabs = await Promise.all( this.viewTabs.map(async (tab) => ({ name: tab.name, iconName: tab.iconName, element: await tab.element, })), ); this.requestUpdate(); await this.updateComplete; const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any; if (simpleLogin) { simpleLogin.addEventListener('login', (e: CustomEvent) => { this.login(e.detail.data.username, e.detail.data.password); }); } const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any; if (appDash) { appDash.addEventListener('view-select', (e: CustomEvent) => { const viewName = e.detail.view.name.toLowerCase(); appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: viewName }); }); appDash.addEventListener('logout', async () => { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); }); } // Load initial view on appdash if (appDash && this.resolvedViewTabs.length > 0) { const initialView = this.resolvedViewTabs.find( (t) => t.name.toLowerCase() === this.uiState.activeView, ) || this.resolvedViewTabs[0]; await appDash.loadView(initialView); } // Check for stored session (persistent login state) const loginState = appstate.loginStatePart.getState(); if (loginState.identity?.jwt) { if (loginState.identity.expiresAt > Date.now()) { try { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_VerifyIdentity >('/typedrequest', 'verifyIdentity'); const response = await typedRequest.fire({ identity: loginState.identity }); if (response.valid) { this.loginState = loginState; if (simpleLogin) { await simpleLogin.switchToSlottedContent(); } } else { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } catch (err) { console.warn('Stored session invalid, returning to login:', err); await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } else { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } } 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) { const domtools = await this.domtoolsPromise; const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any; const form = simpleLogin?.shadowRoot?.querySelector('dees-form') as any; if (form) { form.setStatus('pending', 'Logging in...'); } const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, { username, password, }); if (newState.identity) { if (form) { form.setStatus('success', 'Logged in!'); } if (simpleLogin) { await simpleLogin.switchToSlottedContent(); } } else { if (form) { form.setStatus('error', 'Login failed!'); await domtools.convenience.smartdelay.delayFor(2000); form.reset(); } } } private syncAppdashView(viewName: string): void { const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any; if (!appDash || this.resolvedViewTabs.length === 0) return; const targetTab = this.resolvedViewTabs.find((t) => t.name.toLowerCase() === viewName); if (!targetTab) return; appDash.loadView(targetTab); } }