342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
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';
|
|
|
|
@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)() },
|
|
];
|
|
|
|
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() {
|
|
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`
|
|
<div class="maincontainer">
|
|
<dees-simple-login name="GitOps">
|
|
<dees-simple-appdash
|
|
name="GitOps"
|
|
.viewTabs=${this.resolvedViewTabs}
|
|
>
|
|
</dees-simple-appdash>
|
|
</dees-simple-login>
|
|
</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>
|
|
` : ''}
|
|
`;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|