diff --git a/apps/servicebus-browser-frontend/src/app/app.component.scss b/apps/servicebus-browser-frontend/src/app/app.component.scss index 6b64db8..a51cc33 100644 --- a/apps/servicebus-browser-frontend/src/app/app.component.scss +++ b/apps/servicebus-browser-frontend/src/app/app.component.scss @@ -22,6 +22,11 @@ } } + .sidebar { + width: 100%; + height: 100%; + } + .main-content { width: 100%; height: 100%; diff --git a/apps/servicebus-browser-frontend/src/app/app.component.ts b/apps/servicebus-browser-frontend/src/app/app.component.ts index b34b4cb..bc5635c 100644 --- a/apps/servicebus-browser-frontend/src/app/app.component.ts +++ b/apps/servicebus-browser-frontend/src/app/app.component.ts @@ -8,6 +8,9 @@ import { Tab, TabList, Tabs } from 'primeng/tabs'; import { LogsListComponent } from '@service-bus-browser/logs-components'; import { Store } from '@ngrx/store'; import { LogsSelectors } from '@service-bus-browser/logs-store'; +import { TopologySelectors } from '@service-bus-browser/topology-store'; +import { TopologyTreeComponent } from '@service-bus-browser/topology-components'; +import { Namespace, Queue, Topic, Subscription } from '@service-bus-browser/topology-contracts'; @Component({ imports: [ @@ -20,6 +23,7 @@ import { LogsSelectors } from '@service-bus-browser/logs-store'; Tabs, TabList, Tab, + TopologyTreeComponent, ], selector: 'app-root', templateUrl: './app.component.html', @@ -29,40 +33,32 @@ export class AppComponent { title = 'servicebus-browser-frontend'; items = [ { - label: 'Home', - icon: 'pi pi-home', - }, - { - label: 'Projects', - icon: 'pi pi-search', - items: [ - { - label: 'Core', - icon: 'pi pi-bolt', - shortcut: '⌘+S', - }, - { - label: 'Blocks', - icon: 'pi pi-server', - shortcut: '⌘+B', - }, - { - separator: true, - }, - { - label: 'UI Kit', - icon: 'pi pi-pencil', - shortcut: '⌘+U', - }, - ], - }, + label: 'Connections', + } ]; store = inject(Store); logsOpened = signal(false); logs = this.store.selectSignal(LogsSelectors.selectLogs); + namespaces = this.store.selectSignal(TopologySelectors.selectNamespaces); toggleLogs() { this.logsOpened.update((value) => !value); } + + onNamespaceSelected($event: { namespace: Namespace }) { + console.log($event); + } + + onQueueSelected($event: { namespaceId: string; queue: Queue }) { + console.log($event); + } + + onTopicSelected($event: { namespaceId: string; topic: Topic }) { + console.log($event); + } + + onSubscriptionSelected($event: { namespaceId: string; topicId: string, subscription: Subscription }) { + console.log($event); + } } diff --git a/apps/servicebus-browser-frontend/src/app/app.config.ts b/apps/servicebus-browser-frontend/src/app/app.config.ts index 51d0b88..b91ae7f 100644 --- a/apps/servicebus-browser-frontend/src/app/app.config.ts +++ b/apps/servicebus-browser-frontend/src/app/app.config.ts @@ -7,6 +7,7 @@ import { theme } from './theme'; import { provideLogsState } from '@service-bus-browser/logs-store'; import { provideStore } from '@ngrx/store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; +import { provideTopologyState } from '@service-bus-browser/topology-store'; export const appConfig: ApplicationConfig = { providers: [ @@ -25,6 +26,7 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(appRoutes, withHashLocation()), provideLogsState(), + provideTopologyState(), // ngrx provideStore(), diff --git a/libs/logs/components/src/lib/log-line/log-line.component.scss b/libs/logs/components/src/lib/log-line/log-line.component.scss index 361da89..669cd08 100644 --- a/libs/logs/components/src/lib/log-line/log-line.component.scss +++ b/libs/logs/components/src/lib/log-line/log-line.component.scss @@ -1,7 +1,8 @@ pre { --color: 'inherit'; margin: 0; - font-size: 12px; + font-size: 14px; + line-height: 16px; padding: 4px 1rem; color: var(--color); } diff --git a/libs/logs/components/src/lib/logs-list/logs-list.component.html b/libs/logs/components/src/lib/logs-list/logs-list.component.html index 1888a12..00a7067 100644 --- a/libs/logs/components/src/lib/logs-list/logs-list.component.html +++ b/libs/logs/components/src/lib/logs-list/logs-list.component.html @@ -1,6 +1,6 @@ diff --git a/libs/logs/components/src/lib/logs-list/logs-list.component.scss b/libs/logs/components/src/lib/logs-list/logs-list.component.scss index e5065b5..de0a876 100644 --- a/libs/logs/components/src/lib/logs-list/logs-list.component.scss +++ b/libs/logs/components/src/lib/logs-list/logs-list.component.scss @@ -1,6 +1,5 @@ sbb-logs-log-line { display: block; - height: 20px; &.odd { background: var(--p-surface-100); diff --git a/libs/logs/store/src/lib/logs.store.ts b/libs/logs/store/src/lib/logs.store.ts index f345947..93f3036 100644 --- a/libs/logs/store/src/lib/logs.store.ts +++ b/libs/logs/store/src/lib/logs.store.ts @@ -9,7 +9,7 @@ export type LogsState = { } export const initialState: LogsState = { - logs: Array.from({ length: 1000 }, (_, index) => { + logs: Array.from({ length: 100000 }, (_, index) => { const severity = Math.random(); let severityString: LogLineSeverity = 'verbose'; if (severity > 0.2) { diff --git a/libs/topology/components/README.md b/libs/topology/components/README.md new file mode 100644 index 0000000..f306867 --- /dev/null +++ b/libs/topology/components/README.md @@ -0,0 +1,7 @@ +# @service-bus-browser/topology-components + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test @service-bus-browser/topology-components` to execute the unit tests. diff --git a/libs/topology/components/eslint.config.js b/libs/topology/components/eslint.config.js new file mode 100644 index 0000000..3a3ff72 --- /dev/null +++ b/libs/topology/components/eslint.config.js @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'sbb-tpl', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'sbb-tpl', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/topology/components/jest.config.ts b/libs/topology/components/jest.config.ts new file mode 100644 index 0000000..244d2b8 --- /dev/null +++ b/libs/topology/components/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: '@service-bus-browser/topology-components', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/topology/components', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/topology/components/project.json b/libs/topology/components/project.json new file mode 100644 index 0000000..4ba50d2 --- /dev/null +++ b/libs/topology/components/project.json @@ -0,0 +1,20 @@ +{ + "name": "@service-bus-browser/topology-components", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/topology/components/src", + "prefix": "sbb-tpl", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/topology/components/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/topology/components/src/index.ts b/libs/topology/components/src/index.ts new file mode 100644 index 0000000..0582421 --- /dev/null +++ b/libs/topology/components/src/index.ts @@ -0,0 +1 @@ +export * from './lib/topology-tree/topology-tree.component'; diff --git a/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.html b/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.html new file mode 100644 index 0000000..83d65e0 --- /dev/null +++ b/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.html @@ -0,0 +1 @@ + {{ namespace().name }} diff --git a/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.scss b/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.ts b/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.ts new file mode 100644 index 0000000..27798fb --- /dev/null +++ b/libs/topology/components/src/lib/namespace-tree-node/namespace-tree-node.component.ts @@ -0,0 +1,16 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Namespace } from '@service-bus-browser/topology-contracts'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faServer } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'sbb-tpl-namespace-tree-node', + imports: [CommonModule, FaIconComponent], + templateUrl: './namespace-tree-node.component.html', + styleUrl: './namespace-tree-node.component.scss', +}) +export class NamespaceTreeNodeComponent { + namespace = input.required(); + icon = faServer; +} diff --git a/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.html b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.html new file mode 100644 index 0000000..1d1342a --- /dev/null +++ b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.html @@ -0,0 +1,2 @@ + {{ queue().name }} +({{queue().messageCount}},{{queue().deadLetterMessageCount}},{{queue().transferDeadLetterMessageCount}}) diff --git a/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.scss b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.scss new file mode 100644 index 0000000..626f585 --- /dev/null +++ b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; +} diff --git a/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.ts b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.ts new file mode 100644 index 0000000..105ba3c --- /dev/null +++ b/libs/topology/components/src/lib/queue-tree-node/queue-tree-node.component.ts @@ -0,0 +1,16 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Queue } from '@service-bus-browser/topology-contracts'; +import { faFolder } from '@fortawesome/free-solid-svg-icons'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'sbb-tpl-queue-tree-node', + imports: [CommonModule, FaIconComponent], + templateUrl: './queue-tree-node.component.html', + styleUrl: './queue-tree-node.component.scss', +}) +export class QueueTreeNodeComponent { + queue = input.required(); + icon = faFolder; +} diff --git a/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.html b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.html new file mode 100644 index 0000000..33df356 --- /dev/null +++ b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.html @@ -0,0 +1,2 @@ + {{ subscription().name }} +({{subscription().messageCount}},{{subscription().deadLetterMessageCount}},{{subscription().transferDeadLetterMessageCount}}) diff --git a/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.scss b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.scss new file mode 100644 index 0000000..626f585 --- /dev/null +++ b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; +} diff --git a/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.ts b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.ts new file mode 100644 index 0000000..525052f --- /dev/null +++ b/libs/topology/components/src/lib/subscription-tree-node/subscription-tree-node.component.ts @@ -0,0 +1,16 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Subscription } from '@service-bus-browser/topology-contracts'; +import { faFolder } from '@fortawesome/free-regular-svg-icons'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'sbb-tpl-subscription-tree-node', + imports: [CommonModule, FaIconComponent], + templateUrl: './subscription-tree-node.component.html', + styleUrl: './subscription-tree-node.component.scss', +}) +export class SubscriptionTreeNodeComponent { + subscription = input.required(); + icon = faFolder; +} diff --git a/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.html b/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.html new file mode 100644 index 0000000..c3e3d59 --- /dev/null +++ b/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.html @@ -0,0 +1 @@ + {{ topic().name }} diff --git a/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.scss b/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.ts b/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.ts new file mode 100644 index 0000000..601623d --- /dev/null +++ b/libs/topology/components/src/lib/topic-tree-node/topic-tree-node.component.ts @@ -0,0 +1,16 @@ +import { Component, input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Topic } from '@service-bus-browser/topology-contracts'; +import { faFolderTree } from '@fortawesome/free-solid-svg-icons'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +@Component({ + selector: 'sbb-tpl-topic-tree-node', + imports: [CommonModule, FaIconComponent], + templateUrl: './topic-tree-node.component.html', + styleUrl: './topic-tree-node.component.scss', +}) +export class TopicTreeNodeComponent { + topic = input.required(); + icon = faFolderTree; +} diff --git a/libs/topology/components/src/lib/topology-tree/topology-tree.component.html b/libs/topology/components/src/lib/topology-tree/topology-tree.component.html new file mode 100644 index 0000000..44856c5 --- /dev/null +++ b/libs/topology/components/src/lib/topology-tree/topology-tree.component.html @@ -0,0 +1,17 @@ + + + {{ node.label }} + + + + + + + + + + + + + + diff --git a/libs/topology/components/src/lib/topology-tree/topology-tree.component.scss b/libs/topology/components/src/lib/topology-tree/topology-tree.component.scss new file mode 100644 index 0000000..92331e0 --- /dev/null +++ b/libs/topology/components/src/lib/topology-tree/topology-tree.component.scss @@ -0,0 +1,22 @@ +:host { + ::ng-deep .p-tree-node-label { + width: 100%; + } + + ::ng-deep .p-tree-node-leaf .p-tree-node-toggle-button { + display: none; + } + + ::ng-deep .p-tree-node-leaf { + line-height: 28px; + .p-tree-node-content { + padding: 0 0.4rem; + } + } +} + +p-tree { + --p-tree-padding: 0rem; + --p-tree-node-padding: 0; + --p-tree-indent: 28px; +} diff --git a/libs/topology/components/src/lib/topology-tree/topology-tree.component.ts b/libs/topology/components/src/lib/topology-tree/topology-tree.component.ts new file mode 100644 index 0000000..86f6276 --- /dev/null +++ b/libs/topology/components/src/lib/topology-tree/topology-tree.component.ts @@ -0,0 +1,157 @@ +import { Component, computed, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + Namespace, + NamespaceWithChildren, + Queue, + Subscription, + TopicWithChildren +} from '@service-bus-browser/topology-contracts'; +import { Tree } from 'primeng/tree'; +import { PrimeTemplate, TreeNode } from 'primeng/api'; +import { NamespaceTreeNodeComponent } from '../namespace-tree-node/namespace-tree-node.component'; +import { TopicTreeNodeComponent } from '../topic-tree-node/topic-tree-node.component'; +import { SubscriptionTreeNodeComponent } from '../subscription-tree-node/subscription-tree-node.component'; +import { QueueTreeNodeComponent } from '../queue-tree-node/queue-tree-node.component'; + +@Component({ + selector: 'sbb-tpl-topology-tree', + imports: [ + CommonModule, + Tree, + PrimeTemplate, + NamespaceTreeNodeComponent, + TopicTreeNodeComponent, + SubscriptionTreeNodeComponent, + QueueTreeNodeComponent, + ], + templateUrl: './topology-tree.component.html', + styleUrl: './topology-tree.component.scss', +}) +export class TopologyTreeComponent { + namespaces = + input.required[]>(); + nodes = computed(() => + this.namespaces().map((ns, ns_index) => ({ + key: ns_index.toString(), + label: ns.name, + type: 'namespace', + data: ns, + children: [ + { + key: `${ns_index}-queues`, + label: 'Queues', + selectable: false, + children: ns.queues.map((queue, queue_index) => ({ + key: `${ns_index}-queue-${queue_index}`, + label: queue.name, + type: 'queue', + data: { + namespace: ns, + queue, + }, + leaf: true, + })), + }, + { + key: `${ns_index}-topics`, + label: 'Topics', + selectable: false, + children: ns.topics.map((topic, topic_index) => ({ + key: `${ns_index}-topic-${topic_index}`, + label: topic.name, + type: 'topic', + data: { + namespace: ns, + topic, + }, + children: topic.subscriptions.map((sub, sub_index) => ({ + key: `${ns_index}-topic-${topic_index}-subscription-${sub_index}`, + label: sub.name, + type: 'subscription', + data: { + namespace: ns, + topic, + subscription: sub, + }, + leaf: true, + })), + + })), + }, + ], + })) + ); + + namespaceSelected = output<{ + namespace: Namespace; + }>(); + queueSelected = output<{ + namespaceId: string; + queue: Queue; + }>(); + topicSelected = output<{ + namespaceId: string; + topic: TopicWithChildren; + }>(); + subscriptionSelected = output<{ + namespaceId: string; + topicId: string; + subscription: Subscription; + }>(); + + + onSelectionChange(event: TreeNode | TreeNode[] | null) { + // should not be an array since we have selection mode single + if (!event || event instanceof Array) { + return; + } + + switch (event.type) { + case 'namespace': + this.onNamespaceSelected(event.data); + break; + case 'queue': + this.onQueueSelected(event.data.namespace, event.data.queue); + break; + case 'topic': + this.onTopicSelected(event.data.namespace, event.data.topic); + break; + case 'subscription': + this.onSubscriptionSelected( + event.data.namespace, + event.data.topic, + event.data.subscription + ); + break + } + } + + private onNamespaceSelected(namespace: Namespace) { + this.namespaceSelected.emit({ + namespace, + }) + } + + private onQueueSelected(namespace: Namespace, queue: Queue) { + this.queueSelected.emit({ + namespaceId: namespace.id, + queue, + }) + } + + private onTopicSelected(namespace: Namespace, topic: TopicWithChildren) { + this.topicSelected.emit({ + namespaceId: namespace.id, + topic, + }) + } + + private onSubscriptionSelected(namespace: Namespace, topic: TopicWithChildren, subscription: Subscription) { + this.subscriptionSelected.emit({ + namespaceId: namespace.id, + topicId: topic.id, + subscription, + }) + } +} diff --git a/libs/topology/components/src/test-setup.ts b/libs/topology/components/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/libs/topology/components/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/topology/components/tsconfig.json b/libs/topology/components/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/libs/topology/components/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/topology/components/tsconfig.lib.json b/libs/topology/components/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/libs/topology/components/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/topology/components/tsconfig.spec.json b/libs/topology/components/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/libs/topology/components/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/topology/contracts/README.md b/libs/topology/contracts/README.md new file mode 100644 index 0000000..fffb78c --- /dev/null +++ b/libs/topology/contracts/README.md @@ -0,0 +1,11 @@ +# contracts + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build contracts` to build the library. + +## Running unit tests + +Run `nx test contracts` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/topology/contracts/eslint.config.js b/libs/topology/contracts/eslint.config.js new file mode 100644 index 0000000..ee950eb --- /dev/null +++ b/libs/topology/contracts/eslint.config.js @@ -0,0 +1,19 @@ +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'], + }, + ], + }, + languageOptions: { + parser: require('jsonc-eslint-parser'), + }, + }, +]; diff --git a/libs/topology/contracts/jest.config.ts b/libs/topology/contracts/jest.config.ts new file mode 100644 index 0000000..22bca9c --- /dev/null +++ b/libs/topology/contracts/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'contracts', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/topology/contracts', +}; diff --git a/libs/topology/contracts/package.json b/libs/topology/contracts/package.json new file mode 100644 index 0000000..5d0e9c4 --- /dev/null +++ b/libs/topology/contracts/package.json @@ -0,0 +1,11 @@ +{ + "name": "@service-bus-browser/topology-contracts", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts", + "private": true +} diff --git a/libs/topology/contracts/project.json b/libs/topology/contracts/project.json new file mode 100644 index 0000000..9058f33 --- /dev/null +++ b/libs/topology/contracts/project.json @@ -0,0 +1,26 @@ +{ + "name": "contracts", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/topology/contracts/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/topology/contracts", + "main": "libs/topology/contracts/src/index.ts", + "tsConfig": "libs/topology/contracts/tsconfig.lib.json", + "assets": ["libs/topology/contracts/*.md"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/topology/contracts/jest.config.ts" + } + } + } +} diff --git a/libs/topology/contracts/src/index.ts b/libs/topology/contracts/src/index.ts new file mode 100644 index 0000000..6102413 --- /dev/null +++ b/libs/topology/contracts/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/namespace'; +export * from './lib/subscription'; +export * from './lib/topic'; +export * from './lib/queue'; diff --git a/libs/topology/contracts/src/lib/namespace.ts b/libs/topology/contracts/src/lib/namespace.ts new file mode 100644 index 0000000..f886482 --- /dev/null +++ b/libs/topology/contracts/src/lib/namespace.ts @@ -0,0 +1,15 @@ +import { Queue } from './queue'; +import { Topic } from './topic'; + +export type Namespace = { + id: string; + name: string; +} + +export type NamespaceWithChildren< + TQueue extends Queue = Queue, + TTopic extends Topic = Topic +> = Namespace & { + topics: TTopic[]; + queues: TQueue[]; +} diff --git a/libs/topology/contracts/src/lib/queue.ts b/libs/topology/contracts/src/lib/queue.ts new file mode 100644 index 0000000..d185615 --- /dev/null +++ b/libs/topology/contracts/src/lib/queue.ts @@ -0,0 +1,7 @@ +export type Queue = { + id: string; + name: string; + messageCount: number; + deadLetterMessageCount: number; + transferDeadLetterMessageCount: number; +} diff --git a/libs/topology/contracts/src/lib/subscription.ts b/libs/topology/contracts/src/lib/subscription.ts new file mode 100644 index 0000000..7dfe181 --- /dev/null +++ b/libs/topology/contracts/src/lib/subscription.ts @@ -0,0 +1,7 @@ +export type Subscription = { + id: string; + name: string; + messageCount: number; + deadLetterMessageCount: number; + transferDeadLetterMessageCount: number; +} diff --git a/libs/topology/contracts/src/lib/topic.ts b/libs/topology/contracts/src/lib/topic.ts new file mode 100644 index 0000000..b47eed0 --- /dev/null +++ b/libs/topology/contracts/src/lib/topic.ts @@ -0,0 +1,10 @@ +import { Subscription } from './subscription'; + +export type Topic = { + id: string; + name: string; +} + +export type TopicWithChildren = Topic & { + subscriptions: TSubscription[]; +} diff --git a/libs/topology/contracts/tsconfig.json b/libs/topology/contracts/tsconfig.json new file mode 100644 index 0000000..0dc79ca --- /dev/null +++ b/libs/topology/contracts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/topology/contracts/tsconfig.lib.json b/libs/topology/contracts/tsconfig.lib.json new file mode 100644 index 0000000..4befa7f --- /dev/null +++ b/libs/topology/contracts/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/topology/contracts/tsconfig.spec.json b/libs/topology/contracts/tsconfig.spec.json new file mode 100644 index 0000000..ab55b7c --- /dev/null +++ b/libs/topology/contracts/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/topology/store/README.md b/libs/topology/store/README.md new file mode 100644 index 0000000..bc35631 --- /dev/null +++ b/libs/topology/store/README.md @@ -0,0 +1,7 @@ +# @service-bus-browser/topology-store + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test @service-bus-browser/topology-store` to execute the unit tests. diff --git a/libs/topology/store/eslint.config.js b/libs/topology/store/eslint.config.js new file mode 100644 index 0000000..d36ea77 --- /dev/null +++ b/libs/topology/store/eslint.config.js @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/topology/store/jest.config.ts b/libs/topology/store/jest.config.ts new file mode 100644 index 0000000..67a57a7 --- /dev/null +++ b/libs/topology/store/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: '@service-bus-browser/topology-store', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/topology/store', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/topology/store/project.json b/libs/topology/store/project.json new file mode 100644 index 0000000..1086627 --- /dev/null +++ b/libs/topology/store/project.json @@ -0,0 +1,20 @@ +{ + "name": "@service-bus-browser/topology-store", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/topology/store/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/topology/store/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/topology/store/src/index.ts b/libs/topology/store/src/index.ts new file mode 100644 index 0000000..d6a6797 --- /dev/null +++ b/libs/topology/store/src/index.ts @@ -0,0 +1,14 @@ +import { EnvironmentProviders, Provider } from '@angular/core'; +import { provideState } from '@ngrx/store'; +import { topologyFeature } from './lib/topology.store'; + +export * as TopologySelectors from './lib/topology.selectors' + +export function provideTopologyState(): ( + | Provider + | EnvironmentProviders + )[] { + return [ + provideState(topologyFeature), + ]; +} diff --git a/libs/topology/store/src/lib/topology.selectors.ts b/libs/topology/store/src/lib/topology.selectors.ts new file mode 100644 index 0000000..e04d89e --- /dev/null +++ b/libs/topology/store/src/lib/topology.selectors.ts @@ -0,0 +1,17 @@ +import { featureKey, TopologyState } from './topology.store'; +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { NamespaceWithChildren, Queue, TopicWithChildren } from '@service-bus-browser/topology-contracts'; + +const featureSelector = createFeatureSelector(featureKey); + +export const selectNamespaces = createSelector( + featureSelector, + (state) => state.namespaces.map>((ns) => ({ + ...ns, + queues: state.queuesPerNamespace[ns.id] ?? [], + topics: (state.topicsPerNamespace[ns.id] ?? []).map((topic) => ({ + ...topic, + subscriptions: state.subscriptionsPerNamespaceAndTopic[ns.id]?.[topic.id] ?? [], + })), + })) +); diff --git a/libs/topology/store/src/lib/topology.store.ts b/libs/topology/store/src/lib/topology.store.ts new file mode 100644 index 0000000..e2e6cbf --- /dev/null +++ b/libs/topology/store/src/lib/topology.store.ts @@ -0,0 +1,59 @@ +import { createFeature, createReducer, on } from '@ngrx/store'; +import { Namespace, Queue, Subscription, Topic } from '@service-bus-browser/topology-contracts'; + +export const featureKey = 'topology'; + +export type TopologyState = { + namespaces: Namespace[]; + queuesPerNamespace: Record; + topicsPerNamespace: Record; + subscriptionsPerNamespaceAndTopic: Record>; +} + +export const initialState: TopologyState = { + namespaces: [ + { + name: 'test-namespace', + id: 'test-namespace-id', + } + ], + queuesPerNamespace: { + "test-namespace-id": [ + { + name: 'test-queue', + id: 'test-queue-id', + messageCount: 10, + deadLetterMessageCount: 0, + transferDeadLetterMessageCount: 0 + } + ] + }, + topicsPerNamespace: { + "test-namespace-id": [ + { + name: 'test-topic', + id: 'test-topic-id' + } + ] + }, + subscriptionsPerNamespaceAndTopic: { + "test-namespace-id": { + "test-topic-id": [{ + name: 'test-subscription', + id: 'test-subscription-id', + messageCount: 20, + deadLetterMessageCount: 0, + transferDeadLetterMessageCount: 0 + }] + } + } +}; + +export const logsReducer = createReducer( + initialState, +); + +export const topologyFeature = createFeature({ + name: featureKey, + reducer: logsReducer +}); diff --git a/libs/topology/store/src/test-setup.ts b/libs/topology/store/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/libs/topology/store/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/topology/store/tsconfig.json b/libs/topology/store/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/libs/topology/store/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/topology/store/tsconfig.lib.json b/libs/topology/store/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/libs/topology/store/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/topology/store/tsconfig.spec.json b/libs/topology/store/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/libs/topology/store/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/nx.json b/nx.json index 269599f..983c8a8 100644 --- a/nx.json +++ b/nx.json @@ -42,6 +42,11 @@ "codeCoverage": true } } + }, + "@nx/js:tsc": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "generators": { @@ -56,7 +61,7 @@ "unitTestRunner": "jest" }, "@nx/angular:component": { - "style": "css" + "style": "scss" } }, "defaultProject": "servicebus-browser-app", diff --git a/package-lock.json b/package-lock.json index f4313f3..1eab907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,11 @@ "@angular/platform-browser": "~19.0.0", "@angular/platform-browser-dynamic": "~19.0.0", "@angular/router": "~19.0.0", + "@fortawesome/angular-fontawesome": "^1.0.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", "@ngrx/effects": "^19.0.0", "@ngrx/store": "^19.0.0", "@ngrx/store-devtools": "^19.0.0", @@ -5016,6 +5021,76 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/angular-fontawesome": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-1.0.0.tgz", + "integrity": "sha512-EC2fYuXIuw2ld1kzJi+zysWus6OeGGfLQtbh0hW9zyyq5aBo8ZJkcJKBsVQ8E6Mg7nHyTWaXn+sdcXTPDWz+UQ==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@angular/core": "^19.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz", + "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/package.json b/package.json index a664d97..ee16f08 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,11 @@ "@angular/platform-browser": "~19.0.0", "@angular/platform-browser-dynamic": "~19.0.0", "@angular/router": "~19.0.0", + "@fortawesome/angular-fontawesome": "^1.0.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", "@ngrx/effects": "^19.0.0", "@ngrx/store": "^19.0.0", "@ngrx/store-devtools": "^19.0.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index da02b6d..a538fe7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,7 +21,16 @@ "@service-bus-browser/logs-contracts": [ "libs/logs/contract/src/index.ts" ], - "@service-bus-browser/logs-store": ["libs/logs/store/src/index.ts"] + "@service-bus-browser/logs-store": ["libs/logs/store/src/index.ts"], + "@service-bus-browser/topology-components": [ + "libs/topology/components/src/index.ts" + ], + "@service-bus-browser/topology-contracts": [ + "libs/topology/contracts/src/index.ts" + ], + "@service-bus-browser/topology-store": [ + "libs/topology/store/src/index.ts" + ] } }, "exclude": ["node_modules", "tmp"]