355 lines
13 KiB
TypeScript
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();
|
|
}
|
|
}
|