Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ebac76b4 | |||
| 27888a9fd1 | |||
| 3f6b058ce5 | |||
| ba370cbce8 | |||
| 43c8f261cc | |||
| 2984c41081 | |||
| d143d73ea9 | |||
| 9f8a6eaa76 | |||
| 0af8da2c9d | |||
| fa96d371d6 | |||
| 9e4dcc18a2 | |||
| 15574b8629 |
@@ -1,5 +1,40 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-21 - 1.27.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- group Onebox sidebar navigation into Apps, Network, and Registry sections (web)
|
||||||
|
- add parent/subview routes for grouped app, network, and registry pages
|
||||||
|
|
||||||
|
## 2026-05-21 - 1.26.3
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- use `dees-table` for gateway domains and DNS records views (web)
|
||||||
|
- replace custom row grids with catalog tables, filtering, refresh, and row actions
|
||||||
|
- use dees-table for gateway domains and DNS records views (web)
|
||||||
|
- replace custom row layouts with dees-table in gateway domains and DNS records views
|
||||||
|
- add table filtering, refresh actions, and row/context actions for dcrouter management
|
||||||
|
|
||||||
|
## 2026-05-20 - 1.26.2
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- reload SmartProxy routes after managed startup (proxy)
|
||||||
|
- reloads SmartProxy routes immediately after the admin API is ready during startup, avoiding an empty route table when Docker task state lags behind service readiness
|
||||||
|
|
||||||
|
## 2026-05-09 - 1.26.1 - fix(external-gateway)
|
||||||
|
derive gateway client identity from the dcrouter token and make the settings UI read-only
|
||||||
|
|
||||||
|
- Resolves external gateway ownership and domain sync to use the gateway client context returned by dcrouter instead of a locally entered client ID.
|
||||||
|
- Falls back to stored gateway client settings only when token context is unavailable.
|
||||||
|
- Removes editable Gateway Client ID fields from settings and shows them as diagnostic read-only values for managed and external modes.
|
||||||
|
- Updates external gateway tests to validate token-derived gateway client IDs and admin-token behavior.
|
||||||
|
|
||||||
## 2026-05-09 - 1.26.0 - feat(dcrouter)
|
## 2026-05-09 - 1.26.0 - feat(dcrouter)
|
||||||
add managed local dcrouter mode with status controls and gateway integration
|
add managed local dcrouter mode with status controls and gateway integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/onebox",
|
"name": "@serve.zone/onebox",
|
||||||
"version": "1.26.0",
|
"version": "1.27.0",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"test": "deno test --allow-all test/",
|
"test": "deno test --allow-all test/",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/onebox",
|
"name": "@serve.zone/onebox",
|
||||||
"version": "1.26.0",
|
"version": "1.27.0",
|
||||||
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
|
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ class FakeDatabase {
|
|||||||
const makeOneboxRef = () => {
|
const makeOneboxRef = () => {
|
||||||
const database = new FakeDatabase();
|
const database = new FakeDatabase();
|
||||||
database.settings.set('dcrouterGatewayUrl', 'https://edge.example.com');
|
database.settings.set('dcrouterGatewayUrl', 'https://edge.example.com');
|
||||||
database.settings.set('dcrouterGatewayClientId', 'onebox-1');
|
|
||||||
database.settings.set('dcrouterWorkHosterId', 'onebox-1');
|
|
||||||
database.secretSettings.set('dcrouterGatewayApiToken', 'dcr-token');
|
database.secretSettings.set('dcrouterGatewayApiToken', 'dcr-token');
|
||||||
|
|
||||||
let reloadCount = 0;
|
let reloadCount = 0;
|
||||||
@@ -94,8 +92,11 @@ Deno.test('ExternalGatewayManager syncs dcrouter domains into Onebox domains', a
|
|||||||
|
|
||||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
||||||
|
if (method === 'getGatewayClientContext') {
|
||||||
|
return { context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } } };
|
||||||
|
}
|
||||||
assertEquals(method, 'getGatewayClientDomains');
|
assertEquals(method, 'getGatewayClientDomains');
|
||||||
assertEquals(requestData.gatewayClientId, 'onebox-1');
|
assertEquals(requestData.gatewayClientId, 'onebox-token');
|
||||||
return {
|
return {
|
||||||
domains: [
|
domains: [
|
||||||
{
|
{
|
||||||
@@ -139,6 +140,9 @@ Deno.test('ExternalGatewayManager syncs service routes to dcrouter gatewayClient
|
|||||||
const requests: Array<{ method: string; requestData: Record<string, unknown> }> = [];
|
const requests: Array<{ method: string; requestData: Record<string, unknown> }> = [];
|
||||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
||||||
|
if (method === 'getGatewayClientContext') {
|
||||||
|
return { context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } } };
|
||||||
|
}
|
||||||
requests.push({ method, requestData });
|
requests.push({ method, requestData });
|
||||||
if (method === 'exportCertificate') {
|
if (method === 'exportCertificate') {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -154,7 +158,7 @@ Deno.test('ExternalGatewayManager syncs service routes to dcrouter gatewayClient
|
|||||||
|
|
||||||
assertEquals(ownership, {
|
assertEquals(ownership, {
|
||||||
gatewayClientType: 'onebox',
|
gatewayClientType: 'onebox',
|
||||||
gatewayClientId: 'onebox-1',
|
gatewayClientId: 'onebox-token',
|
||||||
appId: 'hello',
|
appId: 'hello',
|
||||||
hostname: 'hello.example.com',
|
hostname: 'hello.example.com',
|
||||||
});
|
});
|
||||||
@@ -189,6 +193,9 @@ Deno.test('ExternalGatewayManager uses managed dcrouter local target in managed
|
|||||||
let syncRequest: Record<string, unknown> | null = null;
|
let syncRequest: Record<string, unknown> | null = null;
|
||||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>, config: any) => {
|
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>, config: any) => {
|
||||||
|
if (method === 'getGatewayClientContext') {
|
||||||
|
return { context: { role: 'admin' } };
|
||||||
|
}
|
||||||
if (method === 'exportCertificate') {
|
if (method === 'exportCertificate') {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
@@ -213,6 +220,9 @@ Deno.test('ExternalGatewayManager deletes service routes through dcrouter gatewa
|
|||||||
let deleteRequest: Record<string, unknown> | null = null;
|
let deleteRequest: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
||||||
|
if (method === 'getGatewayClientContext') {
|
||||||
|
return { context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } } };
|
||||||
|
}
|
||||||
assertEquals(method, 'syncGatewayClientRoute');
|
assertEquals(method, 'syncGatewayClientRoute');
|
||||||
deleteRequest = requestData;
|
deleteRequest = requestData;
|
||||||
return { success: true, action: 'deleted', routeId: 'route-1' };
|
return { success: true, action: 'deleted', routeId: 'route-1' };
|
||||||
@@ -227,7 +237,7 @@ Deno.test('ExternalGatewayManager deletes service routes through dcrouter gatewa
|
|||||||
assert(deleteRequest);
|
assert(deleteRequest);
|
||||||
const capturedDeleteRequest = deleteRequest as Record<string, unknown>;
|
const capturedDeleteRequest = deleteRequest as Record<string, unknown>;
|
||||||
assertEquals(capturedDeleteRequest.delete, true);
|
assertEquals(capturedDeleteRequest.delete, true);
|
||||||
assertEquals((capturedDeleteRequest.ownership as any).gatewayClientId, 'onebox-1');
|
assertEquals((capturedDeleteRequest.ownership as any).gatewayClientId, 'onebox-token');
|
||||||
assertEquals((capturedDeleteRequest.ownership as any).hostname, 'hello.example.com');
|
assertEquals((capturedDeleteRequest.ownership as any).hostname, 'hello.example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,6 +245,9 @@ Deno.test('ExternalGatewayManager imports exported dcrouter certificates into On
|
|||||||
const oneboxRef = makeOneboxRef();
|
const oneboxRef = makeOneboxRef();
|
||||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
||||||
|
if (method === 'getGatewayClientContext') {
|
||||||
|
return { context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } } };
|
||||||
|
}
|
||||||
assertEquals(method, 'exportCertificate');
|
assertEquals(method, 'exportCertificate');
|
||||||
assertEquals(requestData.domain, 'hello.example.com');
|
assertEquals(requestData.domain, 'hello.example.com');
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/onebox',
|
name: '@serve.zone/onebox',
|
||||||
version: '1.26.0',
|
version: '1.27.0',
|
||||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,24 @@ type TWorkHosterType = 'onebox';
|
|||||||
interface IExternalGatewayConfig {
|
interface IExternalGatewayConfig {
|
||||||
url: string;
|
url: string;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
gatewayClientId: string;
|
gatewayClientType?: TWorkHosterType;
|
||||||
|
gatewayClientId?: string;
|
||||||
/** @deprecated Use gatewayClientId. */
|
/** @deprecated Use gatewayClientId. */
|
||||||
workHosterId: string;
|
workHosterId?: string;
|
||||||
targetHost?: string;
|
targetHost?: string;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IGatewayClientContextResponse {
|
||||||
|
context: {
|
||||||
|
role: 'admin' | 'gatewayClient' | 'operator';
|
||||||
|
gatewayClient?: {
|
||||||
|
type: 'onebox' | 'cloudly' | 'custom';
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface IWorkHosterDomain {
|
interface IWorkHosterDomain {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -62,8 +73,8 @@ interface IWorkAppRouteOwnership {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IGatewayClientOwnership {
|
interface IGatewayClientOwnership {
|
||||||
gatewayClientType: TWorkHosterType;
|
gatewayClientType?: TWorkHosterType;
|
||||||
gatewayClientId: string;
|
gatewayClientId?: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
}
|
}
|
||||||
@@ -128,8 +139,14 @@ export class ExternalGatewayManager {
|
|||||||
if (this.getMode() === 'disabled') {
|
if (this.getMode() === 'disabled') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const config = await this.getConfig({ requireTarget: false });
|
const mode = this.getMode();
|
||||||
return Boolean(config);
|
const url = mode === 'managed'
|
||||||
|
? this.oneboxRef.managedDcRouter.getGatewayUrl()
|
||||||
|
: this.normalizeUrl(this.database.getSetting('dcrouterGatewayUrl') || '');
|
||||||
|
const apiToken = mode === 'managed'
|
||||||
|
? await this.oneboxRef.managedDcRouter.getAdminToken()
|
||||||
|
: await this.database.getSecretSetting('dcrouterGatewayApiToken');
|
||||||
|
return Boolean(url && apiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async syncDomains(): Promise<IDomain[]> {
|
public async syncDomains(): Promise<IDomain[]> {
|
||||||
@@ -188,7 +205,7 @@ export class ExternalGatewayManager {
|
|||||||
try {
|
try {
|
||||||
const response = await this.fireDcRouterRequest<{ domains: IWorkHosterDomain[] }>(
|
const response = await this.fireDcRouterRequest<{ domains: IWorkHosterDomain[] }>(
|
||||||
'getGatewayClientDomains',
|
'getGatewayClientDomains',
|
||||||
{ gatewayClientId: config.gatewayClientId },
|
config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
return response.domains.map((domain) => ({
|
return response.domains.map((domain) => ({
|
||||||
@@ -216,7 +233,7 @@ export class ExternalGatewayManager {
|
|||||||
try {
|
try {
|
||||||
const response = await this.fireDcRouterRequest<{ records: IGatewayDnsRecord[] }>(
|
const response = await this.fireDcRouterRequest<{ records: IGatewayDnsRecord[] }>(
|
||||||
'getGatewayClientDnsRecords',
|
'getGatewayClientDnsRecords',
|
||||||
{ gatewayClientId: config.gatewayClientId },
|
config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
return response.records.map((record) => ({
|
return response.records.map((record) => ({
|
||||||
@@ -355,16 +372,27 @@ export class ExternalGatewayManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gatewayClientId = mode === 'managed'
|
|
||||||
? this.oneboxRef.managedDcRouter.ensureGatewayClientId()
|
|
||||||
: this.ensureGatewayClientId();
|
|
||||||
const config: IExternalGatewayConfig = {
|
const config: IExternalGatewayConfig = {
|
||||||
url,
|
url,
|
||||||
apiToken,
|
apiToken,
|
||||||
gatewayClientId,
|
|
||||||
workHosterId: gatewayClientId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const contextClient = await this.getGatewayClientFromToken(config);
|
||||||
|
if (contextClient) {
|
||||||
|
config.gatewayClientType = contextClient.type;
|
||||||
|
config.gatewayClientId = contextClient.id;
|
||||||
|
config.workHosterId = contextClient.id;
|
||||||
|
} else {
|
||||||
|
const fallbackGatewayClientId = mode === 'managed'
|
||||||
|
? this.oneboxRef.managedDcRouter.ensureGatewayClientId()
|
||||||
|
: this.getStoredGatewayClientId();
|
||||||
|
if (fallbackGatewayClientId) {
|
||||||
|
config.gatewayClientType = 'onebox';
|
||||||
|
config.gatewayClientId = fallbackGatewayClientId;
|
||||||
|
config.workHosterId = fallbackGatewayClientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.requireTarget !== false) {
|
if (options.requireTarget !== false) {
|
||||||
if (mode === 'managed') {
|
if (mode === 'managed') {
|
||||||
const target = this.oneboxRef.managedDcRouter.getRouteTarget();
|
const target = this.oneboxRef.managedDcRouter.getRouteTarget();
|
||||||
@@ -417,13 +445,27 @@ export class ExternalGatewayManager {
|
|||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureGatewayClientId(): string {
|
private getStoredGatewayClientId(): string {
|
||||||
let gatewayClientId = this.database.getSetting('dcrouterGatewayClientId') || this.database.getSetting('dcrouterWorkHosterId');
|
return this.database.getSetting('dcrouterGatewayClientId') || this.database.getSetting('dcrouterWorkHosterId') || '';
|
||||||
if (!gatewayClientId) {
|
}
|
||||||
gatewayClientId = crypto.randomUUID();
|
|
||||||
this.database.setSetting('dcrouterGatewayClientId', gatewayClientId);
|
private async getGatewayClientFromToken(config: IExternalGatewayConfig): Promise<{ type: TWorkHosterType; id: string } | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.fireDcRouterRequest<IGatewayClientContextResponse>(
|
||||||
|
'getGatewayClientContext',
|
||||||
|
{},
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
const gatewayClient = response.context.gatewayClient;
|
||||||
|
if (!gatewayClient) return null;
|
||||||
|
if (gatewayClient.type !== 'onebox') {
|
||||||
|
throw new Error(`dcrouter token is bound to unsupported gateway client type: ${gatewayClient.type}`);
|
||||||
|
}
|
||||||
|
return { type: gatewayClient.type, id: gatewayClient.id };
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`dcrouter gateway client context unavailable: ${getErrorMessage(error)}`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return gatewayClientId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildOwnership(
|
private buildOwnership(
|
||||||
@@ -433,7 +475,7 @@ export class ExternalGatewayManager {
|
|||||||
): IWorkAppRouteOwnership {
|
): IWorkAppRouteOwnership {
|
||||||
return {
|
return {
|
||||||
workHosterType: 'onebox',
|
workHosterType: 'onebox',
|
||||||
workHosterId: config.gatewayClientId,
|
workHosterId: config.gatewayClientId || '',
|
||||||
workAppId: service.name || `service-${service.id}`,
|
workAppId: service.name || `service-${service.id}`,
|
||||||
hostname,
|
hostname,
|
||||||
};
|
};
|
||||||
@@ -444,12 +486,15 @@ export class ExternalGatewayManager {
|
|||||||
hostname: string,
|
hostname: string,
|
||||||
config: IExternalGatewayConfig,
|
config: IExternalGatewayConfig,
|
||||||
): IGatewayClientOwnership {
|
): IGatewayClientOwnership {
|
||||||
return {
|
const ownership: IGatewayClientOwnership = {
|
||||||
gatewayClientType: 'onebox',
|
gatewayClientType: config.gatewayClientType || 'onebox',
|
||||||
gatewayClientId: config.gatewayClientId,
|
|
||||||
appId: service.name || `service-${service.id}`,
|
appId: service.name || `service-${service.id}`,
|
||||||
hostname,
|
hostname,
|
||||||
};
|
};
|
||||||
|
if (config.gatewayClientId) {
|
||||||
|
ownership.gatewayClientId = config.gatewayClientId;
|
||||||
|
}
|
||||||
|
return ownership;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRoute(service: IService, config: IExternalGatewayConfig): IDcRouterRouteConfig {
|
private buildRoute(service: IService, config: IExternalGatewayConfig): IDcRouterRouteConfig {
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export class SmartProxyManager {
|
|||||||
|
|
||||||
await this.waitForReady();
|
await this.waitForReady();
|
||||||
this.serviceRunning = true;
|
this.serviceRunning = true;
|
||||||
await this.reloadConfig();
|
await this.reloadConfig({ skipRunningCheck: true });
|
||||||
|
|
||||||
logger.success(`SmartProxy started (HTTP: ${this.httpPort}, HTTPS: ${this.httpsPort}, Admin: ${this.adminUrl})`);
|
logger.success(`SmartProxy started (HTTP: ${this.httpPort}, HTTPS: ${this.httpsPort}, Admin: ${this.adminUrl})`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -360,11 +360,13 @@ export class SmartProxyManager {
|
|||||||
return routeConfigs;
|
return routeConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadConfig(): Promise<void> {
|
async reloadConfig(options: { skipRunningCheck?: boolean } = {}): Promise<void> {
|
||||||
const isRunning = await this.isRunning();
|
if (!options.skipRunningCheck) {
|
||||||
if (!isRunning) {
|
const isRunning = await this.isRunning();
|
||||||
logger.warn('SmartProxy not running, cannot reload config');
|
if (!isRunning) {
|
||||||
return;
|
logger.warn('SmartProxy not running, cannot reload config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = this.buildRoutes();
|
const routes = this.buildRoutes();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/onebox',
|
name: '@serve.zone/onebox',
|
||||||
version: '1.26.0',
|
version: '1.27.0',
|
||||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -64,6 +64,7 @@ export interface IAppStoreState {
|
|||||||
|
|
||||||
export interface IUiState {
|
export interface IUiState {
|
||||||
activeView: string;
|
activeView: string;
|
||||||
|
activeSubview: string | null;
|
||||||
autoRefresh: boolean;
|
autoRefresh: boolean;
|
||||||
refreshInterval: number;
|
refreshInterval: number;
|
||||||
pendingAppTemplate?: any;
|
pendingAppTemplate?: any;
|
||||||
@@ -161,6 +162,7 @@ export const uiStatePart = await appState.getStatePart<IUiState>(
|
|||||||
'ui',
|
'ui',
|
||||||
{
|
{
|
||||||
activeView: 'dashboard',
|
activeView: 'dashboard',
|
||||||
|
activeSubview: null,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 30000,
|
refreshInterval: 30000,
|
||||||
},
|
},
|
||||||
@@ -1016,10 +1018,17 @@ export const setBackupPasswordAction = settingsStatePart.createAction<{ password
|
|||||||
// UI Actions
|
// UI Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const setActiveViewAction = uiStatePart.createAction<{ view: string }>(
|
export const setActiveViewAction = uiStatePart.createAction<{ view: string; subview?: string | null }>(
|
||||||
async (statePartArg, dataArg) => {
|
async (statePartArg, dataArg) => {
|
||||||
const normalizedView = dataArg.view.toLowerCase().replace(/\s+/g, '-');
|
const normalizedView = dataArg.view.toLowerCase().replace(/\s+/g, '-');
|
||||||
return { ...statePartArg.getState(), activeView: normalizedView };
|
const normalizedSubview = dataArg.subview
|
||||||
|
? dataArg.subview.toLowerCase().replace(/\s+/g, '-')
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
...statePartArg.getState(),
|
||||||
|
activeView: normalizedView,
|
||||||
|
activeSubview: normalizedSubview,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+170
-57
@@ -12,14 +12,21 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import type { ObViewDashboard } from './ob-view-dashboard.js';
|
interface IUnresolvedView {
|
||||||
import type { ObViewServices } from './ob-view-services.js';
|
slug?: string;
|
||||||
import type { ObViewDomains } from './ob-view-domains.js';
|
name: string;
|
||||||
import type { ObViewDnsRecords } from './ob-view-dns-records.js';
|
iconName?: string;
|
||||||
import type { ObViewNetwork } from './ob-view-network.js';
|
element?: Promise<any>;
|
||||||
import type { ObViewRegistries } from './ob-view-registries.js';
|
subViews?: IUnresolvedView[];
|
||||||
import type { ObViewTokens } from './ob-view-tokens.js';
|
}
|
||||||
import type { ObViewSettings } from './ob-view-settings.js';
|
|
||||||
|
interface IResolvedView {
|
||||||
|
slug?: string;
|
||||||
|
name: string;
|
||||||
|
iconName?: string;
|
||||||
|
element?: any;
|
||||||
|
subViews?: IResolvedView[];
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('ob-app-shell')
|
@customElement('ob-app-shell')
|
||||||
export class ObAppShell extends DeesElement {
|
export class ObAppShell extends DeesElement {
|
||||||
@@ -29,6 +36,7 @@ export class ObAppShell extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor uiState: appstate.IUiState = {
|
accessor uiState: appstate.IUiState = {
|
||||||
activeView: 'dashboard',
|
activeView: 'dashboard',
|
||||||
|
activeSubview: null,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 30000,
|
refreshInterval: 30000,
|
||||||
};
|
};
|
||||||
@@ -39,27 +47,93 @@ export class ObAppShell extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor loginError: string = '';
|
accessor loginError: string = '';
|
||||||
|
|
||||||
private viewTabs = [
|
private viewTabs: IUnresolvedView[] = [
|
||||||
{ name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
|
{
|
||||||
{ name: 'App Store', iconName: 'lucide:store', element: (async () => (await import('./ob-view-appstore.js')).ObViewAppStore)() },
|
slug: 'dashboard',
|
||||||
{ name: 'Services', iconName: 'lucide:boxes', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
|
name: 'Dashboard',
|
||||||
{ name: 'Domains', iconName: 'lucide:globe', element: (async () => (await import('./ob-view-domains.js')).ObViewDomains)() },
|
iconName: 'lucide:layoutDashboard',
|
||||||
{ name: 'DNS Records', iconName: 'lucide:listTree', element: (async () => (await import('./ob-view-dns-records.js')).ObViewDnsRecords)() },
|
element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)(),
|
||||||
{ name: 'Network', iconName: 'lucide:network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
|
},
|
||||||
{ name: 'Registries', iconName: 'lucide:package', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
|
{
|
||||||
{ name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
|
slug: 'apps',
|
||||||
{ name: 'Settings', iconName: 'lucide:settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
|
name: 'Apps',
|
||||||
|
iconName: 'lucide:store',
|
||||||
|
subViews: [
|
||||||
|
{
|
||||||
|
slug: 'app-store',
|
||||||
|
name: 'App Store',
|
||||||
|
iconName: 'lucide:store',
|
||||||
|
element: (async () => (await import('./ob-view-appstore.js')).ObViewAppStore)(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'services',
|
||||||
|
name: 'Services',
|
||||||
|
iconName: 'lucide:boxes',
|
||||||
|
element: (async () => (await import('./ob-view-services.js')).ObViewServices)(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'network',
|
||||||
|
name: 'Network',
|
||||||
|
iconName: 'lucide:network',
|
||||||
|
subViews: [
|
||||||
|
{
|
||||||
|
slug: 'proxy',
|
||||||
|
name: 'Proxy',
|
||||||
|
iconName: 'lucide:route',
|
||||||
|
element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'domains',
|
||||||
|
name: 'Domains',
|
||||||
|
iconName: 'lucide:globe',
|
||||||
|
element: (async () => (await import('./ob-view-domains.js')).ObViewDomains)(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'dns-records',
|
||||||
|
name: 'DNS Records',
|
||||||
|
iconName: 'lucide:listTree',
|
||||||
|
element: (async () => (await import('./ob-view-dns-records.js')).ObViewDnsRecords)(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'registry',
|
||||||
|
name: 'Registry',
|
||||||
|
iconName: 'lucide:package',
|
||||||
|
subViews: [
|
||||||
|
{
|
||||||
|
slug: 'registries',
|
||||||
|
name: 'Registries',
|
||||||
|
iconName: 'lucide:package',
|
||||||
|
element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'tokens',
|
||||||
|
name: 'Tokens',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'settings',
|
||||||
|
name: 'Settings',
|
||||||
|
iconName: 'lucide:settings',
|
||||||
|
element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
|
private resolvedViewTabs: IResolvedView[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
document.title = 'Onebox';
|
document.title = 'Onebox';
|
||||||
|
|
||||||
const loginSubscription = appstate.loginStatePart
|
const loginSubscription = appstate.loginStatePart
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg: appstate.ILoginState) => stateArg)
|
||||||
.subscribe((loginState) => {
|
.subscribe((loginState: appstate.ILoginState) => {
|
||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
if (loginState.isLoggedIn) {
|
if (loginState.isLoggedIn) {
|
||||||
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
|
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null);
|
||||||
@@ -68,15 +142,56 @@ export class ObAppShell extends DeesElement {
|
|||||||
this.rxSubscriptions.push(loginSubscription);
|
this.rxSubscriptions.push(loginSubscription);
|
||||||
|
|
||||||
const uiSubscription = appstate.uiStatePart
|
const uiSubscription = appstate.uiStatePart
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg: appstate.IUiState) => stateArg)
|
||||||
.subscribe((uiState) => {
|
.subscribe((uiState: appstate.IUiState) => {
|
||||||
this.uiState = uiState;
|
this.uiState = uiState;
|
||||||
this.syncAppdashView(uiState.activeView);
|
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(uiSubscription);
|
this.rxSubscriptions.push(uiSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
private async resolveViewTabs(tabs: IUnresolvedView[]): Promise<IResolvedView[]> {
|
||||||
|
return Promise.all(
|
||||||
|
tabs.map(async (tab) => {
|
||||||
|
const resolvedTab: IResolvedView = {
|
||||||
|
slug: tab.slug,
|
||||||
|
name: tab.name,
|
||||||
|
iconName: tab.iconName,
|
||||||
|
};
|
||||||
|
if (tab.element) {
|
||||||
|
resolvedTab.element = await tab.element;
|
||||||
|
}
|
||||||
|
if (tab.subViews) {
|
||||||
|
resolvedTab.subViews = await this.resolveViewTabs(tab.subViews);
|
||||||
|
}
|
||||||
|
return resolvedTab;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private slugFor(view: IResolvedView): string {
|
||||||
|
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
private findParent(view: IResolvedView): IResolvedView | undefined {
|
||||||
|
return this.resolvedViewTabs.find((viewTab) => viewTab.subViews?.includes(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
private findViewBySlug(viewSlug: string, subviewSlug: string | null): IResolvedView | undefined {
|
||||||
|
const topLevelView = this.resolvedViewTabs.find((view) => this.slugFor(view) === viewSlug);
|
||||||
|
if (!topLevelView) return undefined;
|
||||||
|
if (subviewSlug && topLevelView.subViews) {
|
||||||
|
return topLevelView.subViews.find((subview) => this.slugFor(subview) === subviewSlug) ?? topLevelView;
|
||||||
|
}
|
||||||
|
return topLevelView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get currentViewTab(): IResolvedView | undefined {
|
||||||
|
if (this.resolvedViewTabs.length === 0) return undefined;
|
||||||
|
return this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.resolvedViewTabs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static override styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
@@ -91,16 +206,14 @@ export class ObAppShell extends DeesElement {
|
|||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public override render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="maincontainer">
|
<div class="maincontainer">
|
||||||
<dees-simple-login name="Onebox">
|
<dees-simple-login name="Onebox">
|
||||||
<dees-simple-appdash
|
<dees-simple-appdash
|
||||||
name="Onebox"
|
name="Onebox"
|
||||||
.viewTabs=${this.resolvedViewTabs}
|
.viewTabs=${this.resolvedViewTabs}
|
||||||
.selectedView=${this.resolvedViewTabs.find(
|
.selectedView=${this.currentViewTab}
|
||||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView
|
|
||||||
) || this.resolvedViewTabs[0]}
|
|
||||||
>
|
>
|
||||||
</dees-simple-appdash>
|
</dees-simple-appdash>
|
||||||
</dees-simple-login>
|
</dees-simple-login>
|
||||||
@@ -108,15 +221,8 @@ export class ObAppShell extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public override async firstUpdated() {
|
||||||
// Resolve async view tab imports
|
this.resolvedViewTabs = await this.resolveViewTabs(this.viewTabs);
|
||||||
this.resolvedViewTabs = await Promise.all(
|
|
||||||
this.viewTabs.map(async (tab) => ({
|
|
||||||
name: tab.name,
|
|
||||||
iconName: tab.iconName,
|
|
||||||
element: await tab.element,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
|
||||||
@@ -130,34 +236,44 @@ export class ObAppShell extends DeesElement {
|
|||||||
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
|
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
|
||||||
if (appDash) {
|
if (appDash) {
|
||||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||||
const viewName = e.detail.view.name.toLowerCase().replace(/\s+/g, '-');
|
const view = e.detail.view as IResolvedView;
|
||||||
appRouter.navigateToView(viewName);
|
const parent = this.findParent(view);
|
||||||
|
const currentState = appstate.uiStatePart.getState();
|
||||||
|
if (parent) {
|
||||||
|
const parentSlug = this.slugFor(parent);
|
||||||
|
const subviewSlug = this.slugFor(view);
|
||||||
|
if (currentState.activeView === parentSlug && currentState.activeSubview === subviewSlug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(parentSlug, subviewSlug);
|
||||||
|
} else {
|
||||||
|
const slug = this.slugFor(view);
|
||||||
|
if (currentState.activeView === slug && !currentState.activeSubview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(slug);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
appDash.addEventListener('logout', async () => {
|
appDash.addEventListener('logout', async () => {
|
||||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the initial view on the appdash now that tabs are resolved
|
|
||||||
// Read activeView directly from state (not this.uiState which may be stale)
|
|
||||||
if (appDash && this.resolvedViewTabs.length > 0) {
|
if (appDash && this.resolvedViewTabs.length > 0) {
|
||||||
const currentActiveView = appstate.uiStatePart.getState().activeView;
|
const currentUiState = appstate.uiStatePart.getState();
|
||||||
const initialView = this.resolvedViewTabs.find(
|
const initialView =
|
||||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === currentActiveView,
|
this.findViewBySlug(currentUiState.activeView, currentUiState.activeSubview) ||
|
||||||
) || this.resolvedViewTabs[0];
|
this.resolvedViewTabs[0];
|
||||||
await appDash.loadView(initialView);
|
await appDash.loadView(initialView);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for stored session (persistent login state)
|
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
if (loginState.identity?.jwt) {
|
if (loginState.identity?.jwt) {
|
||||||
if (loginState.identity.expiresAt > Date.now()) {
|
if (loginState.identity.expiresAt > Date.now()) {
|
||||||
// Switch to dashboard immediately (no flash of login form)
|
|
||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
if (simpleLogin) {
|
if (simpleLogin) {
|
||||||
await simpleLogin.switchToSlottedContent();
|
await simpleLogin.switchToSlottedContent();
|
||||||
}
|
}
|
||||||
// Validate token with server in the background
|
|
||||||
try {
|
try {
|
||||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetSystemStatus
|
interfaces.requests.IReq_GetSystemStatus
|
||||||
@@ -165,11 +281,9 @@ export class ObAppShell extends DeesElement {
|
|||||||
const response = await typedRequest.fire({ identity: loginState.identity });
|
const response = await typedRequest.fire({ identity: loginState.identity });
|
||||||
appstate.systemStatePart.setState({ status: response.status });
|
appstate.systemStatePart.setState({ status: response.status });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Token rejected by server - switch back to login
|
|
||||||
console.warn('Stored session invalid, returning to login:', err);
|
console.warn('Stored session invalid, returning to login:', err);
|
||||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
if (simpleLogin) {
|
if (simpleLogin) {
|
||||||
// Force page reload to show login properly
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,14 +324,13 @@ export class ObAppShell extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncAppdashView(viewName: string): void {
|
private syncAppdashView(viewName: string, subviewName: string | null): void {
|
||||||
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||||
if (!appDash || this.resolvedViewTabs.length === 0) return;
|
if (!appDash || this.resolvedViewTabs.length === 0) return;
|
||||||
// Match kebab-case view name (e.g., 'app-store') to tab name (e.g., 'App Store')
|
|
||||||
const targetTab = this.resolvedViewTabs.find(
|
const targetTab = this.findViewBySlug(viewName, subviewName);
|
||||||
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === viewName
|
if (!targetTab || appDash.selectedView === targetTab) return;
|
||||||
);
|
|
||||||
if (!targetTab) return;
|
|
||||||
appDash.loadView(targetTab);
|
appDash.loadView(targetTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import { appRouter } from '../router.js';
|
import { appRouter } from '../router.js';
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +12,8 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
type TGatewayDnsRecord = appstate.INetworkState['gatewayDnsRecords'][number];
|
||||||
|
|
||||||
@customElement('ob-view-dns-records')
|
@customElement('ob-view-dns-records')
|
||||||
export class ObViewDnsRecords extends DeesElement {
|
export class ObViewDnsRecords extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
@@ -37,16 +40,11 @@ export class ObViewDnsRecords extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.table { border: 1px solid var(--ci-shade-2, #e4e4e7); border-radius: 10px; overflow: hidden; }
|
|
||||||
.row { display: grid; grid-template-columns: 2fr 90px 2fr 90px 140px 220px; gap: 16px; align-items: center; padding: 14px 16px; border-bottom: 1px solid var(--ci-shade-2, #e4e4e7); }
|
|
||||||
.row:last-child { border-bottom: none; }
|
|
||||||
.header { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--ci-shade-5, #71717a); background: var(--ci-shade-1, #f4f4f5); }
|
|
||||||
.name { font-weight: 600; }
|
.name { font-weight: 600; }
|
||||||
.value { font-family: monospace; color: var(--ci-shade-5, #71717a); overflow-wrap: anywhere; }
|
.value { font-family: monospace; color: var(--ci-shade-5, #71717a); overflow-wrap: anywhere; }
|
||||||
|
.muted { color: var(--ci-shade-5, #71717a); font-size: 13px; }
|
||||||
.badge { border-radius: 999px; padding: 3px 8px; background: var(--ci-shade-1, #f4f4f5); font-size: 12px; }
|
.badge { border-radius: 999px; padding: 3px 8px; background: var(--ci-shade-1, #f4f4f5); font-size: 12px; }
|
||||||
.missing { color: #dc2626; }
|
.missing { color: #dc2626; }
|
||||||
a, button.link { color: var(--ci-primary, #2563eb); background: none; border: none; padding: 0; cursor: pointer; font: inherit; text-decoration: none; }
|
|
||||||
.actions { display: flex; gap: 12px; }
|
|
||||||
.empty { padding: 32px; text-align: center; color: var(--ci-shade-5, #71717a); }
|
.empty { padding: 32px; text-align: center; color: var(--ci-shade-5, #71717a); }
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -60,29 +58,60 @@ export class ObViewDnsRecords extends DeesElement {
|
|||||||
const records = this.networkState.gatewayDnsRecords;
|
const records = this.networkState.gatewayDnsRecords;
|
||||||
return html`
|
return html`
|
||||||
<ob-sectionheading>DNS Records</ob-sectionheading>
|
<ob-sectionheading>DNS Records</ob-sectionheading>
|
||||||
<div class="table">
|
${records.length
|
||||||
<div class="row header">
|
? html`
|
||||||
<span>Name</span>
|
<dees-table
|
||||||
<span>Type</span>
|
.heading1=${'Gateway DNS Records'}
|
||||||
<span>Value</span>
|
.heading2=${'DNS records published through dcrouter for Onebox services'}
|
||||||
<span>Status</span>
|
.data=${records}
|
||||||
<span>Service</span>
|
.showColumnFilters=${true}
|
||||||
<span>Actions</span>
|
.displayFunction=${(record: TGatewayDnsRecord) => ({
|
||||||
</div>
|
Name: html`
|
||||||
${records.length ? records.map((record) => html`
|
<div>
|
||||||
<div class="row ${record.status === 'missing' ? 'missing' : ''}">
|
<div class="name">${record.name}</div>
|
||||||
<span class="name">${record.name}</span>
|
${record.domainName ? html`<div class="muted">${record.domainName}</div>` : ''}
|
||||||
<span><span class="badge">${record.type}</span></span>
|
</div>
|
||||||
<span class="value">${record.value || '-'}</span>
|
`,
|
||||||
<span>${record.status}</span>
|
Type: html`<span class="badge">${record.type}</span>`,
|
||||||
<span>${record.serviceName || record.appId}</span>
|
Value: html`<span class="value">${record.value || '-'}</span>`,
|
||||||
<span class="actions">
|
Status: html`<span class=${record.status === 'missing' ? 'missing' : ''}>${record.status}</span>`,
|
||||||
<button class="link" @click=${() => appRouter.navigateToView('services')}>View service</button>
|
Service: record.serviceName || record.appId || '-',
|
||||||
${record.manageUrl ? html`<a href=${record.manageUrl} target="_blank" rel="noopener">Manage in dcrouter</a>` : ''}
|
})}
|
||||||
</span>
|
.dataActions=${[
|
||||||
</div>
|
{
|
||||||
`) : html`<div class="empty">No gateway DNS records found. Configure a dcrouter gateway in Settings.</div>`}
|
name: 'Refresh',
|
||||||
</div>
|
iconName: 'lucide:rotateCw',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await appstate.networkStatePart.dispatchAction(
|
||||||
|
appstate.fetchGatewayDnsRecordsAction,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View service',
|
||||||
|
iconName: 'lucide:boxes',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
appRouter.navigateToView('services');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manage in dcrouter',
|
||||||
|
iconName: 'lucide:externalLink',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionRelevancyCheckFunc: (record: TGatewayDnsRecord) => !!record.manageUrl,
|
||||||
|
actionFunc: async (actionData: plugins.deesCatalog.ITableActionDataArg<TGatewayDnsRecord>) => {
|
||||||
|
if (actionData.item.manageUrl) {
|
||||||
|
globalThis.open(actionData.item.manageUrl, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction<TGatewayDnsRecord>[]}
|
||||||
|
></dees-table>
|
||||||
|
`
|
||||||
|
: html`<div class="empty">No gateway DNS records found. Configure a dcrouter gateway in Settings.</div>`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -10,6 +11,8 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
type TGatewayDomain = appstate.INetworkState['gatewayDomains'][number];
|
||||||
|
|
||||||
@customElement('ob-view-domains')
|
@customElement('ob-view-domains')
|
||||||
export class ObViewDomains extends DeesElement {
|
export class ObViewDomains extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
@@ -36,25 +39,9 @@ export class ObViewDomains extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.table {
|
|
||||||
border: 1px solid var(--ci-shade-2, #e4e4e7);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr 120px 120px 140px;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid var(--ci-shade-2, #e4e4e7);
|
|
||||||
}
|
|
||||||
.row:last-child { border-bottom: none; }
|
|
||||||
.header { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--ci-shade-5, #71717a); background: var(--ci-shade-1, #f4f4f5); }
|
|
||||||
.domain { font-weight: 600; }
|
.domain { font-weight: 600; }
|
||||||
.muted { color: var(--ci-shade-5, #71717a); font-size: 13px; }
|
.muted { color: var(--ci-shade-5, #71717a); font-size: 13px; }
|
||||||
.badge { border-radius: 999px; padding: 3px 8px; background: var(--ci-shade-1, #f4f4f5); font-size: 12px; }
|
.badge { border-radius: 999px; padding: 3px 8px; background: var(--ci-shade-1, #f4f4f5); font-size: 12px; }
|
||||||
a { color: var(--ci-primary, #2563eb); text-decoration: none; }
|
|
||||||
.empty { padding: 32px; text-align: center; color: var(--ci-shade-5, #71717a); }
|
.empty { padding: 32px; text-align: center; color: var(--ci-shade-5, #71717a); }
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -71,27 +58,51 @@ export class ObViewDomains extends DeesElement {
|
|||||||
<div class="muted" style="margin-bottom: 16px;">
|
<div class="muted" style="margin-bottom: 16px;">
|
||||||
Domains are managed in dcrouter. Onebox shows gateway visibility for deployed services.
|
Domains are managed in dcrouter. Onebox shows gateway visibility for deployed services.
|
||||||
</div>
|
</div>
|
||||||
<div class="table">
|
${domains.length
|
||||||
<div class="row header">
|
? html`
|
||||||
<span>Domain</span>
|
<dees-table
|
||||||
<span>Source</span>
|
.heading1=${'Gateway Domains'}
|
||||||
<span>Authoritative</span>
|
.heading2=${'Domains imported from dcrouter gateway visibility'}
|
||||||
<span>Services</span>
|
.data=${domains}
|
||||||
<span>Actions</span>
|
.showColumnFilters=${true}
|
||||||
</div>
|
.displayFunction=${(domain: TGatewayDomain) => ({
|
||||||
${domains.length ? domains.map((domain) => html`
|
Domain: html`
|
||||||
<div class="row">
|
<div>
|
||||||
<span>
|
<div class="domain">${domain.name}</div>
|
||||||
<span class="domain">${domain.name}</span>
|
${domain.providerId ? html`<div class="muted">Provider: ${domain.providerId}</div>` : ''}
|
||||||
${domain.providerId ? html`<div class="muted">Provider: ${domain.providerId}</div>` : ''}
|
</div>
|
||||||
</span>
|
`,
|
||||||
<span><span class="badge">${domain.source || 'dcrouter'}</span></span>
|
Source: html`<span class="badge">${domain.source || 'dcrouter'}</span>`,
|
||||||
<span>${domain.authoritative ? 'Yes' : 'No'}</span>
|
Authoritative: domain.authoritative ? 'Yes' : 'No',
|
||||||
<span>${domain.serviceCount || 0}</span>
|
Services: domain.serviceCount || 0,
|
||||||
<span>${domain.manageUrl ? html`<a href=${domain.manageUrl} target="_blank" rel="noopener">Manage in dcrouter</a>` : '-'}</span>
|
})}
|
||||||
</div>
|
.dataActions=${[
|
||||||
`) : html`<div class="empty">No gateway domains found. Configure a dcrouter gateway in Settings.</div>`}
|
{
|
||||||
</div>
|
name: 'Refresh',
|
||||||
|
iconName: 'lucide:rotateCw',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await appstate.networkStatePart.dispatchAction(
|
||||||
|
appstate.fetchGatewayDomainsAction,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manage in dcrouter',
|
||||||
|
iconName: 'lucide:externalLink',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionRelevancyCheckFunc: (domain: TGatewayDomain) => !!domain.manageUrl,
|
||||||
|
actionFunc: async (actionData: plugins.deesCatalog.ITableActionDataArg<TGatewayDomain>) => {
|
||||||
|
if (actionData.item.manageUrl) {
|
||||||
|
globalThis.open(actionData.item.manageUrl, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction<TGatewayDomain>[]}
|
||||||
|
></dees-table>
|
||||||
|
`
|
||||||
|
: html`<div class="empty">No gateway domains found. Configure a dcrouter gateway in Settings.</div>`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,32 @@ export class ObViewSettings extends DeesElement {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gateway-readonly {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-readonly-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#52525b', '#d4d4d8')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-readonly-value {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway-readonly-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
|
}
|
||||||
|
|
||||||
dees-input-text {
|
dees-input-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -240,11 +266,11 @@ export class ObViewSettings extends DeesElement {
|
|||||||
${this.renderGatewayInput('dcrouterManagedOpsPort', 'Local Ops Port', String(settings?.dcrouterManagedOpsPort || 3300), 'Bound to 127.0.0.1 for Onebox to call dcrouter APIs.')}
|
${this.renderGatewayInput('dcrouterManagedOpsPort', 'Local Ops Port', String(settings?.dcrouterManagedOpsPort || 3300), 'Bound to 127.0.0.1 for Onebox to call dcrouter APIs.')}
|
||||||
${this.renderGatewayInput('dcrouterManagedHttpPort', 'Public HTTP Port', String(settings?.dcrouterManagedHttpPort || 80), 'Host port owned by dcrouter for HTTP ingress.')}
|
${this.renderGatewayInput('dcrouterManagedHttpPort', 'Public HTTP Port', String(settings?.dcrouterManagedHttpPort || 80), 'Host port owned by dcrouter for HTTP ingress.')}
|
||||||
${this.renderGatewayInput('dcrouterManagedHttpsPort', 'Public HTTPS Port', String(settings?.dcrouterManagedHttpsPort || 443), 'Host port owned by dcrouter for HTTPS ingress.')}
|
${this.renderGatewayInput('dcrouterManagedHttpsPort', 'Public HTTPS Port', String(settings?.dcrouterManagedHttpsPort || 443), 'Host port owned by dcrouter for HTTPS ingress.')}
|
||||||
${this.renderGatewayInput('dcrouterGatewayClientId', 'Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || '', 'Leave empty to let Onebox create a stable ID.')}
|
${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || 'Created when managed dcrouter starts', 'Diagnostic only. Onebox manages this local client automatically.')}
|
||||||
` : mode === 'external' ? html`
|
` : mode === 'external' ? html`
|
||||||
${this.renderGatewayInput('dcrouterGatewayUrl', 'Gateway URL', settings?.dcrouterGatewayUrl || '', 'Base URL of the dcrouter OpsServer.')}
|
${this.renderGatewayInput('dcrouterGatewayUrl', 'Gateway URL', settings?.dcrouterGatewayUrl || '', 'Base URL of the dcrouter OpsServer.')}
|
||||||
${this.renderGatewayInput('dcrouterGatewayApiToken', 'API Token', settings?.dcrouterGatewayApiToken || '', 'Requires gateway-client access in dcrouter.', true)}
|
${this.renderGatewayInput('dcrouterGatewayApiToken', 'API Token', settings?.dcrouterGatewayApiToken || '', 'Requires gateway-client access in dcrouter.', true)}
|
||||||
${this.renderGatewayInput('dcrouterGatewayClientId', 'Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || '', 'Leave empty to let Onebox create a stable ID.')}
|
${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || settings?.dcrouterWorkHosterId || 'Derived from token', 'Configure this in dcrouter Gateway Clients, not in Onebox.')}
|
||||||
${this.renderGatewayInput('dcrouterTargetHost', 'Target Host', settings?.dcrouterTargetHost || '', 'Defaults to the configured server IP when empty.')}
|
${this.renderGatewayInput('dcrouterTargetHost', 'Target Host', settings?.dcrouterTargetHost || '', 'Defaults to the configured server IP when empty.')}
|
||||||
${this.renderGatewayInput('dcrouterTargetPort', 'Target Port', String(settings?.dcrouterTargetPort || 80), 'Internal HTTP port dcrouter forwards to.')}
|
${this.renderGatewayInput('dcrouterTargetPort', 'Target Port', String(settings?.dcrouterTargetPort || 80), 'Internal HTTP port dcrouter forwards to.')}
|
||||||
` : html`
|
` : html`
|
||||||
@@ -316,6 +342,16 @@ export class ObViewSettings extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderGatewayReadonly(label: string, value: string, hint: string): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="gateway-readonly">
|
||||||
|
<div class="gateway-readonly-label">${label}</div>
|
||||||
|
<div class="gateway-readonly-value">${value}</div>
|
||||||
|
<div class="gateway-readonly-hint">${hint}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private updateGatewayDraft(
|
private updateGatewayDraft(
|
||||||
key: keyof NonNullable<appstate.ISettingsState['settings']>,
|
key: keyof NonNullable<appstate.ISettingsState['settings']>,
|
||||||
value: string,
|
value: string,
|
||||||
@@ -351,7 +387,6 @@ export class ObViewSettings extends DeesElement {
|
|||||||
dcrouterManagedDataDir: settings.dcrouterManagedDataDir || './.nogit/dcrouter-data',
|
dcrouterManagedDataDir: settings.dcrouterManagedDataDir || './.nogit/dcrouter-data',
|
||||||
dcrouterGatewayUrl: settings.dcrouterGatewayUrl || '',
|
dcrouterGatewayUrl: settings.dcrouterGatewayUrl || '',
|
||||||
dcrouterGatewayApiToken: settings.dcrouterGatewayApiToken || '',
|
dcrouterGatewayApiToken: settings.dcrouterGatewayApiToken || '',
|
||||||
dcrouterGatewayClientId: settings.dcrouterGatewayClientId || settings.dcrouterWorkHosterId || '',
|
|
||||||
dcrouterTargetHost: settings.dcrouterTargetHost || '',
|
dcrouterTargetHost: settings.dcrouterTargetHost || '',
|
||||||
dcrouterTargetPort: Number(settings.dcrouterTargetPort) || 80,
|
dcrouterTargetPort: Number(settings.dcrouterTargetPort) || 80,
|
||||||
},
|
},
|
||||||
|
|||||||
+94
-24
@@ -3,12 +3,40 @@ import * as appstate from './appstate.js';
|
|||||||
|
|
||||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
export const validViews = [
|
const flatViews = ['dashboard', 'settings'] as const;
|
||||||
'dashboard', 'app-store', 'services', 'domains', 'dns-records', 'network',
|
|
||||||
'registries', 'tokens', 'settings',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type TValidView = typeof validViews[number];
|
const subviewMap: Record<string, readonly string[]> = {
|
||||||
|
apps: ['app-store', 'services'] as const,
|
||||||
|
network: ['proxy', 'domains', 'dns-records'] as const,
|
||||||
|
registry: ['registries', 'tokens'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSubview: Record<string, string> = {
|
||||||
|
apps: 'app-store',
|
||||||
|
network: 'proxy',
|
||||||
|
registry: 'registries',
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacySubviewTargetMap: Record<string, { view: string; subview: string }> = {
|
||||||
|
'app-store': { view: 'apps', subview: 'app-store' },
|
||||||
|
services: { view: 'apps', subview: 'services' },
|
||||||
|
proxy: { view: 'network', subview: 'proxy' },
|
||||||
|
domains: { view: 'network', subview: 'domains' },
|
||||||
|
'dns-records': { view: 'network', subview: 'dns-records' },
|
||||||
|
registries: { view: 'registry', subview: 'registries' },
|
||||||
|
tokens: { view: 'registry', subview: 'tokens' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validTopLevelViews = [...flatViews, ...Object.keys(subviewMap)] as const;
|
||||||
|
export type TValidView = typeof validTopLevelViews[number];
|
||||||
|
|
||||||
|
export function isValidView(view: string): boolean {
|
||||||
|
return (validTopLevelViews as readonly string[]).includes(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidSubview(view: string, subview: string): boolean {
|
||||||
|
return subviewMap[view]?.includes(subview) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
private router: InstanceType<typeof SmartRouter>;
|
private router: InstanceType<typeof SmartRouter>;
|
||||||
@@ -28,24 +56,37 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupRoutes(): void {
|
private setupRoutes(): void {
|
||||||
for (const view of validViews) {
|
for (const view of flatViews) {
|
||||||
this.router.on(`/${view}`, async () => {
|
this.router.on(`/${view}`, async () => {
|
||||||
this.updateViewState(view);
|
this.updateViewState(view, null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root redirect
|
for (const view of Object.keys(subviewMap)) {
|
||||||
|
this.router.on(`/${view}`, async () => {
|
||||||
|
this.navigateTo(`/${view}/${defaultSubview[view]}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const subview of subviewMap[view]) {
|
||||||
|
this.router.on(`/${view}/${subview}`, async () => {
|
||||||
|
this.updateViewState(view, subview);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.router.on('/', async () => {
|
this.router.on('/', async () => {
|
||||||
this.navigateTo('/dashboard');
|
this.navigateTo('/dashboard');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupStateSync(): void {
|
private setupStateSync(): void {
|
||||||
appstate.uiStatePart.select((s) => s.activeView).subscribe((activeView) => {
|
appstate.uiStatePart.select().subscribe((uiState: appstate.IUiState) => {
|
||||||
if (this.suppressStateUpdate) return;
|
if (this.suppressStateUpdate) return;
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
const expectedPath = `/${activeView}`;
|
const expectedPath = uiState.activeSubview
|
||||||
|
? `/${uiState.activeView}/${uiState.activeSubview}`
|
||||||
|
: `/${uiState.activeView}`;
|
||||||
|
|
||||||
if (currentPath !== expectedPath) {
|
if (currentPath !== expectedPath) {
|
||||||
this.suppressStateUpdate = true;
|
this.suppressStateUpdate = true;
|
||||||
@@ -60,25 +101,37 @@ class AppRouter {
|
|||||||
|
|
||||||
if (!path || path === '/') {
|
if (!path || path === '/') {
|
||||||
this.router.pushUrl('/dashboard');
|
this.router.pushUrl('/dashboard');
|
||||||
} else {
|
return;
|
||||||
const segments = path.split('/').filter(Boolean);
|
}
|
||||||
const view = segments[0];
|
|
||||||
|
|
||||||
if (validViews.includes(view as TValidView)) {
|
const segments = path.split('/').filter(Boolean);
|
||||||
this.updateViewState(view as TValidView);
|
const view = segments[0];
|
||||||
|
const subview = segments[1];
|
||||||
|
|
||||||
|
if (!isValidView(view)) {
|
||||||
|
this.router.pushUrl('/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subviewMap[view]) {
|
||||||
|
if (subview && isValidSubview(view, subview)) {
|
||||||
|
this.updateViewState(view, subview);
|
||||||
} else {
|
} else {
|
||||||
this.router.pushUrl('/dashboard');
|
this.router.pushUrl(`/${view}/${defaultSubview[view]}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.updateViewState(view, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateViewState(view: string): void {
|
private updateViewState(view: string, subview: string | null): void {
|
||||||
this.suppressStateUpdate = true;
|
this.suppressStateUpdate = true;
|
||||||
const currentState = appstate.uiStatePart.getState();
|
const currentState = appstate.uiStatePart.getState();
|
||||||
if (currentState.activeView !== view) {
|
if (currentState.activeView !== view || currentState.activeSubview !== subview) {
|
||||||
appstate.uiStatePart.setState({
|
appstate.uiStatePart.setState({
|
||||||
...currentState,
|
...currentState,
|
||||||
activeView: view,
|
activeView: view,
|
||||||
|
activeSubview: subview,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.suppressStateUpdate = false;
|
this.suppressStateUpdate = false;
|
||||||
@@ -88,17 +141,34 @@ class AppRouter {
|
|||||||
this.router.pushUrl(path);
|
this.router.pushUrl(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public navigateToView(view: string): void {
|
public navigateToView(view: string, subview?: string): void {
|
||||||
const normalized = view.toLowerCase().replace(/\s+/g, '-');
|
const normalizedView = view.toLowerCase().replace(/\s+/g, '-');
|
||||||
if (validViews.includes(normalized as TValidView)) {
|
const normalizedSubview = subview?.toLowerCase().replace(/\s+/g, '-');
|
||||||
this.navigateTo(`/${normalized}`);
|
|
||||||
} else {
|
if (!isValidView(normalizedView)) {
|
||||||
|
const legacyTarget = legacySubviewTargetMap[normalizedView];
|
||||||
|
if (legacyTarget) {
|
||||||
|
this.navigateToView(legacyTarget.view, legacyTarget.subview);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.navigateTo('/dashboard');
|
this.navigateTo('/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedSubview && isValidSubview(normalizedView, normalizedSubview)) {
|
||||||
|
this.navigateTo(`/${normalizedView}/${normalizedSubview}`);
|
||||||
|
} else if (subviewMap[normalizedView]) {
|
||||||
|
this.navigateTo(`/${normalizedView}/${defaultSubview[normalizedView]}`);
|
||||||
|
} else {
|
||||||
|
this.navigateTo(`/${normalizedView}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentView(): string {
|
public getCurrentView(): string {
|
||||||
return appstate.uiStatePart.getState().activeView;
|
const uiState = appstate.uiStatePart.getState();
|
||||||
|
return uiState.activeSubview
|
||||||
|
? `${uiState.activeView}/${uiState.activeSubview}`
|
||||||
|
: uiState.activeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user