feat(opsserver): introduce OpsServer (TypedRequest API) and new lightweight web UI; replace legacy Angular UI and add typed interfaces

This commit is contained in:
2026-02-24 18:15:44 +00:00
parent 84c47cd7f5
commit ba05cc84fe
143 changed files with 46631 additions and 20632 deletions

13
ts_web/elements/index.ts Normal file
View 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';

View 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);
}
}

View 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' });
}
}
}

View 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>
`;
}
}

View 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>
`;
}
}

View 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;
}
}
}

View 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>
`;
}
}

View 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>
`;
}
}

View 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;
}
`;

View File

@@ -0,0 +1,2 @@
export * from './css.js';
export * from './ob-sectionheading.js';

View 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>
`;
}
}