feat(opsserver): introduce OpsServer (TypedRequest API) and new lightweight web UI; replace legacy Angular UI and add typed interfaces
This commit is contained in:
13
ts_web/elements/index.ts
Normal file
13
ts_web/elements/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Shared utilities
|
||||
export * from './shared/index.js';
|
||||
|
||||
// App shell
|
||||
export * from './ob-app-shell.js';
|
||||
|
||||
// View elements
|
||||
export * from './ob-view-dashboard.js';
|
||||
export * from './ob-view-services.js';
|
||||
export * from './ob-view-network.js';
|
||||
export * from './ob-view-registries.js';
|
||||
export * from './ob-view-tokens.js';
|
||||
export * from './ob-view-settings.js';
|
||||
207
ts_web/elements/ob-app-shell.ts
Normal file
207
ts_web/elements/ob-app-shell.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
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 { ObViewDashboard } from './ob-view-dashboard.js';
|
||||
import type { ObViewServices } from './ob-view-services.js';
|
||||
import type { ObViewNetwork } from './ob-view-network.js';
|
||||
import type { ObViewRegistries } from './ob-view-registries.js';
|
||||
import type { ObViewTokens } from './ob-view-tokens.js';
|
||||
import type { ObViewSettings } from './ob-view-settings.js';
|
||||
|
||||
@customElement('ob-app-shell')
|
||||
export class ObAppShell extends DeesElement {
|
||||
@state()
|
||||
accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false };
|
||||
|
||||
@state()
|
||||
accessor uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor loginLoading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor loginError: string = '';
|
||||
|
||||
private viewTabs = [
|
||||
{ name: 'Dashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
|
||||
{ name: 'Services', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
|
||||
{ name: 'Network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
|
||||
{ name: 'Registries', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
|
||||
{ name: 'Tokens', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
|
||||
{ name: 'Settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
|
||||
];
|
||||
|
||||
private resolvedViewTabs: Array<{ name: string; element: any }> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
document.title = 'Onebox';
|
||||
|
||||
const loginSubscription = appstate.loginStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((loginState) => {
|
||||
this.loginState = loginState;
|
||||
if (loginState.isLoggedIn) {
|
||||
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
|
||||
}
|
||||
});
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
this.syncAppdashView(uiState.activeView);
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.maincontainer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="maincontainer">
|
||||
<dees-simple-login name="Onebox">
|
||||
<dees-simple-appdash
|
||||
name="Onebox"
|
||||
.viewTabs=${this.resolvedViewTabs}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
// Resolve async view tab imports
|
||||
this.resolvedViewTabs = await Promise.all(
|
||||
this.viewTabs.map(async (tab) => ({
|
||||
name: tab.name,
|
||||
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 the initial view on the appdash now that tabs are resolved
|
||||
// (appdash's own firstUpdated already fired when viewTabs was still empty)
|
||||
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()) {
|
||||
// Validate token with server before switching to dashboard
|
||||
// (server may have restarted with a new JWT secret)
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSystemStatus
|
||||
>('/typedrequest', 'getSystemStatus');
|
||||
const response = await typedRequest.fire({ identity: loginState.identity });
|
||||
// Token is valid - switch to dashboard
|
||||
appstate.systemStatePart.setState({ status: response.status });
|
||||
this.loginState = loginState;
|
||||
if (simpleLogin) {
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
}
|
||||
} catch (err) {
|
||||
// Token rejected by server - clear session
|
||||
console.warn('Stored session invalid, returning to login:', err);
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
}
|
||||
} else {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, 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;
|
||||
|
||||
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();
|
||||
}
|
||||
await appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
|
||||
} 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;
|
||||
// Use appdash's own loadView method for proper view management
|
||||
appDash.loadView(targetTab);
|
||||
}
|
||||
}
|
||||
164
ts_web/elements/ob-view-dashboard.ts
Normal file
164
ts_web/elements/ob-view-dashboard.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-dashboard')
|
||||
export class ObViewDashboard extends DeesElement {
|
||||
@state()
|
||||
accessor systemState: appstate.ISystemState = { status: null };
|
||||
|
||||
@state()
|
||||
accessor servicesState: appstate.IServicesState = {
|
||||
services: [],
|
||||
currentService: null,
|
||||
currentServiceLogs: [],
|
||||
currentServiceStats: null,
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor networkState: appstate.INetworkState = {
|
||||
targets: [],
|
||||
stats: null,
|
||||
trafficStats: null,
|
||||
dnsRecords: [],
|
||||
domains: [],
|
||||
certificates: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const systemSub = appstate.systemStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.systemState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(systemSub);
|
||||
|
||||
const servicesSub = appstate.servicesStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.servicesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(servicesSub);
|
||||
|
||||
const networkSub = appstate.networkStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.networkState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(networkSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await Promise.all([
|
||||
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null),
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchCertificatesAction, null),
|
||||
]);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const status = this.systemState.status;
|
||||
const services = this.servicesState.services;
|
||||
const platformServices = this.servicesState.platformServices;
|
||||
const networkStats = this.networkState.stats;
|
||||
const certificates = this.networkState.certificates;
|
||||
|
||||
const runningServices = services.filter((s) => s.status === 'running').length;
|
||||
const stoppedServices = services.filter((s) => s.status === 'stopped').length;
|
||||
|
||||
const validCerts = certificates.filter((c) => c.isValid).length;
|
||||
const expiringCerts = certificates.filter(
|
||||
(c) => c.isValid && c.expiresAt && c.expiresAt - Date.now() < 30 * 24 * 60 * 60 * 1000,
|
||||
).length;
|
||||
const expiredCerts = certificates.filter((c) => !c.isValid).length;
|
||||
|
||||
return html`
|
||||
<ob-sectionheading>Dashboard</ob-sectionheading>
|
||||
<sz-dashboard-view
|
||||
.data=${{
|
||||
cluster: {
|
||||
totalServices: services.length,
|
||||
running: runningServices,
|
||||
stopped: stoppedServices,
|
||||
dockerStatus: status?.docker?.running ? 'running' : 'stopped',
|
||||
},
|
||||
resourceUsage: {
|
||||
cpu: status?.docker?.cpuUsage || 0,
|
||||
memoryUsed: status?.docker?.memoryUsage || 0,
|
||||
memoryTotal: status?.docker?.memoryTotal || 0,
|
||||
networkIn: 0,
|
||||
networkOut: 0,
|
||||
topConsumers: [],
|
||||
},
|
||||
platformServices: platformServices.map((ps) => ({
|
||||
name: ps.displayName,
|
||||
status: ps.status === 'running' ? 'running' : 'stopped',
|
||||
running: ps.status === 'running',
|
||||
})),
|
||||
traffic: {
|
||||
requests: 0,
|
||||
errors: 0,
|
||||
errorPercent: 0,
|
||||
avgResponse: 0,
|
||||
reqPerMin: 0,
|
||||
status2xx: 0,
|
||||
status3xx: 0,
|
||||
status4xx: 0,
|
||||
status5xx: 0,
|
||||
},
|
||||
proxy: {
|
||||
httpPort: networkStats?.proxy?.httpPort || 80,
|
||||
httpsPort: networkStats?.proxy?.httpsPort || 443,
|
||||
httpActive: networkStats?.proxy?.running || false,
|
||||
httpsActive: networkStats?.proxy?.running || false,
|
||||
routeCount: networkStats?.proxy?.routes || 0,
|
||||
},
|
||||
certificates: {
|
||||
valid: validCerts,
|
||||
expiring: expiringCerts,
|
||||
expired: expiredCerts,
|
||||
},
|
||||
dnsConfigured: true,
|
||||
acmeConfigured: true,
|
||||
quickActions: [
|
||||
{ label: 'Deploy Service', icon: 'lucide:Plus', primary: true },
|
||||
{ label: 'Add Domain', icon: 'lucide:Globe' },
|
||||
{ label: 'View Logs', icon: 'lucide:FileText' },
|
||||
],
|
||||
}}
|
||||
@action-click=${(e: CustomEvent) => this.handleQuickAction(e)}
|
||||
></sz-dashboard-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleQuickAction(e: CustomEvent) {
|
||||
const action = e.detail?.action || e.detail?.label;
|
||||
if (action === 'Deploy Service') {
|
||||
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'services' });
|
||||
} else if (action === 'Add Domain') {
|
||||
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'network' });
|
||||
}
|
||||
}
|
||||
}
|
||||
197
ts_web/elements/ob-view-network.ts
Normal file
197
ts_web/elements/ob-view-network.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-network')
|
||||
export class ObViewNetwork extends DeesElement {
|
||||
@state()
|
||||
accessor networkState: appstate.INetworkState = {
|
||||
targets: [],
|
||||
stats: null,
|
||||
trafficStats: null,
|
||||
dnsRecords: [],
|
||||
domains: [],
|
||||
certificates: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor currentTab: 'proxy' | 'dns' | 'domains' | 'domain-detail' = 'proxy';
|
||||
|
||||
@state()
|
||||
accessor selectedDomain: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const networkSub = appstate.networkStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.networkState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(networkSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await Promise.all([
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkTargetsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchTrafficStatsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchDnsRecordsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchDomainsAction, null),
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchCertificatesAction, null),
|
||||
]);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
switch (this.currentTab) {
|
||||
case 'dns':
|
||||
return this.renderDnsView();
|
||||
case 'domains':
|
||||
return this.renderDomainsView();
|
||||
case 'domain-detail':
|
||||
return this.renderDomainDetailView();
|
||||
default:
|
||||
return this.renderProxyView();
|
||||
}
|
||||
}
|
||||
|
||||
private renderProxyView(): TemplateResult {
|
||||
const stats = this.networkState.stats;
|
||||
return html`
|
||||
<ob-sectionheading>Network</ob-sectionheading>
|
||||
<sz-network-proxy-view
|
||||
.proxyStatus=${stats?.proxy?.running ? 'running' : 'stopped'}
|
||||
.routeCount=${String(stats?.proxy?.routes || 0)}
|
||||
.certificateCount=${String(stats?.proxy?.certificates || 0)}
|
||||
.targetCount=${String(this.networkState.targets.length)}
|
||||
.targets=${this.networkState.targets.map((t) => ({
|
||||
type: t.type,
|
||||
name: t.name,
|
||||
domain: t.domain,
|
||||
target: `${t.targetHost}:${t.targetPort}`,
|
||||
status: t.status,
|
||||
}))}
|
||||
.logs=${[]}
|
||||
@refresh=${() => {
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkTargetsAction, null);
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||
}}
|
||||
></sz-network-proxy-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDnsView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>DNS Records</ob-sectionheading>
|
||||
<sz-network-dns-view
|
||||
.records=${this.networkState.dnsRecords}
|
||||
@sync=${() => {
|
||||
appstate.networkStatePart.dispatchAction(appstate.syncDnsAction, null);
|
||||
}}
|
||||
@delete=${(e: CustomEvent) => {
|
||||
console.log('Delete DNS record:', e.detail);
|
||||
}}
|
||||
></sz-network-dns-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDomainsView(): TemplateResult {
|
||||
const certs = this.networkState.certificates;
|
||||
return html`
|
||||
<ob-sectionheading>Domains</ob-sectionheading>
|
||||
<sz-network-domains-view
|
||||
.domains=${this.networkState.domains.map((d) => {
|
||||
const cert = certs.find((c) => c.certDomain === d.domain);
|
||||
let certStatus: 'valid' | 'expiring' | 'expired' | 'pending' = 'pending';
|
||||
if (cert) {
|
||||
if (!cert.isValid) certStatus = 'expired';
|
||||
else if (cert.expiresAt && cert.expiresAt - Date.now() < 30 * 24 * 60 * 60 * 1000)
|
||||
certStatus = 'expiring';
|
||||
else certStatus = 'valid';
|
||||
}
|
||||
return {
|
||||
domain: d.domain,
|
||||
provider: 'cloudflare',
|
||||
serviceCount: d.services?.length || 0,
|
||||
certificateStatus: certStatus,
|
||||
};
|
||||
})}
|
||||
@sync=${() => {
|
||||
appstate.networkStatePart.dispatchAction(appstate.fetchDomainsAction, null);
|
||||
}}
|
||||
@view=${(e: CustomEvent) => {
|
||||
this.selectedDomain = e.detail.domain || e.detail;
|
||||
this.currentTab = 'domain-detail';
|
||||
}}
|
||||
></sz-network-domains-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDomainDetailView(): TemplateResult {
|
||||
const domainDetail = this.networkState.domains.find(
|
||||
(d) => d.domain === this.selectedDomain,
|
||||
);
|
||||
const cert = this.networkState.certificates.find(
|
||||
(c) => c.certDomain === this.selectedDomain,
|
||||
);
|
||||
|
||||
return html`
|
||||
<ob-sectionheading>Domain Details</ob-sectionheading>
|
||||
<sz-domain-detail-view
|
||||
.domain=${domainDetail
|
||||
? {
|
||||
id: this.selectedDomain,
|
||||
name: this.selectedDomain,
|
||||
status: 'active',
|
||||
verified: true,
|
||||
createdAt: '',
|
||||
}
|
||||
: null}
|
||||
.certificate=${cert
|
||||
? {
|
||||
id: cert.domainId,
|
||||
domain: cert.certDomain,
|
||||
issuer: 'Let\'s Encrypt',
|
||||
validFrom: cert.issuedAt ? new Date(cert.issuedAt).toISOString() : '',
|
||||
validUntil: cert.expiresAt ? new Date(cert.expiresAt).toISOString() : '',
|
||||
daysRemaining: cert.expiresAt
|
||||
? Math.floor((cert.expiresAt - Date.now()) / (24 * 60 * 60 * 1000))
|
||||
: 0,
|
||||
status: cert.isValid ? 'valid' : 'expired',
|
||||
autoRenew: true,
|
||||
}
|
||||
: null}
|
||||
.dnsRecords=${this.networkState.dnsRecords
|
||||
.filter((r) => r.domain?.includes(this.selectedDomain))
|
||||
.map((r) => ({
|
||||
id: r.id || '',
|
||||
type: r.type,
|
||||
name: r.domain,
|
||||
value: r.value,
|
||||
ttl: 3600,
|
||||
}))}
|
||||
@renew-certificate=${() => {
|
||||
appstate.networkStatePart.dispatchAction(appstate.renewCertificateAction, {
|
||||
domain: this.selectedDomain,
|
||||
});
|
||||
}}
|
||||
></sz-domain-detail-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
84
ts_web/elements/ob-view-registries.ts
Normal file
84
ts_web/elements/ob-view-registries.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-registries')
|
||||
export class ObViewRegistries extends DeesElement {
|
||||
@state()
|
||||
accessor registriesState: appstate.IRegistriesState = {
|
||||
tokens: [],
|
||||
registryStatus: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor currentTab: 'onebox' | 'external' = 'onebox';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const registriesSub = appstate.registriesStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.registriesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(registriesSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await appstate.registriesStatePart.dispatchAction(
|
||||
appstate.fetchRegistryTokensAction,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
switch (this.currentTab) {
|
||||
case 'external':
|
||||
return this.renderExternalView();
|
||||
default:
|
||||
return this.renderOneboxView();
|
||||
}
|
||||
}
|
||||
|
||||
private renderOneboxView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Registries</ob-sectionheading>
|
||||
<sz-registry-advertisement
|
||||
.status=${'running'}
|
||||
.registryUrl=${'localhost:5000'}
|
||||
@manage-tokens=${() => {
|
||||
// tokens are managed via the tokens view
|
||||
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'tokens' });
|
||||
}}
|
||||
></sz-registry-advertisement>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderExternalView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>External Registries</ob-sectionheading>
|
||||
<sz-registry-external-view
|
||||
.registries=${[]}
|
||||
@add=${(e: CustomEvent) => {
|
||||
console.log('Add external registry:', e.detail);
|
||||
}}
|
||||
></sz-registry-external-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
219
ts_web/elements/ob-view-services.ts
Normal file
219
ts_web/elements/ob-view-services.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-services')
|
||||
export class ObViewServices extends DeesElement {
|
||||
@state()
|
||||
accessor servicesState: appstate.IServicesState = {
|
||||
services: [],
|
||||
currentService: null,
|
||||
currentServiceLogs: [],
|
||||
currentServiceStats: null,
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor backupsState: appstate.IBackupsState = {
|
||||
backups: [],
|
||||
schedules: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor currentView: 'list' | 'create' | 'detail' | 'backups' | 'platform-detail' = 'list';
|
||||
|
||||
@state()
|
||||
accessor selectedServiceName: string = '';
|
||||
|
||||
@state()
|
||||
accessor selectedPlatformType: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const servicesSub = appstate.servicesStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.servicesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(servicesSub);
|
||||
|
||||
const backupsSub = appstate.backupsStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.backupsState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(backupsSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await Promise.all([
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
|
||||
]);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
switch (this.currentView) {
|
||||
case 'create':
|
||||
return this.renderCreateView();
|
||||
case 'detail':
|
||||
return this.renderDetailView();
|
||||
case 'backups':
|
||||
return this.renderBackupsView();
|
||||
case 'platform-detail':
|
||||
return this.renderPlatformDetailView();
|
||||
default:
|
||||
return this.renderListView();
|
||||
}
|
||||
}
|
||||
|
||||
private renderListView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Services</ob-sectionheading>
|
||||
<sz-services-list-view
|
||||
.services=${this.servicesState.services}
|
||||
@service-click=${(e: CustomEvent) => {
|
||||
this.selectedServiceName = e.detail.name || e.detail.service?.name;
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceAction, {
|
||||
name: this.selectedServiceName,
|
||||
});
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceLogsAction, {
|
||||
name: this.selectedServiceName,
|
||||
});
|
||||
this.currentView = 'detail';
|
||||
}}
|
||||
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
|
||||
></sz-services-list-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCreateView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Create Service</ob-sectionheading>
|
||||
<sz-service-create-view
|
||||
.registries=${[]}
|
||||
@create-service=${async (e: CustomEvent) => {
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
|
||||
config: e.detail,
|
||||
});
|
||||
this.currentView = 'list';
|
||||
}}
|
||||
@cancel=${() => {
|
||||
this.currentView = 'list';
|
||||
}}
|
||||
></sz-service-create-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDetailView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Service Details</ob-sectionheading>
|
||||
<sz-service-detail-view
|
||||
.service=${this.servicesState.currentService}
|
||||
.logs=${this.servicesState.currentServiceLogs}
|
||||
.stats=${this.servicesState.currentServiceStats}
|
||||
@back=${() => {
|
||||
this.currentView = 'list';
|
||||
}}
|
||||
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
|
||||
></sz-service-detail-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBackupsView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Backups</ob-sectionheading>
|
||||
<sz-services-backups-view
|
||||
.schedules=${this.backupsState.schedules}
|
||||
.backups=${this.backupsState.backups}
|
||||
@create-schedule=${(e: CustomEvent) => {
|
||||
appstate.backupsStatePart.dispatchAction(appstate.createScheduleAction, {
|
||||
config: e.detail,
|
||||
});
|
||||
}}
|
||||
@run-now=${(e: CustomEvent) => {
|
||||
appstate.backupsStatePart.dispatchAction(appstate.triggerScheduleAction, {
|
||||
scheduleId: e.detail.scheduleId,
|
||||
});
|
||||
}}
|
||||
@delete-backup=${(e: CustomEvent) => {
|
||||
appstate.backupsStatePart.dispatchAction(appstate.deleteBackupAction, {
|
||||
backupId: e.detail.backupId,
|
||||
});
|
||||
}}
|
||||
></sz-services-backups-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPlatformDetailView(): TemplateResult {
|
||||
const platformService = this.servicesState.platformServices.find(
|
||||
(ps) => ps.type === this.selectedPlatformType,
|
||||
);
|
||||
return html`
|
||||
<ob-sectionheading>Platform Service</ob-sectionheading>
|
||||
<sz-platform-service-detail-view
|
||||
.service=${platformService
|
||||
? {
|
||||
id: platformService.type,
|
||||
name: platformService.displayName,
|
||||
type: platformService.type,
|
||||
status: platformService.status,
|
||||
version: '',
|
||||
host: 'localhost',
|
||||
port: 0,
|
||||
config: {},
|
||||
}
|
||||
: null}
|
||||
.logs=${[]}
|
||||
@start=${() => {
|
||||
appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
|
||||
serviceType: this.selectedPlatformType as any,
|
||||
});
|
||||
}}
|
||||
@stop=${() => {
|
||||
appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
|
||||
serviceType: this.selectedPlatformType as any,
|
||||
});
|
||||
}}
|
||||
></sz-platform-service-detail-view>
|
||||
`;
|
||||
}
|
||||
|
||||
private async handleServiceAction(e: CustomEvent) {
|
||||
const action = e.detail.action;
|
||||
const name = e.detail.service?.name || e.detail.name || this.selectedServiceName;
|
||||
switch (action) {
|
||||
case 'start':
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.startServiceAction, { name });
|
||||
break;
|
||||
case 'stop':
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.stopServiceAction, { name });
|
||||
break;
|
||||
case 'restart':
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.restartServiceAction, { name });
|
||||
break;
|
||||
case 'delete':
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.deleteServiceAction, { name });
|
||||
this.currentView = 'list';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
ts_web/elements/ob-view-settings.ts
Normal file
93
ts_web/elements/ob-view-settings.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-settings')
|
||||
export class ObViewSettings extends DeesElement {
|
||||
@state()
|
||||
accessor settingsState: appstate.ISettingsState = {
|
||||
settings: null,
|
||||
backupPasswordConfigured: false,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor loginState: appstate.ILoginState = {
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const settingsSub = appstate.settingsStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.settingsState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(settingsSub);
|
||||
|
||||
const loginSub = appstate.loginStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.loginState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(loginSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await appstate.settingsStatePart.dispatchAction(appstate.fetchSettingsAction, null);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Settings</ob-sectionheading>
|
||||
<sz-settings-view
|
||||
.settings=${this.settingsState.settings || {
|
||||
darkMode: true,
|
||||
cloudflareToken: '',
|
||||
cloudflareZoneId: '',
|
||||
autoRenewCerts: false,
|
||||
renewalThreshold: 30,
|
||||
acmeEmail: '',
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
forceHttps: false,
|
||||
}}
|
||||
.currentUser=${this.loginState.identity?.username || 'admin'}
|
||||
@setting-change=${(e: CustomEvent) => {
|
||||
const { key, value } = e.detail;
|
||||
appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, {
|
||||
settings: { [key]: value },
|
||||
});
|
||||
}}
|
||||
@save=${(e: CustomEvent) => {
|
||||
appstate.settingsStatePart.dispatchAction(appstate.updateSettingsAction, {
|
||||
settings: e.detail,
|
||||
});
|
||||
}}
|
||||
@change-password=${(e: CustomEvent) => {
|
||||
console.log('Change password requested:', e.detail);
|
||||
}}
|
||||
@reset=${() => {
|
||||
appstate.settingsStatePart.dispatchAction(appstate.fetchSettingsAction, null);
|
||||
}}
|
||||
></sz-settings-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
86
ts_web/elements/ob-view-tokens.ts
Normal file
86
ts_web/elements/ob-view-tokens.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-view-tokens')
|
||||
export class ObViewTokens extends DeesElement {
|
||||
@state()
|
||||
accessor registriesState: appstate.IRegistriesState = {
|
||||
tokens: [],
|
||||
registryStatus: null,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const registriesSub = appstate.registriesStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((newState) => {
|
||||
this.registriesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(registriesSub);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await appstate.registriesStatePart.dispatchAction(
|
||||
appstate.fetchRegistryTokensAction,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const globalTokens = this.registriesState.tokens.filter((t) => t.type === 'global');
|
||||
const ciTokens = this.registriesState.tokens.filter((t) => t.type === 'ci');
|
||||
|
||||
return html`
|
||||
<ob-sectionheading>Tokens</ob-sectionheading>
|
||||
<sz-tokens-view
|
||||
.globalTokens=${globalTokens.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
type: 'global' as const,
|
||||
createdAt: t.createdAt,
|
||||
lastUsed: t.lastUsed,
|
||||
}))}
|
||||
.ciTokens=${ciTokens.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
type: 'ci' as const,
|
||||
service: t.service,
|
||||
createdAt: t.createdAt,
|
||||
lastUsed: t.lastUsed,
|
||||
}))}
|
||||
@create=${(e: CustomEvent) => {
|
||||
appstate.registriesStatePart.dispatchAction(appstate.createRegistryTokenAction, {
|
||||
token: {
|
||||
name: `new-${e.detail.type}-token`,
|
||||
type: e.detail.type,
|
||||
permissions: ['pull'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
@delete=${(e: CustomEvent) => {
|
||||
appstate.registriesStatePart.dispatchAction(appstate.deleteRegistryTokenAction, {
|
||||
tokenId: e.detail.id || e.detail.tokenId,
|
||||
});
|
||||
}}
|
||||
></sz-tokens-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
10
ts_web/elements/shared/css.ts
Normal file
10
ts_web/elements/shared/css.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
|
||||
export const viewHostCss = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
padding: 16px 16px;
|
||||
}
|
||||
`;
|
||||
2
ts_web/elements/shared/index.ts
Normal file
2
ts_web/elements/shared/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './css.js';
|
||||
export * from './ob-sectionheading.js';
|
||||
37
ts_web/elements/shared/ob-sectionheading.ts
Normal file
37
ts_web/elements/shared/ob-sectionheading.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ob-sectionheading')
|
||||
export class ObSectionHeading extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.heading {
|
||||
font-family: 'Cal Sans', 'Inter', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<h1 class="heading">
|
||||
<slot></slot>
|
||||
</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user