From d25097ef4069f6405786cb01801ee72492d01003 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 3 Nov 2024 16:37:06 +0100 Subject: [PATCH] feat(ui): show group in ui settings --- build.gradle | 12 +- .../grouping/AsyncApiGroupService.java | 2 +- .../asyncapi/grouping/GroupingService.java | 8 +- .../SpringwolfWebConfiguration.java | 9 + .../core/controller/UiConfigController.java | 33 ++++ .../examples/kafka/ApiIntegrationTest.java | 27 ++- .../src/test/resources/groups/vehicles.json | 157 ++++++++++++++++++ .../src/test/resources/ui-config.json | 1 + springwolf-ui/src/app/app.component.ts | 3 +- springwolf-ui/src/app/app.module.ts | 4 +- .../channel-operation.component.spec.ts | 3 + .../channel-operation.component.ts | 8 +- .../channels/channels.component.spec.ts | 10 +- .../components/channels/channels.component.ts | 6 +- .../components/header/header.component.html | 23 ++- .../header/header.component.spec.ts | 33 +++- .../app/components/header/header.component.ts | 36 +++- .../asyncapi/asyncapi-mapper.service.spec.ts | 12 +- .../app/service/asyncapi/asyncapi.service.ts | 17 +- .../app/service/asyncapi/models/ui.model.ts | 6 + .../src/app/service/endpoint.service.ts | 5 + .../src/app/service/mock/example-data.ts | 32 ++-- .../app/service/mock/mock-asset.service.ts | 3 - .../app/service/mock/mock-asyncapi.service.ts | 2 +- .../src/app/service/mock/mock-server.ts | 34 +++- .../src/app/service/mock/mock-ui.service.ts | 17 ++ springwolf-ui/src/app/service/ui.service.ts | 48 +++++- 27 files changed, 473 insertions(+), 78 deletions(-) create mode 100644 springwolf-core/src/main/java/io/github/springwolf/core/controller/UiConfigController.java create mode 100644 springwolf-examples/springwolf-kafka-example/src/test/resources/groups/vehicles.json create mode 100644 springwolf-examples/springwolf-kafka-example/src/test/resources/ui-config.json create mode 100644 springwolf-ui/src/app/service/asyncapi/models/ui.model.ts create mode 100644 springwolf-ui/src/app/service/mock/mock-ui.service.ts diff --git a/build.gradle b/build.gradle index 38e417271..b6dcf2c0a 100644 --- a/build.gradle +++ b/build.gradle @@ -153,12 +153,12 @@ allprojects { doLast { // ensure that the code is not executed as part of a gradle refresh plugins.forEach { - project - .file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.json') - .renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.json') - project - .file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.yaml') - .renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.yaml') + project.fileTree(dir: project.projectDir, include: '**/src/test/resources/**/*.actual.json').forEach { file -> + file.renameTo(file.path.replace('.actual.json', '.json')) + } + project.fileTree(dir: project.projectDir, include: '**/src/test/resources/**/*.actual.yaml').forEach { file -> + file.renameTo(file.path.replace('.actual.yaml', '.yaml')) + } } } } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/AsyncApiGroupService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/AsyncApiGroupService.java index 0dc513c22..856214cba 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/AsyncApiGroupService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/AsyncApiGroupService.java @@ -26,7 +26,7 @@ public Map group(AsyncAPI asyncAPI) { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private Stream getAsyncApiGroups() { + public Stream getAsyncApiGroups() { return springwolfConfigProperties.getDocket().getGroupConfigs().stream() .map(AsyncApiGroupService::toGroupConfigAndValidate); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/GroupingService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/GroupingService.java index 00fbe67d2..bfb861297 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/GroupingService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/GroupingService.java @@ -62,8 +62,7 @@ private void markChannels(AsyncAPI fullAsyncApi, AsyncApiGroup asyncApiGroup, Ma markOperationsInChannel(fullAsyncApi, markingContext, channel); - markingContext.markedComponentMessageIds.addAll( - channel.getMessages().keySet()); + channel.getMessages().keySet().forEach(markingContext.markedComponentMessageIds::add); }); } @@ -93,11 +92,10 @@ private void markOperations(AsyncAPI fullAsyncApi, AsyncApiGroup asyncApiGroup, markChannelsForOperation(fullAsyncApi, markingContext, operationEntry.getValue()); - Set messageIds = operationEntry.getValue().getMessages().stream() + operationEntry.getValue().getMessages().stream() .map(MessageReference::getRef) .map(ReferenceUtil::getLastSegment) - .collect(Collectors.toSet()); - markingContext.markedComponentMessageIds.addAll(messageIds); + .forEach(markingContext.markedComponentMessageIds::add); }); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/configuration/SpringwolfWebConfiguration.java b/springwolf-core/src/main/java/io/github/springwolf/core/configuration/SpringwolfWebConfiguration.java index 649f0770e..817e2ca85 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/configuration/SpringwolfWebConfiguration.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/configuration/SpringwolfWebConfiguration.java @@ -6,9 +6,11 @@ import io.github.springwolf.asyncapi.v3.jackson.DefaultAsyncApiSerializerService; import io.github.springwolf.core.asyncapi.AsyncApiService; import io.github.springwolf.core.asyncapi.components.ComponentsService; +import io.github.springwolf.core.asyncapi.grouping.AsyncApiGroupService; import io.github.springwolf.core.controller.ActuatorAsyncApiController; import io.github.springwolf.core.controller.AsyncApiController; import io.github.springwolf.core.controller.PublishingPayloadCreator; +import io.github.springwolf.core.controller.UiConfigController; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -47,6 +49,13 @@ public ActuatorAsyncApiController actuatorAsyncApiController( return new ActuatorAsyncApiController(asyncApiService, asyncApiSerializerService); } + @Bean + @ConditionalOnProperty(name = SPRINGWOLF_ENDPOINT_ACTUATOR_ENABLED, havingValue = "false", matchIfMissing = true) + @ConditionalOnMissingBean + public UiConfigController uiConfigController(AsyncApiGroupService asyncApiGroupService) { + return new UiConfigController(asyncApiGroupService); + } + @Bean @ConditionalOnMissingBean public AsyncApiSerializerService asyncApiSerializerService() { diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/controller/UiConfigController.java b/springwolf-core/src/main/java/io/github/springwolf/core/controller/UiConfigController.java new file mode 100644 index 000000000..cb0eda456 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/springwolf/core/controller/UiConfigController.java @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.core.controller; + +import io.github.springwolf.core.asyncapi.grouping.AsyncApiGroupService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class UiConfigController { + + private final AsyncApiGroupService asyncApiGroupService; + + @GetMapping( + path = {"${springwolf.path.base:/springwolf}/ui-config"}, + produces = MediaType.APPLICATION_JSON_VALUE) + public UiConfig getUiConfig() { + return new UiConfig(asyncApiGroupService + .getAsyncApiGroups() + .map(el -> new UiConfig.UiConfigGroup(el.getGroupName())) + .toList()); + } + + private record UiConfig(List groups) { + private record UiConfigGroup(String name) {} + } +} diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiIntegrationTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiIntegrationTest.java index 03568697b..20235b427 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiIntegrationTest.java @@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.ResponseEntity; import java.io.IOException; import java.io.InputStream; @@ -56,12 +55,28 @@ void asyncApiResourceArtifactYamlTest() throws IOException { } @Test - void asyncApiResourceForGroupArtifactTest() { - // when + void asyncApiResourceForVehicleGroupArtifactTest() throws IOException { String url = "/springwolf/docs/Only Vehicles"; - ResponseEntity actual = restTemplate.getForEntity(url, String.class); + String actual = restTemplate.getForObject(url, String.class); + // When running with EmbeddedKafka, the kafka bootstrap server does run on random ports + String actualPatched = actual.replace(bootstrapServers, "kafka:29092").trim(); + Files.writeString(Path.of("src", "test", "resources", "groups", "vehicles.actual.json"), actualPatched); + + InputStream s = this.getClass().getResourceAsStream("/groups/vehicles.json"); + String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).trim(); + + assertEquals(expected, actualPatched); + } + + @Test + void uiConfigTest() throws IOException { + String url = "/springwolf/ui-config"; + String actual = restTemplate.getForObject(url, String.class); + Files.writeString(Path.of("src", "test", "resources", "ui-config.actual.json"), actual); + + InputStream s = this.getClass().getResourceAsStream("/ui-config.json"); + String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).trim(); - // then - assertEquals(200, actual.getStatusCode().value()); + assertEquals(expected, actual); } } diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/groups/vehicles.json b/springwolf-examples/springwolf-kafka-example/src/test/resources/groups/vehicles.json new file mode 100644 index 000000000..848db8665 --- /dev/null +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/groups/vehicles.json @@ -0,0 +1,157 @@ +{ + "asyncapi": "3.0.0", + "info": { + "title": "Springwolf example project - Kafka", + "version": "1.0.0", + "description": "Springwolf example project to demonstrate springwolfs abilities, including **markdown** support for descriptions.", + "contact": { + "name": "springwolf", + "url": "https://github.com/springwolf/springwolf-core", + "email": "example@example.com" + }, + "license": { + "name": "Apache License 2.0" + }, + "x-generator": "springwolf" + }, + "defaultContentType": "application/json", + "servers": { + "kafka-server": { + "host": "kafka:29092", + "protocol": "kafka" + } + }, + "channels": { + "vehicle-topic": { + "address": "vehicle-topic", + "messages": { + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase": { + "$ref": "#/components/messages/io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + } + }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + } + } + }, + "components": { + "schemas": { + "SpringKafkaDefaultHeaders-VehicleBase": { + "title": "SpringKafkaDefaultHeaders-VehicleBase", + "type": "object", + "properties": { + "__TypeId__": { + "type": "string", + "description": "Spring Type Id Header", + "enum": [ + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + ], + "examples": [ + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + ] + } + }, + "examples": [ + { + "__TypeId__": "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + } + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + ], + "type": "string" + } + }, + "title": "SpringKafkaDefaultHeaders-VehicleBase", + "type": "object" + } + }, + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase": { + "discriminator": "vehicleType", + "title": "VehicleBase", + "type": "object", + "properties": { + "powerSource": { + "type": "string" + }, + "topSpeed": { + "type": "integer", + "format": "int32" + }, + "vehicleType": { + "type": "string" + } + }, + "description": "Demonstrates the use of discriminator for polymorphic deserialization (not publishable)", + "examples": [ + { + "powerSource": "string", + "topSpeed": 0, + "vehicleType": "string" + } + ], + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Demonstrates the use of discriminator for polymorphic deserialization (not publishable)", + "properties": { + "powerSource": { + "type": "string" + }, + "topSpeed": { + "format": "int32", + "type": "integer" + }, + "vehicleType": { } + }, + "title": "VehicleBase", + "type": "object" + } + } + }, + "messages": { + "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase": { + "headers": { + "$ref": "#/components/schemas/SpringKafkaDefaultHeaders-VehicleBase" + }, + "payload": { + "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", + "schema": { + "$ref": "#/components/schemas/io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + } + }, + "name": "io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase", + "title": "VehicleBase", + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + } + } + } + }, + "operations": { + "vehicle-topic_receive_receiveExamplePayload": { + "action": "receive", + "channel": { + "$ref": "#/channels/vehicle-topic" + }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + }, + "messages": [ + { + "$ref": "#/channels/vehicle-topic/messages/io.github.springwolf.examples.kafka.dtos.discriminator.VehicleBase" + } + ] + } + } +} \ No newline at end of file diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/ui-config.json b/springwolf-examples/springwolf-kafka-example/src/test/resources/ui-config.json new file mode 100644 index 000000000..e44f969cf --- /dev/null +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/ui-config.json @@ -0,0 +1 @@ +{"groups":[{"name":"Only Vehicles"}]} \ No newline at end of file diff --git a/springwolf-ui/src/app/app.component.ts b/springwolf-ui/src/app/app.component.ts index dfc2c28fe..19663f8dc 100644 --- a/springwolf-ui/src/app/app.component.ts +++ b/springwolf-ui/src/app/app.component.ts @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { Component, OnInit } from "@angular/core"; -import { UiService } from "./service/ui.service"; +import { Component } from "@angular/core"; @Component({ selector: "app-root", diff --git a/springwolf-ui/src/app/app.module.ts b/springwolf-ui/src/app/app.module.ts index 190022796..7bb7b291a 100644 --- a/springwolf-ui/src/app/app.module.ts +++ b/springwolf-ui/src/app/app.module.ts @@ -23,7 +23,7 @@ import { FormsModule } from "@angular/forms"; import { JsonComponent } from "./components/json/json.component"; import { AsyncApiMapperService } from "./service/asyncapi/asyncapi-mapper.service"; import { MarkdownModule, provideMarkdown } from "ngx-markdown"; -import { UiService } from "./service/ui.service"; +import { IUiService, UiService } from "./service/ui.service"; import { provideAnimationsAsync } from "@angular/platform-browser/animations/async"; import { SidenavComponent } from "./components/sidenav/sidenav.component"; import { NavigationTargetDirective } from "./components/sidenav/navigation.directive"; @@ -80,7 +80,7 @@ export const providers = [ AsyncApiMapperService, { provide: INotificationService, useClass: NotificationService }, PublisherService, - UiService, + { provide: IUiService, useClass: UiService }, ]; export const ngModule = { diff --git a/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.spec.ts b/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.spec.ts index 24d1a6266..160fce253 100644 --- a/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.spec.ts +++ b/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.spec.ts @@ -9,11 +9,13 @@ import { mockedAsyncApiService, mockedExampleSchemaMapped, } from "../../../service/mock/mock-asyncapi.service"; +import { mockedUiService } from "../../../service/mock/mock-ui.service"; import { MockAppJson, MockAppSchemaNewComponent, MockPrismEditorComponent, } from "../../mock-components.spec"; +import { IUiService } from "../../../service/ui.service"; describe("ChannelOperationComponent", () => { const mockData = mockedExampleSchemaMapped.channelOperations @@ -31,6 +33,7 @@ describe("ChannelOperationComponent", () => { ], imports: [MaterialModule, MarkdownModule.forRoot()], providers: [ + { provide: IUiService, useValue: mockedUiService }, { provide: AsyncApiService, useValue: mockedAsyncApiService }, { provide: PublisherService, useValue: {} }, ], diff --git a/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.ts b/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.ts index d20193e6d..8a94fb6a1 100644 --- a/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.ts +++ b/springwolf-ui/src/app/components/channels/channel-main/channel-operation.component.ts @@ -14,7 +14,7 @@ import { initSchema, noExample, } from "../../../service/mock/init-values"; -import { UiService } from "../../../service/ui.service"; +import { IUiService } from "../../../service/ui.service"; @Component({ selector: "app-channel-operation", @@ -39,14 +39,14 @@ export class ChannelOperationComponent implements OnInit { operationBindingExampleString?: string; messageBindingExampleString?: string; - isShowBindings: boolean = UiService.DEFAULT_SHOW_BINDINGS; - isShowHeaders: boolean = UiService.DEFAULT_SHOW_HEADERS; + isShowBindings: boolean = IUiService.DEFAULT_SHOW_BINDINGS; + isShowHeaders: boolean = IUiService.DEFAULT_SHOW_HEADERS; canPublish: boolean = false; constructor( private asyncApiService: AsyncApiService, private publisherService: PublisherService, - private uiService: UiService, + private uiService: IUiService, private snackBar: MatSnackBar ) {} diff --git a/springwolf-ui/src/app/components/channels/channels.component.spec.ts b/springwolf-ui/src/app/components/channels/channels.component.spec.ts index 6e2a8343f..a9e4999c5 100644 --- a/springwolf-ui/src/app/components/channels/channels.component.spec.ts +++ b/springwolf-ui/src/app/components/channels/channels.component.spec.ts @@ -6,8 +6,13 @@ import { mockedAsyncApiService, mockedExampleSchemaMapped, } from "../../service/mock/mock-asyncapi.service"; +import { mockedUiService } from "../../service/mock/mock-ui.service"; import { MaterialModule } from "../../material.module"; -import { MockChannelOperationComponent } from "../mock-components.spec"; +import { + MockChannelOperationComponent, + MockPrismEditorComponent, +} from "../mock-components.spec"; +import { IUiService } from "../../service/ui.service"; describe("ChannelsNewComponent", () => { beforeEach(async () => { @@ -15,8 +20,9 @@ describe("ChannelsNewComponent", () => { await render(ChannelsComponent, { imports: [MaterialModule], - declarations: [MockChannelOperationComponent], + declarations: [MockChannelOperationComponent, MockPrismEditorComponent], providers: [ + { provide: IUiService, useValue: mockedUiService }, { provide: AsyncApiService, useValue: mockedAsyncApiService }, ], }); diff --git a/springwolf-ui/src/app/components/channels/channels.component.ts b/springwolf-ui/src/app/components/channels/channels.component.ts index 4f58368ba..63ba6aaf1 100644 --- a/springwolf-ui/src/app/components/channels/channels.component.ts +++ b/springwolf-ui/src/app/components/channels/channels.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { AsyncApiService } from "../../service/asyncapi/asyncapi.service"; import { Channel } from "../../models/channel.model"; -import { UiService } from "../../service/ui.service"; +import { IUiService } from "../../service/ui.service"; @Component({ selector: "app-channels", @@ -11,12 +11,12 @@ import { UiService } from "../../service/ui.service"; }) export class ChannelsComponent implements OnInit { channels: Channel[] = []; - isShowBindings: boolean = UiService.DEFAULT_SHOW_BINDINGS; + isShowBindings: boolean = IUiService.DEFAULT_SHOW_BINDINGS; JSON = JSON; constructor( private asyncApiService: AsyncApiService, - private uiService: UiService + private uiService: IUiService ) {} ngOnInit(): void { diff --git a/springwolf-ui/src/app/components/header/header.component.html b/springwolf-ui/src/app/components/header/header.component.html index 1b0ecb35d..019f4081d 100644 --- a/springwolf-ui/src/app/components/header/header.component.html +++ b/springwolf-ui/src/app/components/header/header.component.html @@ -18,11 +18,32 @@

{{ title }}

- + + + @for (group of groups; track group) { + + } + +