diff --git a/ScriptBeeClient/federation.config.js b/ScriptBeeClient/federation.config.js
index 1b20b291..82749737 100644
--- a/ScriptBeeClient/federation.config.js
+++ b/ScriptBeeClient/federation.config.js
@@ -4,7 +4,7 @@ module.exports = withNativeFederation({
name: 'ScriptBeeUI',
shared: {
- ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
+ ...shareAll({ singleton: true, strictVersion: false, requiredVersion: 'auto' }),
},
skip: ['@angular-architects/native-federation', 'es-module-shims', 'monaco-editor', 'rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
diff --git a/ScriptBeeClient/package-lock.json b/ScriptBeeClient/package-lock.json
index 19167b1b..dbb774db 100644
--- a/ScriptBeeClient/package-lock.json
+++ b/ScriptBeeClient/package-lock.json
@@ -22,7 +22,6 @@
"@microsoft/signalr": "^10.0.0",
"angular-split": "^20.0.0",
"cytoscape": "^3.34.0",
- "es-module-shims": "^2.8.1",
"monaco-editor": "^0.55.1",
"ngx-monaco-editor-v2": "^21.1.4",
"rxjs": "~7.8.2",
@@ -34,9 +33,11 @@
"@angular/build": "^21.2.10",
"@angular/cli": "^21.2.10",
"@angular/compiler-cli": "^21.2.12",
+ "@eslint/js": "^10.0.1",
"@vitest/browser-playwright": "^4.1.9",
"angular-eslint": "21.3.1",
- "eslint": "^10.5.0",
+ "es-module-shims": "^2.8.1",
+ "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-unused-imports": "^4.4.1",
@@ -44,7 +45,7 @@
"prettier": "^3.8.4",
"prettier-eslint": "^17.1.1",
"typescript": "~5.9.3",
- "typescript-eslint": "8.61.1",
+ "typescript-eslint": "8.56.1",
"vitest": "^4.1.9"
}
},
@@ -4389,6 +4390,27 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@eslint/object-schema": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
@@ -8171,34 +8193,6 @@
"@types/node": "*"
}
},
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.61.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
- "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
- "dev": true,
- "dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.61.1",
- "@typescript-eslint/type-utils": "8.61.1",
- "@typescript-eslint/utils": "8.61.1",
- "@typescript-eslint/visitor-keys": "8.61.1",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.61.1",
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
"node_modules/@typescript-eslint/parser": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
@@ -8277,30 +8271,6 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.61.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
- "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "8.61.1",
- "@typescript-eslint/typescript-estree": "8.61.1",
- "@typescript-eslint/utils": "8.61.1",
- "debug": "^4.4.3",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
"node_modules/@typescript-eslint/types": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
@@ -10402,7 +10372,9 @@
"node_modules/es-module-shims": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-2.8.1.tgz",
- "integrity": "sha512-Ztg5TxFCT/ffzmSf/ginHIvSbsneH60X9tpaMBD/jWXHQZT1JJNj8tOO3mRmUT3ar7Qn3YK4EbM1SjJpN5dPBA=="
+ "integrity": "sha512-Ztg5TxFCT/ffzmSf/ginHIvSbsneH60X9tpaMBD/jWXHQZT1JJNj8tOO3mRmUT3ar7Qn3YK4EbM1SjJpN5dPBA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
@@ -16356,15 +16328,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.61.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
- "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
+ "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.61.1",
- "@typescript-eslint/parser": "8.61.1",
- "@typescript-eslint/typescript-estree": "8.61.1",
- "@typescript-eslint/utils": "8.61.1"
+ "@typescript-eslint/eslint-plugin": "8.56.1",
+ "@typescript-eslint/parser": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -16375,7 +16348,240 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
+ "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/type-utils": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.56.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
+ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
+ "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.56.1",
+ "@typescript-eslint/types": "^8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
+ "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
+ "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
+ "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
+ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
+ "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.56.1",
+ "@typescript-eslint/tsconfig-utils": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
+ "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
+ "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/undici": {
diff --git a/ScriptBeeClient/package.json b/ScriptBeeClient/package.json
index 86889ff1..16bf0d54 100644
--- a/ScriptBeeClient/package.json
+++ b/ScriptBeeClient/package.json
@@ -29,7 +29,6 @@
"@microsoft/signalr": "^10.0.0",
"angular-split": "^20.0.0",
"cytoscape": "^3.34.0",
- "es-module-shims": "^2.8.1",
"monaco-editor": "^0.55.1",
"ngx-monaco-editor-v2": "^21.1.4",
"rxjs": "~7.8.2",
@@ -41,9 +40,11 @@
"@angular/build": "^21.2.10",
"@angular/cli": "^21.2.10",
"@angular/compiler-cli": "^21.2.12",
+ "@eslint/js": "^10.0.1",
"@vitest/browser-playwright": "^4.1.9",
"angular-eslint": "21.3.1",
- "eslint": "^10.5.0",
+ "es-module-shims": "^2.8.1",
+ "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-unused-imports": "^4.4.1",
@@ -51,7 +52,7 @@
"prettier": "^3.8.4",
"prettier-eslint": "^17.1.1",
"typescript": "~5.9.3",
- "typescript-eslint": "8.61.1",
+ "typescript-eslint": "8.56.1",
"vitest": "^4.1.9"
}
}
diff --git a/ScriptBeeClient/src/app/app.component.html b/ScriptBeeClient/src/app/app.component.html
index effc0e62..61039bcb 100644
--- a/ScriptBeeClient/src/app/app.component.html
+++ b/ScriptBeeClient/src/app/app.component.html
@@ -13,6 +13,10 @@
+ @for (element of navBarsElements(); track element.path) {
+
+ }
+
@if (projectState.currentProjectId()) {
diff --git a/ScriptBeeClient/src/app/app.component.ts b/ScriptBeeClient/src/app/app.component.ts
index 5e9512e1..1819372f 100644
--- a/ScriptBeeClient/src/app/app.component.ts
+++ b/ScriptBeeClient/src/app/app.component.ts
@@ -10,6 +10,7 @@ import { MatToolbar } from '@angular/material/toolbar';
import { ProjectStateService } from './services/projects/project-state.service';
import { ProjectSideNavListComponent } from './components/navigation/project-side-nav-list/project-side-nav-list.component';
import { ThemeService } from './services/common/theme.service';
+import { GatewayPluginsService } from './services/plugin/gateway-plugins.service';
@Component({
selector: 'app-root',
@@ -31,9 +32,13 @@ import { ThemeService } from './services/common/theme.service';
export class AppComponent {
private matIconRegistry = inject(MatIconRegistry);
private domSanitizer = inject(DomSanitizer);
+ private gatewayPluginsService = inject(GatewayPluginsService);
+
themeService = inject(ThemeService);
projectState = inject(ProjectStateService);
+ navBarsElements = this.gatewayPluginsService.topNavigationBarOutlets;
+
isMenuOpen = signal(localStorage.getItem('isMenuOpen') === 'true');
shouldDisplayMenuHamburger = computed(() => {
diff --git a/ScriptBeeClient/src/app/app.config.ts b/ScriptBeeClient/src/app/app.config.ts
index 1d788317..b5898faa 100644
--- a/ScriptBeeClient/src/app/app.config.ts
+++ b/ScriptBeeClient/src/app/app.config.ts
@@ -1,13 +1,15 @@
-import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
+import { ApplicationConfig, inject, provideAppInitializer, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes, withErrorNavigation } from './app.routes';
import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http';
import { provideMonacoEditor } from 'ngx-monaco-editor-v2';
import { clientIdInterceptor } from './utils/client-id.interceptor';
+import { GatewayPluginsService } from './services/plugin/gateway-plugins.service';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
+ provideAppInitializer(() => inject(GatewayPluginsService).fetchUIPlugins()),
provideRouter(routes, withComponentInputBinding(), withErrorNavigation),
provideHttpClient(
withInterceptors([clientIdInterceptor]),
diff --git a/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.html b/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.html
index 976c0cdf..9ac9ba96 100644
--- a/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.html
+++ b/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.html
@@ -1 +1 @@
-
+
diff --git a/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.ts b/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.ts
index f7054402..aae8a9aa 100644
--- a/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.ts
+++ b/ScriptBeeClient/src/app/components/navigation/project-side-nav-list/project-side-nav-list.component.ts
@@ -1,6 +1,7 @@
-import { Component, computed, input } from '@angular/core';
+import { Component, computed, inject, input } from '@angular/core';
import { SideNavListComponent } from '../side-nav-list/side-nav-list.component';
import { NavItem } from '../navItem';
+import { GatewayPluginsService } from '../../../services/plugin/gateway-plugins.service';
@Component({
selector: 'app-project-side-nav-list',
@@ -12,47 +13,56 @@ export class ProjectSideNavListComponent {
isCollapsed = input.required();
projectId = input.required();
- linkPrefix = computed(() => {
- return `/projects/${this.projectId()}`;
- });
+ linkPrefix = computed(() => `/projects/${this.projectId()}`);
+ navItems = computed(() => [...defaultNavItems, ...this.pluginNavItems()]);
- navItems: NavItem[] = [
- {
- link: '/model',
- name: 'Model',
- icon: 'model_training',
- children: [
- {
- link: '/upload',
- name: 'Upload',
- icon: 'upload',
- },
- {
- link: '/load',
- name: 'Load',
- icon: 'autorenew',
- },
- {
- link: '/context',
- name: 'Context',
- icon: 'account_tree',
- },
- ],
- },
- {
- link: '/analysis',
- name: 'Analysis',
- icon: 'query_stats',
- },
- {
- link: '/settings',
- name: 'Settings',
- icon: 'settings',
- },
- {
- link: '/plugins',
- name: 'Plugins',
- icon: 'extension',
- },
- ];
+ private gatewayPluginsService = inject(GatewayPluginsService);
+
+ private pluginNavItems = computed(() =>
+ this.gatewayPluginsService.sidePanelOutlets().map((outlet) => ({
+ link: outlet.path,
+ name: outlet.label,
+ icon: outlet.icon,
+ }))
+ );
}
+
+const defaultNavItems: NavItem[] = [
+ {
+ link: '/model',
+ name: 'Model',
+ icon: 'model_training',
+ children: [
+ {
+ link: '/upload',
+ name: 'Upload',
+ icon: 'upload',
+ },
+ {
+ link: '/load',
+ name: 'Load',
+ icon: 'autorenew',
+ },
+ {
+ link: '/context',
+ name: 'Context',
+ icon: 'account_tree',
+ },
+ ],
+ },
+ {
+ link: '/analysis',
+ name: 'Analysis',
+ icon: 'query_stats',
+ },
+ {
+ link: '/settings',
+ name: 'Settings',
+ icon: 'settings',
+ },
+ {
+ link: '/plugins',
+ name: 'Plugins',
+ icon: 'extension',
+ },
+];
diff --git a/ScriptBeeClient/src/app/services/plugin/gateway-plugins.service.ts b/ScriptBeeClient/src/app/services/plugin/gateway-plugins.service.ts
new file mode 100644
index 00000000..6b982958
--- /dev/null
+++ b/ScriptBeeClient/src/app/services/plugin/gateway-plugins.service.ts
@@ -0,0 +1,97 @@
+import { computed, inject, Injectable, signal } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { catchError, map, Observable } from 'rxjs';
+import {
+ Plugin,
+ RoutingExtensionPointOutlet,
+ SidePanelExtensionPointOutlet,
+ TopNavigationBarExtensionPointOutlet,
+ UIExtensionPointOutlet,
+} from '../../types/plugin';
+import { WebResponse } from '../../types/web-response';
+import { Router, Routes } from '@angular/router';
+import { loadRemoteModule } from '@angular-architects/native-federation';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class GatewayPluginsService {
+ private http = inject(HttpClient);
+ private router = inject(Router);
+
+ private uiPlugins = signal([]);
+
+ public topNavigationBarOutlets = computed(() => this.getOutlets(this.uiPlugins(), 'top-navigation-bar'));
+ public sidePanelOutlets = computed(() => this.getOutlets(this.uiPlugins(), 'side-panel'));
+
+ public fetchUIPlugins(): Observable {
+ return this.http.get>('/api/plugins/gateway', { params: { kind: 'UI' } }).pipe(
+ map((response) => {
+ const plugins = response.data;
+ this.uiPlugins.set(plugins);
+ this.updateRoutes(plugins);
+ }),
+ catchError((err) => {
+ console.log('Failed to fetch UI plugins', err);
+ return [];
+ })
+ );
+ }
+ private updateRoutes(plugins: Plugin[]) {
+ const topBarRoutes = this.buildRoutesFromPlugins(plugins, 'top-navigation-bar');
+ const sidePanelRoutes = this.buildRoutesFromPlugins(plugins, 'side-panel');
+
+ const updatedProjectRoutesWithPluginSidePanelRoutes = this.router.config.map((route) => {
+ if (route.path === 'projects/:id') {
+ return {
+ ...route,
+ children: [...sidePanelRoutes, ...(route.children ?? [])],
+ };
+ }
+ return route;
+ });
+
+ this.router.resetConfig([...topBarRoutes, ...updatedProjectRoutesWithPluginSidePanelRoutes]);
+ }
+
+ private buildRoutesFromPlugins(plugins: Plugin[], type: UIExtensionPointOutlet['type']): Routes {
+ return plugins.flatMap((plugin) =>
+ plugin.extensionPoints
+ .filter((ep) => ep.kind === 'UI')
+ .flatMap((extensionPoint) => {
+ const pluginRoutes: Routes = extensionPoint.outlets
+ .filter((outlet) => outlet.type === type)
+ .map((outlet) => {
+ return this.buildRouteFromPlugin(extensionPoint.remoteName, outlet);
+ });
+
+ return pluginRoutes;
+ })
+ );
+ }
+
+ private buildRouteFromPlugin(remoteName: string, outlet: RoutingExtensionPointOutlet) {
+ if (outlet.nested) {
+ return {
+ path: outlet.path.replace(/^\//, ''),
+ loadChildren: () =>
+ loadRemoteModule(remoteName, outlet.exposedModule).then((m) => {
+ const exposedRouteName = outlet.exposedModule.replace(/^\.\//, '');
+ return m[exposedRouteName];
+ }),
+ };
+ }
+ return {
+ path: outlet.path.replace(/^\//, ''),
+ loadComponent: () => loadRemoteModule(remoteName, outlet.exposedModule).then((m) => m[outlet.componentName ?? 'App']),
+ };
+ }
+
+ private getOutlets(plugins: Plugin[], type: T): Extract[] {
+ return plugins
+ .flatMap((plugin) => plugin.extensionPoints)
+ .filter((extensionPoint) => extensionPoint.kind === 'UI')
+ .flatMap((extensionPoint) => extensionPoint.outlets)
+ .filter((outlet): outlet is Extract => outlet.type === type);
+ }
+}
diff --git a/ScriptBeeClient/src/app/types/plugin.ts b/ScriptBeeClient/src/app/types/plugin.ts
index 329b7ccc..ee379583 100644
--- a/ScriptBeeClient/src/app/types/plugin.ts
+++ b/ScriptBeeClient/src/app/types/plugin.ts
@@ -8,6 +8,14 @@ export interface Plugin {
extensionPoints: ExtensionPoint[];
}
+type ExtensionPoint =
+ | HelperFunctionsExtensionPoint
+ | ScriptGeneratorExtensionPoint
+ | ScriptRunnerExtensionPoint
+ | LoaderExtensionPoint
+ | LinkerExtensionPoint
+ | UIExtensionPoint;
+
interface BasicExtensionPoint {
kind: PluginKind;
entryPoint: string;
@@ -39,17 +47,29 @@ interface LinkerExtensionPoint extends BasicExtensionPoint {
interface UIExtensionPoint extends BasicExtensionPoint {
kind: 'UI';
- port: number;
+ remoteName: string;
remoteEntry: string;
+ outlets: UIExtensionPointOutlet[];
+}
+
+export type UIExtensionPointOutlet = TopNavigationBarExtensionPointOutlet | SidePanelExtensionPointOutlet;
+
+interface BaseUIExtensionPointOutlet {
+ type: string;
+}
+export interface RoutingExtensionPointOutlet extends BaseUIExtensionPointOutlet {
exposedModule: string;
- componentName: string;
- uiPluginType: string;
+ label: string;
+ path: string;
+ nested: boolean | undefined;
+ componentName: string | undefined;
}
-type ExtensionPoint =
- | HelperFunctionsExtensionPoint
- | ScriptGeneratorExtensionPoint
- | ScriptRunnerExtensionPoint
- | LoaderExtensionPoint
- | LinkerExtensionPoint
- | UIExtensionPoint;
+export interface TopNavigationBarExtensionPointOutlet extends RoutingExtensionPointOutlet {
+ type: 'top-navigation-bar';
+}
+
+export interface SidePanelExtensionPointOutlet extends RoutingExtensionPointOutlet {
+ type: 'side-panel';
+ icon: string;
+}