formatting

This commit is contained in:
Joel Wetzell
2026-04-05 18:09:47 -05:00
parent 10ff26e48c
commit e66195d702
24 changed files with 248 additions and 258 deletions
+3 -6
View File
@@ -1,15 +1,12 @@
import { expect, test } from '@playwright/test';
test.beforeEach(async ({page})=>{
await page.goto('/')
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(page.locator('div').nth(2)).toContainClass('bg-green-400');
})
});
test('has title', async ({ page }) => {
await page.goto('/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Showbridge/);
});
+5 -5
View File
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'
import { expect, test } from '@playwright/test';
test('add module', async ({ page }) => {
await page.goto('/');
await page.locator('app-module-list').getByText('add').click();
await page.getByRole('menuitem', { name: 'HTTP Server' }).click();
})
await page.goto('/');
await page.locator('app-module-list').getByText('add').click();
await page.getByRole('menuitem', { name: 'HTTP Server' }).click();
});
+5 -5
View File
@@ -23,11 +23,11 @@ export default defineConfig({
reporter: 'html',
webServer: [
{
name: "showbridge",
command: "showbridge --config config.yaml",
url: "http://localhost:8080/health",
timeout: 10000
}
name: 'showbridge',
command: 'showbridge --config config.yaml',
url: 'http://localhost:8080/health',
timeout: 10000,
},
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
+1 -3
View File
@@ -1,7 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
],
providers: [provideBrowserGlobalErrorListeners()],
};
+25 -13
View File
@@ -1,7 +1,7 @@
<div class="h-screen max-h-screen flex flex-col">
<div class="w-full">
<mat-toolbar color="primary">
@if(eventsService.status() === 'open') {
@if (eventsService.status() === 'open') {
<div matTooltip="Connected" class="h-3 w-3 rounded-full bg-green-400 mr-2"></div>
} @else if (eventsService.status() === 'closed') {
<div matTooltip="Disconnected" class="h-3 w-3 rounded-full bg-orange-400 mr-2"></div>
@@ -10,18 +10,23 @@
}
<span>showbridge</span>
<div>
<label class="ml-4 mr-2">URL:</label>
<input
type="text"
class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm"
[placeholder]="'http://localhost:8000'"
[value]="settingsService.baseUrl()"
(change)="settingsService.updateBaseUrl($event.target.value)"
/>
<label class="ml-4 mr-2">URL:</label>
<input
type="text"
class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm"
[placeholder]="'http://localhost:8000'"
[value]="settingsService.baseUrl()"
(change)="settingsService.updateBaseUrl($event.target.value)"
/>
</div>
<span class="flex-auto"></span>
@if (configService.pendingConfigIsValid()) {
<button mat-icon-button matTooltip="Apply Config" (click)="applyConfig()" [disabled]="!configService.configIsDirty()">
<button
mat-icon-button
matTooltip="Apply Config"
(click)="applyConfig()"
[disabled]="!configService.configIsDirty()"
>
<mat-icon>save</mat-icon>
</button>
} @else {
@@ -34,11 +39,18 @@
}
</mat-toolbar>
</div>
@if (eventsService.status() === 'open' && schemaService.schemasLoaded()) {
@if (eventsService.status() === 'open' && schemaService.schemasLoaded()) {
<div class="grow overflow-hidden flex m-2">
<div class="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>
<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>
+3 -4
View File
@@ -28,13 +28,12 @@ import { ConfigPreviewComponent } from './components/config-preview/config-previ
MatTooltipModule,
ModuleListComponent,
RouteListComponent,
ConfigPreviewComponent
],
ConfigPreviewComponent,
],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
config = computed<Config | undefined>(() => this.configService.currentlyShownConfig());
modules = computed(() => this.config()?.modules ?? []);
@@ -47,7 +46,7 @@ export class App {
}
return [];
});
public schemaService = inject(SchemaService);
public configService = inject(ConfigService);
+2 -5
View File
@@ -31,10 +31,7 @@ export class ArrayFormComponent implements OnInit {
ngOnInit(): void {
if (this.paramFormControl && this.paramInfo?.schema) {
if (!Array.isArray(this.paramFormControl.value)) {
this.arrayValue = parseStringToArray(
this.paramFormControl.value,
this.paramInfo.schema,
);
this.arrayValue = parseStringToArray(this.paramFormControl.value, this.paramInfo.schema);
} else {
this.arrayValue = this.paramFormControl.value;
}
@@ -101,7 +98,7 @@ export class ArrayFormComponent implements OnInit {
);
}
}
// NOTE(jwetzel): this is only needed for object item types
updateItem(index: number, value: any) {
if (this.arrayValue) {
@@ -2,17 +2,13 @@ import { JsonPipe } from '@angular/common';
import { Component, computed, input } from '@angular/core';
import * as yaml from 'js-yaml';
import { Config } from '../../models/config';
import { MatIcon, MatIconModule } from "@angular/material/icon";
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
],
imports: [MatIconModule, MatButtonModule, MatTooltipModule],
templateUrl: './config-preview.html',
styleUrl: './config-preview.css',
})
@@ -10,13 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-module-list',
imports: [
MatMenuModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
ModuleComponent
],
imports: [MatMenuModule, MatIconModule, MatButtonModule, MatTooltipModule, ModuleComponent],
templateUrl: './module-list.html',
styleUrl: './module-list.css',
})
+51 -44
View File
@@ -1,49 +1,56 @@
@if (module() && schema()) {
<div
class="flex flex-col bg-gray-800 border-2 {{
isInError() ? 'border-red-500' : 'border-gray-600'
}} border-solid w-full h-full"
>
<div class="flex items-center justify-center">
<div class="flex items-center">
<mat-icon [style.color]="inputIndicatorColor()" matTooltip="Input Indicator">keyboard_arrow_down</mat-icon>
<mat-icon [style.color]="outputIndicatorColor()" matTooltip="Output Indicator">keyboard_arrow_up</mat-icon>
</div>
<div class="text-white">
{{ schema().title || module()?.type }}
</div>
<div class="grow"></div>
<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>
</div>
<div
class="flex flex-col bg-gray-800 border-2 {{
isInError() ? 'border-red-500' : 'border-gray-600'
}} border-solid w-full h-full"
>
<div class="flex items-center justify-center">
<div class="flex items-center">
<mat-icon [style.color]="inputIndicatorColor()" matTooltip="Input Indicator"
>keyboard_arrow_down</mat-icon
>
<mat-icon [style.color]="outputIndicatorColor()" matTooltip="Output Indicator"
>keyboard_arrow_up</mat-icon
>
</div>
<hr class="bg-gray-600 h-0.5 border-0">
<div>
<form [formGroup]="formGroup" class="h-full">
<div class="m-2">
<div class="flex items-center m-1">
<label class="mr-2 text-white"> ID: </label>
<input
type="text"
class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm"
[placeholder]="'module-1'"
[formControlName]="'id'"
/>
<mat-icon class="text-white!" matTooltip="String">abc</mat-icon>
@if (!formGroup.controls['id'].valid) {
<!-- TODO(jwetzell): better error displaying -->
<div class="ml-2 text-red-500">{{ formGroup.controls['id'].errors | json }}</div>
}
</div>
<app-params-form
[paramsSchema]="schema()?.properties?.params"
[data]="params()"
(updated)="paramsUpdated($event)"
></app-params-form>
<div class="text-white">
{{ schema().title || module()?.type }}
</div>
<div class="grow"></div>
<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>
</div>
</div>
<hr class="bg-gray-600 h-0.5 border-0" />
<div>
<form [formGroup]="formGroup" class="h-full">
<div class="m-2">
<div class="flex items-center m-1">
<label class="mr-2 text-white"> ID: </label>
<input
type="text"
class="pl-1 border-2 border-gray-200 text-white border-solid rounded-sm"
[placeholder]="'module-1'"
[formControlName]="'id'"
/>
<mat-icon class="text-white!" matTooltip="String">abc</mat-icon>
@if (!formGroup.controls['id'].valid) {
<!-- TODO(jwetzell): better error displaying -->
<div class="ml-2 text-red-500">{{ formGroup.controls['id'].errors | json }}</div>
}
</div>
</form>
</div>
<div>
<app-params-form
[paramsSchema]="schema()?.properties?.params"
[data]="params()"
(updated)="paramsUpdated($event)"
></app-params-form>
</div>
</form>
</div>
<div>
@if (moduleConfigErrors().length > 0) {
<div class="m-2 p-2 border-2 border-red-500">
<div class="flex items-center mb-2 text-red-500">
@@ -58,5 +65,5 @@
</div>
}
</div>
</div>
</div>
}
@@ -1,95 +1,95 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParamsFormComponent } from './params-form';
import { SchemaService } from '../../services/schema';
import { SomeJSONSchema } from 'ajv/dist/types/json-schema';
describe('ParamsForm', ()=>{
let component: ParamsFormComponent;
let fixture: ComponentFixture<ParamsFormComponent>;
describe('ParamsForm', () => {
let component: ParamsFormComponent;
let fixture: ComponentFixture<ParamsFormComponent>;
beforeEach(async ()=>{
fixture = TestBed.createComponent(ParamsFormComponent);
component = fixture.componentInstance;
await fixture.whenStable();
})
beforeEach(async () => {
fixture = TestBed.createComponent(ParamsFormComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', ()=>{
expect(component).toBeTruthy();
})
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create form for schema', async ()=>{
const paramsSchema: SomeJSONSchema = {
type: "object",
properties: {},
required: []
}
fixture.componentRef.setInput('paramsSchema', paramsSchema)
await fixture.whenStable()
expect(fixture.nativeElement.querySelector('form')).toBeTruthy()
})
it('should create form for schema', async () => {
const paramsSchema: SomeJSONSchema = {
type: 'object',
properties: {},
required: [],
};
fixture.componentRef.setInput('paramsSchema', paramsSchema);
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('form')).toBeTruthy();
});
it('form should have input for string property', async ()=>{
const paramsSchema: SomeJSONSchema = {
type: "object",
properties: {
"test": {
type: "string",
}
},
required: []
}
fixture.componentRef.setInput('paramsSchema', paramsSchema)
await fixture.whenStable()
const formEl = fixture.nativeElement.querySelector('form')
expect(formEl).toBeDefined()
const inputEl = formEl.querySelector('input')
expect(inputEl).toBeDefined()
})
it('form should have input for string property', async () => {
const paramsSchema: SomeJSONSchema = {
type: 'object',
properties: {
test: {
type: 'string',
},
},
required: [],
};
fixture.componentRef.setInput('paramsSchema', paramsSchema);
await fixture.whenStable();
const formEl = fixture.nativeElement.querySelector('form');
expect(formEl).toBeDefined();
const inputEl = formEl.querySelector('input');
expect(inputEl).toBeDefined();
});
it('form should have select for string enum property', async ()=>{
const paramsSchema: SomeJSONSchema = {
type: "object",
properties: {
"test": {
type: "string",
enum: ['one','two','three']
}
},
required: []
}
fixture.componentRef.setInput('paramsSchema', paramsSchema)
await fixture.whenStable()
const formEl = fixture.nativeElement.querySelector('form')
expect(formEl).toBeDefined()
const selectEl = formEl.querySelector('select')
expect(selectEl).toBeDefined()
})
it('form should have select for string enum property', async () => {
const paramsSchema: SomeJSONSchema = {
type: 'object',
properties: {
test: {
type: 'string',
enum: ['one', 'two', 'three'],
},
},
required: [],
};
fixture.componentRef.setInput('paramsSchema', paramsSchema);
await fixture.whenStable();
const formEl = fixture.nativeElement.querySelector('form');
expect(formEl).toBeDefined();
const selectEl = formEl.querySelector('select');
expect(selectEl).toBeDefined();
});
it('form input should reflect data', async ()=>{
const paramsSchema: SomeJSONSchema = {
type: "object",
properties: {
"test": {
type: "string",
}
},
required: []
}
it('form input should reflect data', async () => {
const paramsSchema: SomeJSONSchema = {
type: 'object',
properties: {
test: {
type: 'string',
},
},
required: [],
};
fixture.componentRef.setInput('paramsSchema', paramsSchema)
await fixture.whenStable()
fixture.componentRef.setInput('data', {
test: 'hello'
})
await fixture.whenStable()
const inputEl = fixture.nativeElement.querySelector('input')
expect(inputEl).toBeDefined()
expect(inputEl.value).toBe('hello')
fixture.componentRef.setInput('paramsSchema', paramsSchema);
await fixture.whenStable();
fixture.componentRef.setInput('data', {
test: 'hello',
});
await fixture.whenStable();
const inputEl = fixture.nativeElement.querySelector('input');
expect(inputEl).toBeDefined();
expect(inputEl.value).toBe('hello');
fixture.componentRef.setInput('data', {
test: 'changed'
})
await fixture.whenStable()
expect(inputEl.value).toBe('changed')
})
})
fixture.componentRef.setInput('data', {
test: 'changed',
});
await fixture.whenStable();
expect(inputEl.value).toBe('changed');
});
});
@@ -1,11 +1,5 @@
import { JsonPipe } from '@angular/common';
import {
Component,
effect,
input,
OnDestroy,
output
} from '@angular/core';
import { Component, effect, input, OnDestroy, output } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
+5 -2
View File
@@ -9,12 +9,15 @@
<div class="grow ml-1 mr-3 text-white">
{{ schema()?.title || processor()?.type }}
</div>
<div class="flex items-center justify-center hover:bg-gray-700 hover:cursor-pointer" (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>
</div>
</div>
@if (hasParams()) {
<hr class="bg-gray-600 h-0.5 border-0">
<hr class="bg-gray-600 h-0.5 border-0" />
<div>
<app-params-form
[paramsSchema]="schema()?.properties?.params"
+2 -2
View File
@@ -37,7 +37,7 @@ export class ProcessorComponent {
paramsUpdated(params: any) {
this.processor.update((processor) => {
if (processor !== undefined) {
if (params !== undefined){
if (params !== undefined) {
return {
...processor,
params: params,
@@ -45,7 +45,7 @@ export class ProcessorComponent {
}
return {
...processor,
}
};
}
return undefined;
});
@@ -12,7 +12,7 @@
<div class="grow overflow-x-hidden overflow-y-auto gap-1.5">
@for (route of routes(); track $index; let i = $index) {
<div class="m-2">
<app-route
<app-route
[path]="`routes/${i}`"
(routeChange)="routeUpdated(i, $event)"
[route]="route"
+2 -9
View File
@@ -10,24 +10,17 @@ import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-route-list',
imports: [
RouteComponent,
MatMenuModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
],
imports: [RouteComponent, MatMenuModule, MatIconModule, MatButtonModule, MatTooltipModule],
templateUrl: './route-list.html',
styleUrl: './route-list.css',
})
export class RouteListComponent {
routes = model<RouteConfig[]>();
moduleIds = input<string[]>();
public schemaService = inject(SchemaService);
private snackBar = inject(MatSnackBar);
deleteRoute(index: number) {
this.routes.update((routes) => {
if (routes) {
+10 -5
View File
@@ -7,14 +7,19 @@
>
<div class="flex items-center justify-end">
<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 class="grow text-left ml-2 text-white">Route {{ index() }}</div>
<div class="flex items-center justify-center hover:bg-gray-700 hover:cursor-pointer" (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>
</div>
</div>
<hr class="bg-gray-600 h-0.5 border-0">
<hr class="bg-gray-600 h-0.5 border-0" />
<div>
<form [formGroup]="formGroup">
<div class="m-2">
@@ -58,7 +63,7 @@
<div class="text-md text-white text-center">Processors</div>
</div>
@if (processors() !== undefined && processors()!.length > 0) {
<div class="border-2 border-gray-500 m-2">
<div class="border-2 border-gray-500 m-2">
@for (processor of processors(); track $index; let i = $index) {
<div cdkDrag>
<app-processor
@@ -70,7 +75,7 @@
</div>
}
</div>
}
}
</div>
</div>
<div>
+12 -9
View File
@@ -91,14 +91,17 @@ export class RouteComponent {
});
if (this.index() !== undefined) {
this.eventsService.getRouteEventsForIndex(this.index()!).pipe(
tap((routeEvent)=>{
this.indicatorColor.set(routeEvent.error ? 'red' : 'greenyellow');
}),
debounceTime(100)
).subscribe((routeEvent) => {
this.indicatorColor.set('gray')
})
this.eventsService
.getRouteEventsForIndex(this.index()!)
.pipe(
tap((routeEvent) => {
this.indicatorColor.set(routeEvent.error ? 'red' : 'greenyellow');
}),
debounceTime(100),
)
.subscribe((routeEvent) => {
this.indicatorColor.set('gray');
});
}
}
@@ -115,7 +118,7 @@ export class RouteComponent {
this.route.update((route) => {
if (route) {
const processors = route.processors || [];
processors.push(processorTemplate);
return {
...route,
+4 -5
View File
@@ -1,5 +1,5 @@
export type Config = {
api: ApiConfig
api: ApiConfig;
modules: ModuleConfig[];
routes: RouteConfig[];
};
@@ -31,20 +31,19 @@ export type ProcessorConfig = {
params?: Record<string, any>;
};
export type ModuleError = {
index: number;
config: ModuleConfig;
error: string;
}
};
export type RouteError = {
index: number;
config: RouteConfig;
error: string;
}
};
export type ConfigError = {
moduleErrors?: ModuleError[];
routeErrors?: RouteError[];
}
};
+11 -11
View File
@@ -1,19 +1,19 @@
import { Config } from "./config"
import { Config } from './config';
export type RouterEvent<T, D> = {
type: T,
data?: D,
error?: string,
}
type: T;
data?: D;
error?: string;
};
export type RouteEventData = {
index: number
}
index: number;
};
export type InputEventData = {
source: string
}
source: string;
};
export type OutputEventData = {
destination: string
}
destination: string;
};
+5 -5
View File
@@ -34,20 +34,20 @@ export class ConfigService {
private http = inject(HttpClient);
private settingsService = inject(SettingsService);
private eventsService = inject(EventsService);
constructor(private schemaService: SchemaService) {
effect(() => {
console.log('config state changed', this.currentlyShownConfig());
});
effect(() =>{
effect(() => {
switch (this.eventsService.status()) {
case 'open':
console.log('Websocket connection opened, reloading config');
this.loadConfig();
break;
}
})
});
}
loadConfig() {
@@ -107,9 +107,9 @@ export class ConfigService {
setEmptyConfig() {
console.log('Setting empty config');
this.updateCurrentlyShownConfig({
api: {
api: {
enabled: true,
port: 8080
port: 8080,
},
modules: [],
routes: [],
+1 -6
View File
@@ -1,10 +1,5 @@
import { effect, inject, Injectable, signal } from '@angular/core';
import {
InputEventData,
OutputEventData,
RouteEventData,
RouterEvent,
} from '../models/events';
import { InputEventData, OutputEventData, RouteEventData, RouterEvent } from '../models/events';
import { filter, Subject } from 'rxjs';
import { SettingsService } from './settings';
@Injectable({
-2
View File
@@ -286,8 +286,6 @@ export class SchemaService {
return 0;
}
cleanArray(values: any[], itemSchema: SomeJSONSchema) {
if (Array.isArray(values)) {
switch (itemSchema.type) {
+13 -13
View File
@@ -1,6 +1,13 @@
import { FormGroup, ValidatorFn, Validators, FormControl, AbstractControl, ValidationErrors } from "@angular/forms";
import { SomeJSONSchema } from "ajv/dist/types/json-schema";
import { ParamsFormInfo } from "../models/form";
import {
FormGroup,
ValidatorFn,
Validators,
FormControl,
AbstractControl,
ValidationErrors,
} from '@angular/forms';
import { SomeJSONSchema } from 'ajv/dist/types/json-schema';
import { ParamsFormInfo } from '../models/form';
export function schemaToParamsFormInfo(schema: SomeJSONSchema): ParamsFormInfo {
const paramsFormInfo: ParamsFormInfo = {
@@ -67,10 +74,7 @@ export function schemaToParamsFormInfo(schema: SomeJSONSchema): ParamsFormInfo {
}
//TODO(jwetzell): figure out how to disable a control but not have to deal with undefined values on disabled controls
paramsFormInfo.formGroup.addControl(
paramKey,
new FormControl(formDefault, validators),
);
paramsFormInfo.formGroup.addControl(paramKey, new FormControl(formDefault, validators));
if (paramSchema.enum) {
paramsFormInfo.paramsInfo[paramKey].options = paramSchema.enum;
@@ -100,11 +104,7 @@ export function cleanParams(paramsSchema: SomeJSONSchema, params: any): any {
const paramSchema = paramsSchema.properties[paramKey];
// delete null/undefined/empty params that aren't required
if (
params[paramKey] === undefined ||
params[paramKey] === null ||
params[paramKey] === ''
) {
if (params[paramKey] === undefined || params[paramKey] === null || params[paramKey] === '') {
if (paramSchema.required) {
if (!paramSchema.includes(paramKey)) {
delete params[paramKey];
@@ -203,4 +203,4 @@ export function parseStringToArray(value: any, schema: SomeJSONSchema): any[] |
}
}
return undefined;
}
}