mirror of
https://github.com/jwetzell/showbridge-webui.git
synced 2026-09-10 07:39:25 +00:00
add config preview with a bunch of other tweaks
This commit is contained in:
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user