diff --git a/package-lock.json b/package-lock.json index 5c99d5f..f8ae485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "ajv": "8.17.1", "js-yaml": "4.1.1", "lodash-es": "4.17.23", - "ngx-timeago": "4.0.0", "rxjs": "7.8.2", "tslib": "2.3.0" }, @@ -4153,66 +4152,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", @@ -7241,19 +7180,6 @@ "node": ">= 0.6" } }, - "node_modules/ngx-timeago": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ngx-timeago/-/ngx-timeago-4.0.0.tgz", - "integrity": "sha512-pTzQyOzefsDGcaM5b4aVtM1gDiP8bteQBTJNyQ/pKR0Hbgue08Zq/SztJK8zZCo08kLi8WiC0DvgWzfQF30C1g==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": ">=20.0.0", - "rxjs": ">=7.0.0" - } - }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", diff --git a/package.json b/package.json index 583f965..04e5a37 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "ajv": "8.17.1", "js-yaml": "4.1.1", "lodash-es": "4.17.23", - "ngx-timeago": "4.0.0", "rxjs": "7.8.2", "tslib": "2.3.0" }, diff --git a/src/app/app.html b/src/app/app.html index d96f9eb..fdf26e0 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,12 +1,29 @@
+ @if(eventsService.status() === 'open') { +
+ } @else if (eventsService.status() === 'closed') { +
+ } @else if (eventsService.status() === 'error') { +
+ } showbridge +
+ + +
@if (configService.pendingConfigIsValid()) { - } @else { diff --git a/src/app/app.ts b/src/app/app.ts index 06fd22b..2dfb3f5 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,18 +1,18 @@ import { Component, effect, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { TimeagoModule } from 'ngx-timeago'; +import { RouterOutlet } from '@angular/router'; +import * as yaml from 'js-yaml'; import { Config } from './models/config.models'; import { ConfigService } from './services/config.service'; +import { EventsService } from './services/events.service'; import { SchemaService } from './services/schema.service'; -import { ConfigComponent } from './components/config/config.component'; -import * as yaml from 'js-yaml'; -import { RouterOutlet } from '@angular/router'; +import { SettingsService } from './services/settings.service'; +import { MatTooltipModule } from '@angular/material/tooltip'; @Component({ selector: 'app-root', @@ -20,9 +20,9 @@ import { RouterOutlet } from '@angular/router'; MatToolbarModule, MatProgressSpinnerModule, MatIconModule, - TimeagoModule, MatMenuModule, MatButtonModule, + MatTooltipModule, RouterOutlet ], templateUrl: './app.html', @@ -32,6 +32,8 @@ export class App { public schemaService = inject(SchemaService); public configService = inject(ConfigService); + public settingsService = inject(SettingsService); + public eventsService = inject(EventsService); private snackBar = inject(MatSnackBar); constructor() { effect(() => { @@ -44,8 +46,8 @@ export class App { applyConfig() { const config = this.configService.currentlyShownConfig(); if (config !== undefined) { - this.configService.saveConfig(config); - this.snackBar.open('Config Saved', 'Dismiss', { + this.configService.uploadConfig(config); + this.snackBar.open('Config Uploaded', 'Dismiss', { duration: 3000, }); } diff --git a/src/app/components/config/config.component.ts b/src/app/components/config/config.component.ts index e05c2f6..b4e0448 100644 --- a/src/app/components/config/config.component.ts +++ b/src/app/components/config/config.component.ts @@ -3,11 +3,12 @@ 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, ModuleConfiguration, RouteConfiguration } from '../../models/config.models'; +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', @@ -39,7 +40,9 @@ export class ConfigComponent { public configService = inject(ConfigService); public schemaService = inject(SchemaService); - modulesUpdated(modules: ModuleConfiguration[] | undefined) { + public eventsService = inject(EventsService); + + modulesUpdated(modules: ModuleConfig[] | undefined) { if (modules === undefined) { console.error('modules is undefined, not updating'); return; @@ -55,7 +58,7 @@ export class ConfigComponent { }); } - routesUpdated(routes: RouteConfiguration[] | undefined) { + routesUpdated(routes: RouteConfig[] | undefined) { if (routes === undefined) { console.error('routes is undefined, not updating'); return; diff --git a/src/app/components/module-list/module-list.component.ts b/src/app/components/module-list/module-list.component.ts index f818193..a0f2655 100644 --- a/src/app/components/module-list/module-list.component.ts +++ b/src/app/components/module-list/module-list.component.ts @@ -1,7 +1,7 @@ import { Component, inject, model } from '@angular/core'; import { ModuleComponent } from '../module/module.component'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { ModuleConfiguration } from '../../models/config.models'; +import { ModuleConfig } from '../../models/config.models'; import { SchemaService } from '../../services/schema.service'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -21,12 +21,12 @@ import { MatTooltipModule } from '@angular/material/tooltip'; styleUrl: './module-list.component.css', }) export class ModuleListComponent { - modules = model(); + modules = model(); public schemaService = inject(SchemaService); private snackBar = inject(MatSnackBar); - moduleUpdated(index: number, module: ModuleConfiguration | undefined) { + moduleUpdated(index: number, module: ModuleConfig | undefined) { if (module === undefined) { console.error('module is undefined, not updating'); return; diff --git a/src/app/components/module/module.component.html b/src/app/components/module/module.component.html index ef5f35a..6b6661a 100644 --- a/src/app/components/module/module.component.html +++ b/src/app/components/module/module.component.html @@ -4,14 +4,20 @@ isInError() ? 'border-red-500' : 'border-gray-600' }} border-solid w-full h-full" > -
-
+
+
+ keyboard_arrow_down + keyboard_arrow_up +
+
{{ schema().title || module()?.type }}
-
+
+
close
+
@@ -37,5 +43,20 @@
+
+ @if (moduleConfigErrors().length > 0) { +
+
+ error + Module Errors +
+ @for (error of moduleConfigErrors(); track error) { +
+ {{ error.error }} +
+ } +
+ } +
} diff --git a/src/app/components/module/module.component.ts b/src/app/components/module/module.component.ts index 7eb9ba8..09febd6 100644 --- a/src/app/components/module/module.component.ts +++ b/src/app/components/module/module.component.ts @@ -1,13 +1,16 @@ -import { Component, computed, inject, input, model, output } from '@angular/core'; +import { Component, computed, inject, input, model, output, signal } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { ModuleConfiguration } from '../../models/config.models'; +import { ModuleConfig } from '../../models/config.models'; import { SchemaService } from '../../services/schema.service'; import { ParamsFormComponent } from '../params-form/params-form.component'; import { JsonPipe } from '@angular/common'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { EventsService } from '../../services/events.service'; +import { tap, debounceTime } from 'rxjs'; +import { ConfigService } from '../../services/config.service'; @Component({ selector: 'app-module', @@ -25,7 +28,17 @@ import { MatTooltipModule } from '@angular/material/tooltip'; }) export class ModuleComponent { path = input(''); - module = model(); + index = computed(() => { + const path = this.path(); + if (path) { + const parts = path.split('/'); + const lastPart = parts[parts.length - 1]; + return parseInt(lastPart, 10); + } + return undefined; + }); + + module = model(); delete = output(); params = computed(() => this.module()!.params); @@ -37,13 +50,26 @@ export class ModuleComponent { : undefined; }); + moduleConfigErrors = computed(() => { + const index = this.index(); + const moduleErrors = this.configService.moduleErrors(); + if (index !== undefined) { + return moduleErrors.filter((error) => error.index === index); + } + return []; + }); + formGroup: FormGroup = new FormGroup({ id: new FormControl('', [Validators.required]), type: new FormControl(''), }); - private schemaService = inject(SchemaService); + inputIndicatorColor = signal('gray'); + outputIndicatorColor = signal('gray'); + private schemaService = inject(SchemaService); + private eventsService = inject(EventsService); + private configService = inject(ConfigService); ngOnInit(): void { this.formGroup.patchValue({ id: this.module()?.id, @@ -64,6 +90,30 @@ export class ModuleComponent { return undefined; }); }); + if (this.id() !== undefined) { + this.eventsService + .getInputEventsForSource(this.id()!) + .pipe( + tap((inputEvent) => { + this.inputIndicatorColor.set(inputEvent.error ? 'red' : 'greenyellow'); + }), + debounceTime(100), + ) + .subscribe((inputEvent) => { + this.inputIndicatorColor.set('gray'); + }); + this.eventsService + .getOutputEventsForDestination(this.id()!) + .pipe( + tap((outputEvent) => { + this.outputIndicatorColor.set(outputEvent.error ? 'red' : 'greenyellow'); + }), + debounceTime(100), + ) + .subscribe((outputEvent) => { + this.outputIndicatorColor.set('gray'); + }); + } } paramsUpdated(params: any) { diff --git a/src/app/components/processor/processor.component.html b/src/app/components/processor/processor.component.html index 533e255..3c1ce86 100644 --- a/src/app/components/processor/processor.component.html +++ b/src/app/components/processor/processor.component.html @@ -5,15 +5,16 @@ isInError() ? 'border-red-500' : 'border-gray-600' }} border-solid w-fit h-full" > -
+
{{ schema()?.title || processor()?.type }}
-
+
close
@if (hasParams()) { +
(''); - processor = model(); + processor = model(); delete = output(); params = computed(() => this.processor()!.params); diff --git a/src/app/components/route-list.component/route-list.component.ts b/src/app/components/route-list.component/route-list.component.ts index 2bc9230..6938905 100644 --- a/src/app/components/route-list.component/route-list.component.ts +++ b/src/app/components/route-list.component/route-list.component.ts @@ -1,6 +1,6 @@ import { Component, inject, input, model } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { ModuleConfiguration, RouteConfiguration } from '../../models/config.models'; +import { ModuleConfig, RouteConfig } from '../../models/config.models'; import { SchemaService } from '../../services/schema.service'; import { RouteComponent } from '../route/route.component'; import { MatButtonModule } from '@angular/material/button'; @@ -22,7 +22,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; }) export class RouteListComponent { - routes = model(); + routes = model(); moduleIds = input(); public schemaService = inject(SchemaService); @@ -41,7 +41,7 @@ export class RouteListComponent { }); } - routeUpdated(index: number, route: RouteConfiguration | undefined) { + routeUpdated(index: number, route: RouteConfig | undefined) { if (route === undefined) { console.error('route is undefined, not updating'); return; @@ -49,7 +49,6 @@ export class RouteListComponent { this.routes.update((routes) => { if (routes) { routes[index].input = route.input; - routes[index].output = route.output; return [...routes]; } return routes; diff --git a/src/app/components/route/route.component.html b/src/app/components/route/route.component.html index 95dfc65..300abc0 100644 --- a/src/app/components/route/route.component.html +++ b/src/app/components/route/route.component.html @@ -5,12 +5,16 @@ isInError() ? 'border-red-500' : 'border-gray-600' }} border-solid w-full h-full" > -
+
+
+ chevron_right +
Route {{ index() }}
close
+
@@ -69,6 +73,21 @@ }
+
+ @if (routeConfigErrors().length > 0) { +
+
+ error + Route Errors +
+ @for (error of routeConfigErrors(); track error) { +
+ {{ error.error }} +
+ } +
+ } +
diff --git a/src/app/components/route/route.component.ts b/src/app/components/route/route.component.ts index bf70e47..5a7d6c3 100644 --- a/src/app/components/route/route.component.ts +++ b/src/app/components/route/route.component.ts @@ -1,14 +1,17 @@ -import { Component, computed, inject, input, model, output } from '@angular/core'; +import { Component, computed, inject, input, model, output, signal } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { ProcessorConfiguration, RouteConfiguration } from '../../models/config.models'; +import { ProcessorConfig, RouteConfig } from '../../models/config.models'; import { SchemaService } from '../../services/schema.service'; import { JsonPipe } from '@angular/common'; import { ProcessorComponent } from '../processor/processor.component'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { EventsService } from '../../services/events.service'; +import { debounceTime, tap } from 'rxjs'; +import { ConfigService } from '../../services/config.service'; @Component({ selector: 'app-route', @@ -37,7 +40,7 @@ export class RouteComponent { return undefined; }); - route = model(); + route = model(); moduleIds = input([]); delete = output(); @@ -49,12 +52,25 @@ export class RouteComponent { return []; }); + routeConfigErrors = computed(() => { + const index = this.index(); + const routeErrors = this.configService.routeErrors(); + if (index !== undefined) { + return routeErrors.filter((error) => error.index === index); + } + return []; + }); + formGroup: FormGroup = new FormGroup({ input: new FormControl('', [Validators.required]), }); public schemaService = inject(SchemaService); private snackBar = inject(MatSnackBar); + private eventsService = inject(EventsService); + private configService = inject(ConfigService); + + indicatorColor = signal('gray'); ngOnInit(): void { this.formGroup.patchValue({ @@ -73,6 +89,17 @@ export class RouteComponent { return undefined; }); }); + + 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') + }) + } } isInError(): boolean { @@ -103,7 +130,7 @@ export class RouteComponent { }); } - processorUpdated(index: number, processor: ProcessorConfiguration | undefined) { + processorUpdated(index: number, processor: ProcessorConfig | undefined) { if (processor === undefined) { console.error('processor is undefined, not updating'); return; diff --git a/src/app/models/config.models.ts b/src/app/models/config.models.ts index 00aa1cc..4879581 100644 --- a/src/app/models/config.models.ts +++ b/src/app/models/config.models.ts @@ -1,6 +1,7 @@ export type Config = { - modules: ModuleConfiguration[]; - routes: RouteConfiguration[]; + api: ApiConfig + modules: ModuleConfig[]; + routes: RouteConfig[]; }; export type ConfigState = { @@ -9,19 +10,40 @@ export type ConfigState = { isCurrent: boolean; }; -export type ModuleConfiguration = { +export type ApiConfig = { + port: number; +}; + +export type ModuleConfig = { id?: string; type: string; params?: Record; }; -export type RouteConfiguration = { +export type RouteConfig = { input?: string; - processors?: ProcessorConfiguration[]; - output?: string; + processors?: ProcessorConfig[]; }; -export type ProcessorConfiguration = { +export type ProcessorConfig = { type: string; params?: Record; }; + + +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[]; +} \ No newline at end of file diff --git a/src/app/models/events.models.ts b/src/app/models/events.models.ts new file mode 100644 index 0000000..d276ebd --- /dev/null +++ b/src/app/models/events.models.ts @@ -0,0 +1,19 @@ +import { Config } from "./config.models" + +export type RouterEvent = { + type: T, + data?: D, + error?: string, +} + +export type RouteEventData = { + index: number +} + +export type InputEventData = { + source: string +} + +export type OutputEventData = { + destination: string +} \ No newline at end of file diff --git a/src/app/services/config.service.ts b/src/app/services/config.service.ts index 3a07af9..4ac56d1 100644 --- a/src/app/services/config.service.ts +++ b/src/app/services/config.service.ts @@ -1,7 +1,10 @@ -import { computed, effect, Injectable, signal } from '@angular/core'; -import { cloneDeep } from 'lodash-es'; -import { Config } from '../models/config.models'; +import { computed, effect, inject, Injectable, signal } from '@angular/core'; +import { cloneDeep, isEqual } from 'lodash-es'; +import { Config, ConfigError, ModuleError, RouteError } from '../models/config.models'; import { SchemaService } from './schema.service'; +import { HttpClient } from '@angular/common/http'; +import { SettingsService } from './settings.service'; +import { EventsService } from './events.service'; @Injectable({ providedIn: 'root', }) @@ -14,7 +17,23 @@ export class ConfigService { return this.schemaService.validate(config); }); currentlyShownConfig = signal(undefined); + runningConfig = signal(undefined); + configIsDirty = computed(() => { + const currentlyShown = this.currentlyShownConfig(); + const running = this.runningConfig(); + if (currentlyShown === undefined || running === undefined) { + return false; + } + return !isEqual(currentlyShown, running); + }); + + moduleErrors = signal([]); + routeErrors = signal([]); + + private http = inject(HttpClient); + private settingsService = inject(SettingsService); + constructor(private schemaService: SchemaService) { this.loadConfig(); effect(() => { @@ -23,42 +42,69 @@ export class ConfigService { } loadConfig() { - const configString = localStorage.getItem('config'); - if (configString) { - try { - const config = JSON.parse(configString); - const valid = this.schemaService.validate(config); - if (valid) { - console.log('Loaded config from local storage', config); + const configUrl = this.settingsService.configUrl(); + if (!configUrl) { + console.error('Config URL is not set'); + this.setEmptyConfig(); + return; + } + this.http.get(configUrl.toString()).subscribe({ + next: (config) => { + if (this.schemaService.validate(config)) { this.updateCurrentlyShownConfig(config); + this.runningConfig.set(cloneDeep(config)); } else { - console.error('Config in local storage is invalid', config); + console.error('Config from server is invalid', config); this.setEmptyConfig(); } - } catch (e) { - console.error('Failed to parse config from local storage', e); + }, + error: (err) => { + console.error('Failed to load config from server', err); this.setEmptyConfig(); + }, + }); + } + + uploadConfig(config: Config) { + if (this.schemaService.validate(config)) { + this.updateCurrentlyShownConfig(config); + const configUrl = this.settingsService.configUrl(); + if (!configUrl) { + console.error('Config URL is not set, cannot upload config'); + return; } + this.http.put(configUrl.toString(), config).subscribe({ + next: () => { + console.log('Config uploaded successfully'); + this.moduleErrors.set([]); + this.routeErrors.set([]); + this.runningConfig.set(cloneDeep(config)); + }, + error: (err) => { + console.error('Problems occurred while uploading config', err); + if (err.error) { + const configError: ConfigError = err.error; + this.moduleErrors.set(configError.moduleErrors ?? []); + this.routeErrors.set(configError.routeErrors ?? []); + } + this.runningConfig.set(cloneDeep(config)); + }, + }); } else { - this.setEmptyConfig(); + console.error('Uploaded config is invalid', config); } } setEmptyConfig() { console.log('Setting empty config'); this.updateCurrentlyShownConfig({ + api: { port: 8080 }, modules: [], routes: [], }); } - saveConfig(config: Config) { - localStorage.setItem('config', JSON.stringify(config)); - console.log('Config saved to local storage', config); - } - updateCurrentlyShownConfig(config: Config) { - // NOTE(jwetzell): mark the configStates that match the currently shown as such this.currentlyShownConfig.set(cloneDeep(config)); } } diff --git a/src/app/services/events.service.ts b/src/app/services/events.service.ts new file mode 100644 index 0000000..082fc69 --- /dev/null +++ b/src/app/services/events.service.ts @@ -0,0 +1,84 @@ +import { effect, inject, Injectable, signal } from '@angular/core'; +import { + InputEventData, + OutputEventData, + RouteEventData, + RouterEvent, +} from '../models/events.models'; +import { filter, Subject } from 'rxjs'; +import { SettingsService } from './settings.service'; +@Injectable({ + providedIn: 'root', +}) +export class EventsService { + status = signal('closed'); + socket?: WebSocket; + private routeEvents$ = new Subject>(); + private inputEvents$ = new Subject>(); + private outputEvents$ = new Subject>(); + private settingsService = inject(SettingsService); + + constructor() { + effect(() => { + console.log('Websocket URL changed:', this.settingsService.wsUrl()); + this.reload(); + }); + } + + reload() { + try { + this.socket = new WebSocket(this.settingsService.wsUrl()); + this.socket.onopen = () => { + this.status.set('open'); + }; + + this.socket.onclose = () => { + this.status.set('closed'); + }; + + this.socket.onerror = (error) => { + this.status.set('error'); + }; + + this.socket.onmessage = (event) => { + const messageObj = JSON.parse(event.data); + switch (messageObj.type) { + case 'route': + this.routeEvents$.next(messageObj); + break; + case 'input': + this.inputEvents$.next(messageObj); + break; + case 'output': + this.outputEvents$.next(messageObj); + break; + default: + console.warn('Unknown event type:', messageObj.type); + } + }; + } catch (error) { + console.error('Failed to connect to WebSocket:', error); + } + } + + getRouteEventsForIndex(index: number) { + return this.routeEvents$.pipe(filter((event) => event.data?.index === index)); + } + + getInputEventsForSource(source: string) { + return this.inputEvents$.pipe(filter((event) => event.data?.source === source)); + } + + getOutputEventsForDestination(destination: string) { + return this.outputEvents$.pipe(filter((event) => event.data?.destination === destination)); + } + + sendEvent(event: RouterEvent) { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + console.log('sending event:', event); + this.socket.send(JSON.stringify(event)); + } else { + console.warn('WebSocket is not open. Cannot send event:', event); + } + } +} diff --git a/src/app/services/schema.service.ts b/src/app/services/schema.service.ts index 9f1dda6..0bc2e74 100644 --- a/src/app/services/schema.service.ts +++ b/src/app/services/schema.service.ts @@ -11,19 +11,18 @@ import { import { Ajv2020, JSONSchemaType } from 'ajv/dist/2020'; import { SomeJSONSchema } from 'ajv/dist/types/json-schema'; import { sortBy } from 'lodash-es'; -import { Config, ModuleConfiguration, ProcessorConfiguration } from '../models/config.models'; +import { Config, ModuleConfig, ProcessorConfig, RouteConfig } from '../models/config.models'; import { ObjectInfo, ParamsFormInfo } from '../models/form.model'; import { SettingsService } from './settings.service'; -import { RouteConfiguration } from '../models/config.models'; @Injectable({ providedIn: 'root', }) export class SchemaService { configSchema = signal | undefined>(undefined); - modulesSchema = signal | undefined>(undefined); - routesSchema = signal | undefined>(undefined); - processorsSchema = signal | undefined>(undefined); + modulesSchema = signal | undefined>(undefined); + routesSchema = signal | undefined>(undefined); + processorsSchema = signal | undefined>(undefined); schemasLoaded = computed(() => { return ( @@ -90,7 +89,7 @@ export class SchemaService { loadModulesSchema() { this.http .get< - JSONSchemaType + JSONSchemaType >(this.settingsService.modulesSchemaUrl().toString()) .subscribe((schema) => { this.setModulesSchema(schema); @@ -99,7 +98,7 @@ export class SchemaService { loadRoutesSchema() { this.http - .get>(this.settingsService.routesSchemaUrl().toString()) + .get>(this.settingsService.routesSchemaUrl().toString()) .subscribe((schema) => { this.setRoutesSchema(schema); }); @@ -108,7 +107,7 @@ export class SchemaService { loadProcessorsSchema() { this.http .get< - JSONSchemaType + JSONSchemaType >(this.settingsService.processorsSchemaUrl().toString()) .subscribe((schema) => { this.setProcessorsSchema(schema); @@ -142,13 +141,13 @@ export class SchemaService { return true; } - getSkeletonForRoute(): RouteConfiguration { - const template: RouteConfiguration = {}; + getSkeletonForRoute(): RouteConfig { + const template: RouteConfig = {}; return template; } - getSkeletonForProcessor(processorType: string): ProcessorConfiguration { - const template: ProcessorConfiguration = { + getSkeletonForProcessor(processorType: string): ProcessorConfig { + const template: ProcessorConfig = { type: processorType, }; const itemInfo = this.processorTypes.find((itemInfo) => itemInfo.type === processorType); @@ -160,8 +159,8 @@ export class SchemaService { return template; } - getSkeletonForModule(moduleType: string): ModuleConfiguration { - const template: ModuleConfiguration = { + getSkeletonForModule(moduleType: string): ModuleConfig { + const template: ModuleConfig = { type: moduleType, }; const itemInfo = this.moduleTypes.find((itemInfo) => itemInfo.type === moduleType); @@ -198,17 +197,17 @@ export class SchemaService { this.configSchema.set(schema); } - setModulesSchema(schema: JSONSchemaType) { + setModulesSchema(schema: JSONSchemaType) { this.modulesSchema.set(schema); this.moduleTypes = []; this.populateModuleTypes(); } - setRoutesSchema(schema: JSONSchemaType) { + setRoutesSchema(schema: JSONSchemaType) { this.routesSchema.set(schema); } - setProcessorsSchema(schema: JSONSchemaType) { + setProcessorsSchema(schema: JSONSchemaType) { this.processorsSchema.set(schema); this.processorTypes = []; this.populateProcessorTypes(); diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index c3bc3c3..acd0af1 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -5,11 +5,17 @@ import { computed, Injectable, signal } from '@angular/core'; }) export class SettingsService { public isDummySite: boolean = false; - public configSchemaPath = signal('/config.schema.json'); - public modulesSchemaPath = signal('/modules.schema.json'); - public routesSchemaPath = signal('/routes.schema.json'); - public processorsSchemaPath = signal('/processors.schema.json'); - public baseUrl = signal(window.location.href); + public configPath = signal('/api/v1/config'); + public configSchemaPath = signal('/schema/config'); + public modulesSchemaPath = signal('/schema/modules'); + public routesSchemaPath = signal('/schema/routes'); + public processorsSchemaPath = signal('/schema/processors'); + public wsPath = signal('/ws'); + public baseUrl = signal('http://localhost:8080'); + + public configUrl = computed(() => { + return new URL(this.configPath(), this.baseUrl()); + }); public configSchemaUrl = computed(() => { return new URL(this.configSchemaPath(), this.baseUrl()); @@ -27,6 +33,13 @@ export class SettingsService { return new URL(this.processorsSchemaPath(), this.baseUrl()); }); + public wsUrl = computed(() => { + const url = new URL(this.baseUrl()); + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.pathname = this.wsPath(); + return url; + }); + constructor() {} updateBaseUrl(baseUrl: string) {