Files
cloudly/ts_web/elements/cloudly-dashboard.ts
T

355 lines
13 KiB
TypeScript

import { commitinfo } from '../00_commitinfo_data.js';
import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import { appRouter } from '../router.js';
import {
DeesElement,
css,
cssManager,
customElement,
html,
state
} from '@design.estate/dees-element';
import { CloudlyViewBackups } from './views/backups/index.js';
import { CloudlyViewBaseOs } from './views/baseos/index.js';
import { CloudlyViewClusters } from './views/clusters/index.js';
import { CloudlyViewDbs } from './views/dbs/index.js';
import { CloudlyViewDeployments } from './views/deployments/index.js';
import { CloudlyViewDns } from './views/dns/index.js';
import { CloudlyViewDomains } from './views/domains/index.js';
import { CloudlyViewAppStore } from './views/appstore/index.js';
import { CloudlyViewImages } from './views/images/index.js';
import { CloudlyViewLogs } from './views/logs/index.js';
import { CloudlyViewMails } from './views/mails/index.js';
import { CloudlyViewOverview } from './views/overview/index.js';
import { CloudlyViewS3 } from './views/s3/index.js';
import { CloudlyViewSecretBundles } from './views/secretbundles/index.js';
import { CloudlyViewSecretGroups } from './views/secretgroups/index.js';
import { CloudlyViewServices } from './views/services/index.js';
import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js';
import { CloudlyViewSettings } from './views/settings/index.js';
import { CloudlyViewTasks } from './views/tasks/index.js';
declare global {
interface HTMLElementTagNameMap {
'cvault-dashboard': CloudlyDashboard;
}
}
interface ICloudlyView extends plugins.deesCatalog.IView {
slug?: string;
subViews?: ICloudlyView[];
}
@customElement('cloudly-dashboard')
export class CloudlyDashboard extends DeesElement {
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
@state() private accessor data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
clusters: [],
};
@state() private accessor uiState: appstate.IUiState = {
activeView: 'overview',
activeSubview: null,
};
// Keep view tabs stable across renders to preserve active selection
private readonly viewTabs: ICloudlyView[] = [
{
slug: 'overview',
name: 'Overview',
iconName: 'lucide:LayoutDashboard',
element: CloudlyViewOverview,
},
{
slug: 'platform',
name: 'Platform',
iconName: 'lucide:Settings',
subViews: [
{ slug: 'settings', name: 'Settings', iconName: 'lucide:Settings', element: CloudlyViewSettings },
{ slug: 'baseos', name: 'BaseOS', iconName: 'lucide:HardDriveDownload', element: CloudlyViewBaseOs },
{ slug: 'fleet', name: 'Fleet', iconName: 'lucide:Truck', element: CloudlyViewBackups },
],
},
{
slug: 'runtime',
name: 'Runtime',
iconName: 'lucide:Network',
subViews: [
{ slug: 'clusters', name: 'Clusters', iconName: 'lucide:Network', element: CloudlyViewClusters },
{ slug: 'appstore', name: 'App Store', iconName: 'lucide:Store', element: CloudlyViewAppStore },
{ slug: 'services', name: 'Services', iconName: 'lucide:Layers', element: CloudlyViewServices },
{ slug: 'images', name: 'Images', iconName: 'lucide:Image', element: CloudlyViewImages },
{ slug: 'deployments', name: 'Deployments', iconName: 'lucide:Rocket', element: CloudlyViewDeployments },
{ slug: 'tasks', name: 'Tasks', iconName: 'lucide:ListChecks', element: CloudlyViewTasks },
],
},
{
slug: 'registry',
name: 'Registry & Build',
iconName: 'lucide:Package',
subViews: [
{ slug: 'externalregistries', name: 'ExternalRegistries', iconName: 'lucide:Package', element: CloudlyViewExternalRegistries },
{ slug: 'testing', name: 'Testing & Building', iconName: 'lucide:HardHat', element: CloudlyViewServices },
],
},
{
slug: 'secrets',
name: 'Secrets',
iconName: 'lucide:ShieldCheck',
subViews: [
{ slug: 'secretgroups', name: 'SecretGroups', iconName: 'lucide:ShieldCheck', element: CloudlyViewSecretGroups },
{ slug: 'secretbundles', name: 'SecretBundles', iconName: 'lucide:LockKeyhole', element: CloudlyViewSecretBundles },
],
},
{
slug: 'domains',
name: 'Domains & Messaging',
iconName: 'lucide:Globe2',
subViews: [
{ slug: 'domains', name: 'Domains', iconName: 'lucide:Globe2', element: CloudlyViewDomains },
{ slug: 'dns', name: 'DNS', iconName: 'lucide:Globe', element: CloudlyViewDns },
{ slug: 'mails', name: 'Mails', iconName: 'lucide:Mail', element: CloudlyViewMails },
],
},
{
slug: 'storage',
name: 'Storage',
iconName: 'lucide:Database',
subViews: [
{ slug: 's3', name: 's3', iconName: 'lucide:Cloud', element: CloudlyViewS3 },
{ slug: 'dbs', name: 'DBs', iconName: 'lucide:Database', element: CloudlyViewDbs },
{ slug: 'backups', name: 'Backups', iconName: 'lucide:Save', element: CloudlyViewBackups },
],
},
{
slug: 'logs',
name: 'Logs',
iconName: 'lucide:FileText',
element: CloudlyViewLogs,
},
];
private slugFor(view: ICloudlyView): string {
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '');
}
private findParent(view: ICloudlyView): ICloudlyView | undefined {
return this.viewTabs.find((viewTab) => viewTab.subViews?.includes(view));
}
private findViewBySlug(viewSlug: string, subviewSlug: string | null): ICloudlyView | undefined {
const topLevelView = this.viewTabs.find((view) => this.slugFor(view) === viewSlug);
if (!topLevelView) return undefined;
if (subviewSlug && topLevelView.subViews) {
return topLevelView.subViews.find((subview) => this.slugFor(subview) === subviewSlug) ?? topLevelView;
}
return topLevelView;
}
private get currentViewTab(): ICloudlyView {
return this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.viewTabs[0];
}
constructor() {
super();
document.title = `cloudly v${commitinfo.version}`;
const subcription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subcription);
const uiSubscription = appstate.uiStatePart
.select((stateArg) => stateArg)
.subscribe((uiState) => {
this.uiState = uiState;
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
});
this.rxSubscriptions.push(uiSubscription);
const loginSubscription = appstate.loginStatePart
.select((stateArg) => stateArg?.identity ?? null)
.subscribe((identityArg) => {
const hadIdentity = !!this.identity;
this.identity = identityArg ?? null;
if (!identityArg && hadIdentity) {
void this.switchToLoginContent('Session expired. Please sign in again.');
}
});
this.rxSubscriptions.push(loginSubscription);
}
private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
if (!appDash) return;
const targetView = this.findViewBySlug(viewSlug, subviewSlug);
if (!targetView || appDash.selectedView === targetView) return;
appDash.loadView(targetView);
}
public static styles = [
cssManager.defaultStyles,
css`
.maincontainer {
position: relative;
width: 100vw;
height: 100vh;
}
h1 {
font-weight: 400;
font-size: 24px;
font-family: 'Cal Sans';
}
`,
];
public render() {
return html`
<div class="maincontainer">
<dees-simple-login name="cloudly v${commitinfo.version}">
<dees-simple-appdash name="cloudly v${commitinfo.version}"
.viewTabs=${this.viewTabs}
.selectedView=${this.currentViewTab}
></dees-simple-appdash>
</dees-simple-login>
</div>
`;
}
public async firstUpdated() {
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
simpleLogin.addEventListener('login', (eventArg: Event) => {
const loginEvent = eventArg as CustomEvent;
this.login(loginEvent.detail.data.username, loginEvent.detail.data.password);
});
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
if (appDash) {
appDash.addEventListener('view-select', (eventArg: Event) => {
const view = (eventArg as CustomEvent).detail.view as ICloudlyView;
const parent = this.findParent(view);
const currentState = appstate.uiStatePart.getState();
if (parent) {
const parentSlug = this.slugFor(parent);
const subviewSlug = this.slugFor(view);
if (currentState?.activeView === parentSlug && currentState?.activeSubview === subviewSlug) {
return;
}
appRouter.navigateToView(parentSlug, subviewSlug);
} else {
const slug = this.slugFor(view);
if (currentState?.activeView === slug && !currentState?.activeSubview) {
return;
}
appRouter.navigateToView(slug);
}
});
}
this.addEventListener('contextmenu', (eventArg) => {
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'About',
iconName: 'lucide:Coffee',
action: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'About',
content: html`cloudly ${commitinfo.version}`,
menuOptions: [
{
name: 'close',
iconName: undefined,
action: async (modalArg: any) => {
await modalArg.destroy();
},
},
],
});
},
},
]);
});
// lets deal with initial state
const domtools = await this.domtoolsPromise;
const loginState = appstate.loginStatePart.getState();
if (loginState?.identity) {
const identityValid = await appstate.validateStoredIdentity();
const currentIdentity = appstate.loginStatePart.getState()?.identity ?? null;
if (!identityValid || !currentIdentity) {
await this.switchToLoginContent('Session expired. Please sign in again.');
return;
}
this.identity = currentIdentity;
try {
appstate.apiClient.identity = currentIdentity;
if (!appstate.apiClient['typedsocketClient']) {
await appstate.apiClient.start();
}
try { await appstate.apiClient.typedsocketClient.setTag('identity', appstate.apiClient.identity); } catch {}
} catch (e) { console.warn('Failed to initialize API client WS', e); }
await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
}
}
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;
form.setStatus('pending', 'Logging in...');
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
username,
password,
});
if (state?.identity) {
this.identity = state.identity;
form.setStatus('success', 'Logged in!');
await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
} else {
form.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000);
form.reset();
}
}
private async switchToLoginContent(statusMessageArg?: string) {
const simpleLogin = this.shadowRoot?.querySelector('dees-simple-login') as any;
if (!simpleLogin?.shadowRoot) return;
const loginDiv = simpleLogin.shadowRoot.querySelector('.login') as HTMLDivElement | null;
const loginContainerDiv = simpleLogin.shadowRoot.querySelector('.loginContainer') as HTMLDivElement | null;
const slotContainerDiv = simpleLogin.shadowRoot.querySelector('.slotContainer') as HTMLDivElement | null;
const form = simpleLogin.shadowRoot.querySelector('dees-form') as any;
if (loginDiv) {
loginDiv.style.opacity = '1';
loginDiv.style.transform = 'translateY(0px)';
}
if (loginContainerDiv) {
loginContainerDiv.style.pointerEvents = 'all';
}
if (slotContainerDiv) {
slotContainerDiv.style.opacity = '0';
slotContainerDiv.style.transform = 'translateY(20px)';
slotContainerDiv.style.pointerEvents = 'none';
}
if (form && statusMessageArg) {
form.setStatus('error', statusMessageArg);
}
}
private async logout() {
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
await this.switchToLoginContent();
}
}