feat(opsserver): introduce OpsServer (TypedRequest API) and new lightweight web UI; replace legacy Angular UI and add typed interfaces
This commit is contained in:
8
ts_web/00_commitinfo_data.ts
Normal file
8
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/onebox',
|
||||
version: '1.10.0',
|
||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||
}
|
||||
919
ts_web/appstate.ts
Normal file
919
ts_web/appstate.ts
Normal file
@@ -0,0 +1,919 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Smartstate instance
|
||||
// ============================================================================
|
||||
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
||||
|
||||
// ============================================================================
|
||||
// State Part Interfaces
|
||||
// ============================================================================
|
||||
|
||||
export interface ILoginState {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export interface ISystemState {
|
||||
status: interfaces.data.ISystemStatus | null;
|
||||
}
|
||||
|
||||
export interface IServicesState {
|
||||
services: interfaces.data.IService[];
|
||||
currentService: interfaces.data.IService | null;
|
||||
currentServiceLogs: interfaces.data.ILogEntry[];
|
||||
currentServiceStats: interfaces.data.IContainerStats | null;
|
||||
platformServices: interfaces.data.IPlatformService[];
|
||||
currentPlatformService: interfaces.data.IPlatformService | null;
|
||||
}
|
||||
|
||||
export interface INetworkState {
|
||||
targets: interfaces.data.INetworkTarget[];
|
||||
stats: interfaces.data.INetworkStats | null;
|
||||
trafficStats: interfaces.data.ITrafficStats | null;
|
||||
dnsRecords: interfaces.data.IDnsRecord[];
|
||||
domains: interfaces.data.IDomainDetail[];
|
||||
certificates: interfaces.data.ICertificate[];
|
||||
}
|
||||
|
||||
export interface IRegistriesState {
|
||||
tokens: interfaces.data.IRegistryToken[];
|
||||
registryStatus: { running: boolean; port: number } | null;
|
||||
}
|
||||
|
||||
export interface IBackupsState {
|
||||
backups: interfaces.data.IBackup[];
|
||||
schedules: interfaces.data.IBackupSchedule[];
|
||||
}
|
||||
|
||||
export interface ISettingsState {
|
||||
settings: interfaces.data.ISettings | null;
|
||||
backupPasswordConfigured: boolean;
|
||||
}
|
||||
|
||||
export interface IUiState {
|
||||
activeView: string;
|
||||
autoRefresh: boolean;
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State Parts
|
||||
// ============================================================================
|
||||
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
{
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'persistent',
|
||||
);
|
||||
|
||||
export const systemStatePart = await appState.getStatePart<ISystemState>(
|
||||
'system',
|
||||
{
|
||||
status: null,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const servicesStatePart = await appState.getStatePart<IServicesState>(
|
||||
'services',
|
||||
{
|
||||
services: [],
|
||||
currentService: null,
|
||||
currentServiceLogs: [],
|
||||
currentServiceStats: null,
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
'network',
|
||||
{
|
||||
targets: [],
|
||||
stats: null,
|
||||
trafficStats: null,
|
||||
dnsRecords: [],
|
||||
domains: [],
|
||||
certificates: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const registriesStatePart = await appState.getStatePart<IRegistriesState>(
|
||||
'registries',
|
||||
{
|
||||
tokens: [],
|
||||
registryStatus: null,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const backupsStatePart = await appState.getStatePart<IBackupsState>(
|
||||
'backups',
|
||||
{
|
||||
backups: [],
|
||||
schedules: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const settingsStatePart = await appState.getStatePart<ISettingsState>(
|
||||
'settings',
|
||||
{
|
||||
settings: null,
|
||||
backupPasswordConfigured: false,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
'ui',
|
||||
{
|
||||
activeView: 'dashboard',
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
}
|
||||
|
||||
const getActionContext = (): IActionContext => {
|
||||
return { identity: loginStatePart.getState().identity };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Login Actions
|
||||
// ============================================================================
|
||||
|
||||
export const loginAction = loginStatePart.createAction<{
|
||||
username: string;
|
||||
password: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
||||
>('/typedrequest', 'adminLoginWithUsernameAndPassword');
|
||||
|
||||
const response = await typedRequest.fire({
|
||||
username: dataArg.username,
|
||||
password: dataArg.password,
|
||||
});
|
||||
|
||||
return {
|
||||
identity: response.identity,
|
||||
isLoggedIn: true,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Login failed:', err);
|
||||
return { identity: null, isLoggedIn: false };
|
||||
}
|
||||
});
|
||||
|
||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
if (context.identity) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLogout
|
||||
>('/typedrequest', 'adminLogout');
|
||||
await typedRequest.fire({ identity: context.identity });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
return { identity: null, isLoggedIn: false };
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// System Status Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchSystemStatusAction = systemStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSystemStatus
|
||||
>('/typedrequest', 'getSystemStatus');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { status: response.status };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch system status:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Services Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchServicesAction = servicesStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServices
|
||||
>('/typedrequest', 'getServices');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), services: response.services };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch services:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetService
|
||||
>('/typedrequest', 'getService');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
return { ...statePartArg.getState(), currentService: response.service };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const createServiceAction = servicesStatePart.createAction<{
|
||||
config: interfaces.data.IServiceCreate;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateService
|
||||
>('/typedrequest', 'createService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceConfig: dataArg.config,
|
||||
});
|
||||
// Re-fetch services list
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServices
|
||||
>('/typedrequest', 'getServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), services: listResp.services };
|
||||
} catch (err) {
|
||||
console.error('Failed to create service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteService
|
||||
>('/typedrequest', 'deleteService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
services: state.services.filter((s) => s.name !== dataArg.name),
|
||||
currentService: null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const startServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_StartService
|
||||
>('/typedrequest', 'startService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
// Re-fetch services
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServices
|
||||
>('/typedrequest', 'getServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), services: listResp.services };
|
||||
} catch (err) {
|
||||
console.error('Failed to start service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const stopServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_StopService
|
||||
>('/typedrequest', 'stopService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServices
|
||||
>('/typedrequest', 'getServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), services: listResp.services };
|
||||
} catch (err) {
|
||||
console.error('Failed to stop service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const restartServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RestartService
|
||||
>('/typedrequest', 'restartService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServices
|
||||
>('/typedrequest', 'getServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), services: listResp.services };
|
||||
} catch (err) {
|
||||
console.error('Failed to restart service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchServiceLogsAction = servicesStatePart.createAction<{
|
||||
name: string;
|
||||
lines?: number;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServiceLogs
|
||||
>('/typedrequest', 'getServiceLogs');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
lines: dataArg.lines || 200,
|
||||
});
|
||||
return { ...statePartArg.getState(), currentServiceLogs: response.logs };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch service logs:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchServiceStatsAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServiceStats
|
||||
>('/typedrequest', 'getServiceStats');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
return { ...statePartArg.getState(), currentServiceStats: response.stats };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch service stats:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Platform Services Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchPlatformServicesAction = servicesStatePart.createAction(
|
||||
async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetPlatformServices
|
||||
>('/typedrequest', 'getPlatformServices');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), platformServices: response.platformServices };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch platform services:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const startPlatformServiceAction = servicesStatePart.createAction<{
|
||||
serviceType: interfaces.data.TPlatformServiceType;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_StartPlatformService
|
||||
>('/typedrequest', 'startPlatformService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceType: dataArg.serviceType,
|
||||
});
|
||||
// Re-fetch platform services
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetPlatformServices
|
||||
>('/typedrequest', 'getPlatformServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), platformServices: listResp.platformServices };
|
||||
} catch (err) {
|
||||
console.error('Failed to start platform service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const stopPlatformServiceAction = servicesStatePart.createAction<{
|
||||
serviceType: interfaces.data.TPlatformServiceType;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_StopPlatformService
|
||||
>('/typedrequest', 'stopPlatformService');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceType: dataArg.serviceType,
|
||||
});
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetPlatformServices
|
||||
>('/typedrequest', 'getPlatformServices');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), platformServices: listResp.platformServices };
|
||||
} catch (err) {
|
||||
console.error('Failed to stop platform service:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Network Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchNetworkTargetsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetNetworkTargets
|
||||
>('/typedrequest', 'getNetworkTargets');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), targets: response.targets };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch network targets:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetNetworkStats
|
||||
>('/typedrequest', 'getNetworkStats');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), stats: response.stats };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch network stats:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchTrafficStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetTrafficStats
|
||||
>('/typedrequest', 'getTrafficStats');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), trafficStats: response.stats };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch traffic stats:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchDnsRecordsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetDnsRecords
|
||||
>('/typedrequest', 'getDnsRecords');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), dnsRecords: response.records };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch DNS records:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const syncDnsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_SyncDns
|
||||
>('/typedrequest', 'syncDns');
|
||||
await typedRequest.fire({ identity: context.identity! });
|
||||
// Re-fetch DNS records
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetDnsRecords
|
||||
>('/typedrequest', 'getDnsRecords');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), dnsRecords: listResp.records };
|
||||
} catch (err) {
|
||||
console.error('Failed to sync DNS:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchDomainsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetDomains
|
||||
>('/typedrequest', 'getDomains');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), domains: response.domains };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch domains:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchCertificatesAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListCertificates
|
||||
>('/typedrequest', 'listCertificates');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), certificates: response.certificates };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch certificates:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const renewCertificateAction = networkStatePart.createAction<{ domain: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RenewCertificate
|
||||
>('/typedrequest', 'renewCertificate');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
domain: dataArg.domain,
|
||||
});
|
||||
// Re-fetch certificates
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListCertificates
|
||||
>('/typedrequest', 'listCertificates');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), certificates: listResp.certificates };
|
||||
} catch (err) {
|
||||
console.error('Failed to renew certificate:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Registry Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchRegistryTokensAction = registriesStatePart.createAction(
|
||||
async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRegistryTokens
|
||||
>('/typedrequest', 'getRegistryTokens');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), tokens: response.tokens };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch registry tokens:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const createRegistryTokenAction = registriesStatePart.createAction<{
|
||||
token: interfaces.data.ICreateTokenRequest;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateRegistryToken
|
||||
>('/typedrequest', 'createRegistryToken');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
token: dataArg.token,
|
||||
});
|
||||
// Re-fetch tokens
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRegistryTokens
|
||||
>('/typedrequest', 'getRegistryTokens');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), tokens: listResp.tokens };
|
||||
} catch (err) {
|
||||
console.error('Failed to create registry token:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteRegistryTokenAction = registriesStatePart.createAction<{
|
||||
tokenId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteRegistryToken
|
||||
>('/typedrequest', 'deleteRegistryToken');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
tokenId: dataArg.tokenId,
|
||||
});
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
tokens: state.tokens.filter((t) => t.id !== dataArg.tokenId),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete registry token:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Backups Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchBackupsAction = backupsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBackups
|
||||
>('/typedrequest', 'getBackups');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), backups: response.backups };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch backups:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteBackupAction = backupsStatePart.createAction<{ backupId: number }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteBackup
|
||||
>('/typedrequest', 'deleteBackup');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
backupId: dataArg.backupId,
|
||||
});
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
backups: state.backups.filter((b) => b.id !== dataArg.backupId),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete backup:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchSchedulesAction = backupsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBackupSchedules
|
||||
>('/typedrequest', 'getBackupSchedules');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), schedules: response.schedules };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch schedules:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const createScheduleAction = backupsStatePart.createAction<{
|
||||
config: interfaces.data.IBackupScheduleCreate;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateBackupSchedule
|
||||
>('/typedrequest', 'createBackupSchedule');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
scheduleConfig: dataArg.config,
|
||||
});
|
||||
// Re-fetch schedules
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBackupSchedules
|
||||
>('/typedrequest', 'getBackupSchedules');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), schedules: listResp.schedules };
|
||||
} catch (err) {
|
||||
console.error('Failed to create schedule:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteScheduleAction = backupsStatePart.createAction<{ scheduleId: number }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteBackupSchedule
|
||||
>('/typedrequest', 'deleteBackupSchedule');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
scheduleId: dataArg.scheduleId,
|
||||
});
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
schedules: state.schedules.filter((s) => s.id !== dataArg.scheduleId),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete schedule:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const triggerScheduleAction = backupsStatePart.createAction<{ scheduleId: number }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_TriggerBackupSchedule
|
||||
>('/typedrequest', 'triggerBackupSchedule');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
scheduleId: dataArg.scheduleId,
|
||||
});
|
||||
// Re-fetch backups
|
||||
const backupsReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBackups
|
||||
>('/typedrequest', 'getBackups');
|
||||
const backupsResp = await backupsReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), backups: backupsResp.backups };
|
||||
} catch (err) {
|
||||
console.error('Failed to trigger schedule:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Settings Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchSettingsAction = settingsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const [settingsResp, passwordResp] = await Promise.all([
|
||||
new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSettings
|
||||
>('/typedrequest', 'getSettings').fire({ identity: context.identity! }),
|
||||
new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBackupPasswordStatus
|
||||
>('/typedrequest', 'getBackupPasswordStatus').fire({ identity: context.identity! }),
|
||||
]);
|
||||
return {
|
||||
settings: settingsResp.settings,
|
||||
backupPasswordConfigured: passwordResp.status.isConfigured,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch settings:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const updateSettingsAction = settingsStatePart.createAction<{
|
||||
settings: Partial<interfaces.data.ISettings>;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateSettings
|
||||
>('/typedrequest', 'updateSettings');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
settings: dataArg.settings,
|
||||
});
|
||||
return { ...statePartArg.getState(), settings: response.settings };
|
||||
} catch (err) {
|
||||
console.error('Failed to update settings:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const setBackupPasswordAction = settingsStatePart.createAction<{ password: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_SetBackupPassword
|
||||
>('/typedrequest', 'setBackupPassword');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
password: dataArg.password,
|
||||
});
|
||||
return { ...statePartArg.getState(), backupPasswordConfigured: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to set backup password:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// UI Actions
|
||||
// ============================================================================
|
||||
|
||||
export const setActiveViewAction = uiStatePart.createAction<{ view: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
return { ...statePartArg.getState(), activeView: dataArg.view };
|
||||
},
|
||||
);
|
||||
|
||||
export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg) => {
|
||||
const state = statePartArg.getState();
|
||||
return { ...state, autoRefresh: !state.autoRefresh };
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Auto-refresh system
|
||||
// ============================================================================
|
||||
|
||||
let refreshIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const dispatchCombinedRefreshAction = async () => {
|
||||
const loginState = loginStatePart.getState();
|
||||
if (!loginState.isLoggedIn) return;
|
||||
|
||||
try {
|
||||
await systemStatePart.dispatchAction(fetchSystemStatusAction, null);
|
||||
} catch (err) {
|
||||
// Silently fail on auto-refresh
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
const uiState = uiStatePart.getState();
|
||||
const loginState = loginStatePart.getState();
|
||||
|
||||
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||
if (refreshIntervalHandle) {
|
||||
clearInterval(refreshIntervalHandle);
|
||||
}
|
||||
refreshIntervalHandle = setInterval(() => {
|
||||
dispatchCombinedRefreshAction();
|
||||
}, uiState.refreshInterval);
|
||||
} else {
|
||||
if (refreshIntervalHandle) {
|
||||
clearInterval(refreshIntervalHandle);
|
||||
refreshIntervalHandle = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uiStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
||||
loginStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
||||
startAutoRefresh();
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
7
ts_web/index.ts
Normal file
7
ts_web/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import './elements/index.js';
|
||||
|
||||
plugins.deesElement.render(html`
|
||||
<ob-app-shell></ob-app-shell>
|
||||
`, document.body);
|
||||
14
ts_web/plugins.ts
Normal file
14
ts_web/plugins.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// @design.estate scope
|
||||
import * as deesElement from '@design.estate/dees-element';
|
||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
||||
|
||||
// @serve.zone scope — side-effect import registers all sz-* custom elements
|
||||
import '@serve.zone/catalog';
|
||||
|
||||
export {
|
||||
deesElement,
|
||||
deesCatalog,
|
||||
};
|
||||
|
||||
// domtools gives us TypedRequest, smartstate, smartrouter, and other utilities
|
||||
export const domtools = deesElement.domtools;
|
||||
Reference in New Issue
Block a user