add config preview with a bunch of other tweaks

This commit is contained in:
Joel Wetzell
2026-03-14 10:26:53 -05:00
parent 4ca78f6cbf
commit 36c60a6ce7
15 changed files with 179 additions and 186 deletions
-3
View File
@@ -1,10 +1,7 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideRouter(appRoutes),
], ],
}; };
+44 -44
View File
@@ -1,48 +1,48 @@
<div class="h-screen max-h-screen flex flex-col"> <div class="h-screen max-h-screen flex flex-col">
<mat-toolbar color="primary" class="w-full"> <div class="w-full">
@if(eventsService.status() === 'open') { <mat-toolbar color="primary">
<div matTooltip="Connected" class="h-3 w-3 rounded-full bg-green-400 mr-2"></div> @if(eventsService.status() === 'open') {
} @else if (eventsService.status() === 'closed') { <div matTooltip="Connected" class="h-3 w-3 rounded-full bg-green-400 mr-2"></div>
<div matTooltip="Disconnected" class="h-3 w-3 rounded-full bg-orange-400 mr-2"></div> } @else if (eventsService.status() === 'closed') {
} @else if (eventsService.status() === 'error') { <div matTooltip="Disconnected" class="h-3 w-3 rounded-full bg-orange-400 mr-2"></div>
<div matTooltip="Error" class="h-3 w-3 rounded-full bg-red-400 mr-2"></div> } @else if (eventsService.status() === 'error') {
} <div matTooltip="Error" class="h-3 w-3 rounded-full bg-red-400 mr-2"></div>
<span>showbridge</span> }
<div> <span>showbridge</span>
<label class="ml-4 mr-2">URL:</label> <div>
<input <label class="ml-4 mr-2">URL:</label>
type="text" <input
class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm" type="text"
[placeholder]="'http://localhost:8000'" class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm"
[value]="settingsService.baseUrl()" [placeholder]="'http://localhost:8000'"
(change)="settingsService.baseUrl.set($event.target.value)" [value]="settingsService.baseUrl()"
/> (change)="settingsService.baseUrl.set($event.target.value)"
</div> />
<span class="flex-auto"></span>
<button mat-icon-button matTooltip="Download Config" (click)="downloadConfig()">
<mat-icon>download</mat-icon>
</button>
@if (configService.pendingConfigIsValid()) {
<button mat-icon-button matTooltip="Apply Config" (click)="applyConfig()" [disabled]="!configService.configIsDirty()">
<mat-icon>save</mat-icon>
</button>
} @else {
<div
class="flex items-center justify-center text-red-500 hover:cursor-pointer"
matTooltip="Config Errors"
>
<mat-icon>error</mat-icon>
</div> </div>
} <span class="flex-auto"></span>
</mat-toolbar> @if (configService.pendingConfigIsValid()) {
<div class="flex-grow h-full overflow-y-auto"> <button mat-icon-button matTooltip="Apply Config" (click)="applyConfig()" [disabled]="!configService.configIsDirty()">
<router-outlet></router-outlet> <mat-icon>save</mat-icon>
<!-- @if (configService.currentlyShownConfig() && schemaService.schemasLoaded()) { </button>
<app-config [(config)]="configService.currentlyShownConfig"></app-config> } @else {
} @else { <div
<div class="flex items-center justify-center mt-28"> class="flex items-center justify-center text-red-500 hover:cursor-pointer"
<mat-spinner></mat-spinner> matTooltip="Config Errors"
</div> >
} --> <mat-icon>error</mat-icon>
</div>
}
</mat-toolbar>
</div> </div>
@if (schemaService.schemasLoaded()) {
<div class="flex-grow overflow-hidden flex">
<div class="flex-grow overflow-y-auto">
<app-module-list [modules]="config()?.modules" (modulesChange)="modulesUpdated($event)"></app-module-list>
<app-route-list [routes]="config()?.routes" [moduleIds]="moduleIds()" (routesChange)="routesUpdated($event)"></app-route-list>
</div>
<div class="w-1/2 overflow-y-hidden">
<app-config-preview [config]="config()"></app-config-preview>
</div>
</div>
}
</div> </div>
-13
View File
@@ -1,13 +0,0 @@
import { Routes } from '@angular/router';
export const appRoutes: Routes = [
{
path: '',
redirectTo: 'config',
pathMatch: 'full',
},
{
path: 'config',
loadComponent: () => import('./components/config/config.component').then((m) => m.ConfigComponent),
},
];
+49 -41
View File
@@ -1,4 +1,4 @@
import { Component, effect, inject } from '@angular/core'; import { Component, computed, effect, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@@ -7,12 +7,15 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Config } from './models/config.models'; import { Config, ModuleConfig, RouteConfig } from './models/config.models';
import { ConfigService } from './services/config.service'; import { ConfigService } from './services/config.service';
import { EventsService } from './services/events.service'; import { EventsService } from './services/events.service';
import { SchemaService } from './services/schema.service'; import { SchemaService } from './services/schema.service';
import { SettingsService } from './services/settings.service'; import { SettingsService } from './services/settings.service';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { ModuleListComponent } from './components/module-list/module-list.component';
import { RouteListComponent } from './components/route-list/route-list.component';
import { ConfigPreviewComponent } from './components/config-preview/config-preview.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -23,12 +26,28 @@ import { MatTooltipModule } from '@angular/material/tooltip';
MatMenuModule, MatMenuModule,
MatButtonModule, MatButtonModule,
MatTooltipModule, MatTooltipModule,
RouterOutlet ModuleListComponent,
RouteListComponent,
ConfigPreviewComponent
], ],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.css', styleUrl: './app.css',
}) })
export class App { export class App {
config = computed<Config | undefined>(() => this.configService.currentlyShownConfig());
modules = computed(() => this.config()?.modules ?? []);
routes = computed(() => this.config()?.routes ?? []);
moduleIds = computed(() => {
const config = this.config();
if (config !== undefined && config.modules !== undefined) {
return config.modules.map((module) => module.id!) ?? [];
}
return [];
});
public schemaService = inject(SchemaService); public schemaService = inject(SchemaService);
public configService = inject(ConfigService); public configService = inject(ConfigService);
@@ -57,46 +76,35 @@ export class App {
this.configService.updateCurrentlyShownConfig(config); this.configService.updateCurrentlyShownConfig(config);
} }
downloadConfig() { modulesUpdated(modules: ModuleConfig[] | undefined) {
const config = this.configService.currentlyShownConfig(); if (modules === undefined) {
if (config) { console.error('modules is undefined, not updating');
this.downloadYAML(config, 'config.yaml'); return;
} else {
this.snackBar.open('No config to download.', 'Dismiss', {
duration: 3000,
});
} }
this.configService.currentlyShownConfig.update((config) => {
if (config) {
return {
...config,
modules: modules,
};
}
return config;
});
} }
downloadJSON(data: object, filename: string) { routesUpdated(routes: RouteConfig[] | undefined) {
const content = JSON.stringify(data, null, 2); if (routes === undefined) {
const dataUri = URL.createObjectURL( console.error('routes is undefined, not updating');
new Blob([content], { return;
type: 'application/json;charset=utf-8', }
}), this.configService.currentlyShownConfig.update((config) => {
); if (config) {
const dummyLink = document.createElement('a'); return {
dummyLink.href = dataUri; ...config,
dummyLink.download = filename; routes: routes,
};
document.body.appendChild(dummyLink); }
dummyLink.click(); return config;
document.body.removeChild(dummyLink); });
}
downloadYAML(data: object, filename: string) {
const content = yaml.dump(data);
const dataUri = URL.createObjectURL(
new Blob([content], {
type: 'application/yaml;charset=utf-8',
}),
);
const dummyLink = document.createElement('a');
dummyLink.href = dataUri;
dummyLink.download = filename;
document.body.appendChild(dummyLink);
dummyLink.click();
document.body.removeChild(dummyLink);
} }
} }
@@ -0,0 +1,14 @@
<div class="h-full flex flex-col">
<div class="flex items-center">
<div class="text-white">Config Preview</div>
<div class="flex-grow"></div>
<div>
<button mat-icon-button matTooltip="Download Config" (click)="downloadConfig()">
<mat-icon class="!text-white">download</mat-icon>
</button>
</div>
</div>
<div class="flex-1 border border-gray-500 overflow-y-auto">
<pre class="p-3 text-xs text-white leading-relaxed whitespace-pre-wrap">{{ yamlString() }}</pre>
</div>
</div>
@@ -0,0 +1,67 @@
import { JsonPipe } from '@angular/common';
import { Component, computed, input } from '@angular/core';
import * as yaml from 'js-yaml';
import { Config } from '../../models/config.models';
import { MatIcon, MatIconModule } from "@angular/material/icon";
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-config-preview',
imports: [
MatIconModule,
MatButtonModule,
MatTooltipModule
],
templateUrl: './config-preview.component.html',
styleUrl: './config-preview.component.css',
})
export class ConfigPreviewComponent {
config = input<Config | undefined>();
yamlString = computed(() => {
if (this.config() === undefined) {
return '';
}
return yaml.dump(this.config());
});
downloadConfig() {
const config = this.config();
if (config) {
this.downloadYAML(config, 'config.yaml');
}
}
downloadJSON(data: object, filename: string) {
const content = JSON.stringify(data, null, 2);
const dataUri = URL.createObjectURL(
new Blob([content], {
type: 'application/json;charset=utf-8',
}),
);
const dummyLink = document.createElement('a');
dummyLink.href = dataUri;
dummyLink.download = filename;
document.body.appendChild(dummyLink);
dummyLink.click();
document.body.removeChild(dummyLink);
}
downloadYAML(data: object, filename: string) {
const content = yaml.dump(data);
const dataUri = URL.createObjectURL(
new Blob([content], {
type: 'application/yaml;charset=utf-8',
}),
);
const dummyLink = document.createElement('a');
dummyLink.href = dataUri;
dummyLink.download = filename;
document.body.appendChild(dummyLink);
dummyLink.click();
document.body.removeChild(dummyLink);
}
}
@@ -1,4 +0,0 @@
@if (config() && schemaService.schemasLoaded()) {
<app-module-list [modules]="config()?.modules" (modulesChange)="modulesUpdated($event)"></app-module-list>
<app-route-list [routes]="config()?.routes" [moduleIds]="moduleIds()" (routesChange)="routesUpdated($event)"></app-route-list>
}
@@ -1,76 +0,0 @@
import { Component, computed, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Config, ModuleConfig, RouteConfig } from '../../models/config.models';
import { ConfigService } from '../../services/config.service';
import { SchemaService } from '../../services/schema.service';
import { ModuleListComponent } from '../module-list/module-list.component';
import { RouteListComponent } from '../route-list.component/route-list.component';
import { EventsService } from '../../services/events.service';
@Component({
selector: 'app-config',
imports: [
MatMenuModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
ModuleListComponent,
RouteListComponent,
],
templateUrl: './config.component.html',
styleUrl: './config.component.css',
})
export class ConfigComponent {
config = computed<Config | undefined>(() => this.configService.currentlyShownConfig());
modules = computed(() => this.config()?.modules ?? []);
routes = computed(() => this.config()?.routes ?? []);
moduleIds = computed(() => {
const config = this.config();
if (config !== undefined && config.modules !== undefined) {
return config.modules.map((module) => module.id!) ?? [];
}
return [];
});
public configService = inject(ConfigService);
public schemaService = inject(SchemaService);
public eventsService = inject(EventsService);
modulesUpdated(modules: ModuleConfig[] | undefined) {
if (modules === undefined) {
console.error('modules is undefined, not updating');
return;
}
this.configService.currentlyShownConfig.update((config) => {
if (config) {
return {
...config,
modules: modules,
};
}
return config;
});
}
routesUpdated(routes: RouteConfig[] | undefined) {
if (routes === undefined) {
console.error('routes is undefined, not updating');
return;
}
this.configService.currentlyShownConfig.update((config) => {
if (config) {
return {
...config,
routes: routes,
};
}
return config;
});
}
}
@@ -13,7 +13,7 @@
{{ schema().title || module()?.type }} {{ schema().title || module()?.type }}
</div> </div>
<div class="flex-grow"></div> <div class="flex-grow"></div>
<div class="flex items-center justify-center hover:bg-gray-700" (click)="delete.emit()"> <div class="flex items-center justify-center hover:bg-gray-700 hover:cursor-pointer" (click)="delete.emit()">
<mat-icon class="!text-red-500">close</mat-icon> <mat-icon class="!text-red-500">close</mat-icon>
</div> </div>
</div> </div>
@@ -9,7 +9,7 @@
<div class="flex-grow ml-1 mr-3 text-white"> <div class="flex-grow ml-1 mr-3 text-white">
{{ schema()?.title || processor()?.type }} {{ schema()?.title || processor()?.type }}
</div> </div>
<div class="flex items-center justify-center hover:bg-gray-700" (click)="delete.emit()"> <div class="flex items-center justify-center hover:bg-gray-700 hover:cursor-pointer" (click)="delete.emit()">
<mat-icon class="!text-red-500">close</mat-icon> <mat-icon class="!text-red-500">close</mat-icon>
</div> </div>
</div> </div>
@@ -12,7 +12,7 @@
<div class="flex-grow overflow-x-hidden overflow-y-auto gap-1.5"> <div class="flex-grow overflow-x-hidden overflow-y-auto gap-1.5">
@for (route of routes(); track route; let i = $index) { @for (route of routes(); track route; let i = $index) {
<div class="m-2"> <div class="m-2">
<app-route <app-route
[path]="`routes/${i}`" [path]="`routes/${i}`"
(routeChange)="routeUpdated(i, $event)" (routeChange)="routeUpdated(i, $event)"
[route]="route" [route]="route"
@@ -3,14 +3,14 @@
<div <div
class="flex flex-col bg-gray-800 border-2 {{ class="flex flex-col bg-gray-800 border-2 {{
isInError() ? 'border-red-500' : 'border-gray-600' isInError() ? 'border-red-500' : 'border-gray-600'
}} border-solid w-full h-full" }} border-solid w-full"
> >
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<div class="flex items-center"> <div class="flex items-center">
<mat-icon [style.color]="indicatorColor()" matTooltip="Route Indicator">chevron_right</mat-icon> <mat-icon [style.color]="indicatorColor()" matTooltip="Route Indicator">chevron_right</mat-icon>
</div> </div>
<div class="flex-grow text-left ml-2 text-white">Route {{ index() }}</div> <div class="flex-grow text-left ml-2 text-white">Route {{ index() }}</div>
<div class="flex items-center justify-center hover:bg-gray-700" (click)="delete.emit()"> <div class="flex items-center justify-center hover:bg-gray-700 hover:cursor-pointer" (click)="delete.emit()">
<mat-icon class="!text-red-500">close</mat-icon> <mat-icon class="!text-red-500">close</mat-icon>
</div> </div>
</div> </div>