From fa9a09d6506be52779d44a9a7696e4b323340ab6 Mon Sep 17 00:00:00 2001 From: LenKIM Date: Wed, 4 Jan 2023 17:50:07 +0900 Subject: [PATCH] relay --- .idea/compiler.xml | 23 +- .idea/modules.xml | 3 + codegen/.gitignore | 42 + codegen/build.gradle | 19 + .../codegen/spring/SpringCodeGenerator.java | 38 + .../org.openapitools.codegen.CodegenConfig | 1 + .../src/main/resources/trevari/api.mustache | 135 + .../resources/trevari/bodyParams.mustache | 1 + .../resources/trevari/cookieParams.mustache | 1 + .../resources/trevari/formParams.mustache | 1 + .../resources/trevari/headerParams.mustache | 1 + .../src/main/resources/trevari/model.mustache | 43 + .../resources/trevari/pathParams.mustache | 1 + .../src/main/resources/trevari/pojo.mustache | 167 + .../resources/trevari/queryParams.mustache | 1 + refs/build.gradle | 12 + refs/destination/build.gradle | 25 + .../events/destination/Address.java | 9 + .../events/destination/Destination.java | 53 + .../events/destination/DestinationId.java | 14 + .../events/destination/DestinationStatus.java | 5 + .../events/destination/DestinationTarget.java | 7 + .../events/destination/DestinationType.java | 6 + .../events/destination/DestinationTest.java | 21 + refs/event/build.gradle | 20 + .../org.masil.seoulyeok.events/Event.java | 29 + schema/schema.sql | 20 + seoulyeok-message-relay/api/Dockerfile | 9 + seoulyeok-message-relay/api/build.gradle | 117 + .../org/masil/seoulyeok/events/relay/App.java | 23 + .../relay/api/GlobalExceptionHandler.java | 19 + .../relay/api/SendMessagesController.java | 21 + .../application/AmazonSQSMessageSender.java | 47 + .../relay/application/MessageSender.java | 7 + .../events/relay/config/AmazonSQSConfig.java | 24 + .../relay/config/LongIdGeneratorFactory.java | 56 + .../events/relay/config/MessageConfig.java | 6 + .../src/main/resources/application-local.yml | 23 + .../main/resources/application-production.yml | 5 + .../api/src/main/resources/application.yml | 5 + .../swagger/.openapi-generator-ignore | 23 + .../swagger/.openapi-generator/FILES | 3 + .../swagger/.openapi-generator/VERSION | 1 + .../api/src/main/resources/swagger/README.md | 2 + .../src/main/resources/swagger/openapi.yml | 105 + .../seoulyeok/events/relay/api/ApiUtil.java | 19 + .../seoulyeok/events/relay/api/RelayApi.java | 41 + .../relay/models/RelayClientRequest.java | 156 + .../events/relay/models/RelayRequest.java | 130 + .../seoulyeok/events/relay/models/Result.java | 101 + .../api/src/main/swagger-config/.gitignore | 1 + .../api/src/main/swagger-config/api.yml | 97 + .../api/src/main/swagger-config/config.json | 23 + .../application/Dockerfile | 9 + .../application/build.gradle | 48 + .../events/relay/MessageRelayApplication.java | 22 + .../relay/application/DestinationFetcher.java | 19 + .../application/DestinationStatusChanger.java | 23 + .../application/RelaidMessageUpdater.java | 18 + .../relay/application/RelayProcessor.java | 57 + .../relay/application/SchedulerClient.java | 19 + .../relay/application/TopMessageFinder.java | 22 + .../application/alert/FakeSlackNotifier.java | 38 + .../application/alert/SendSlackAlertData.java | 11 + .../application/alert/SlackAlertNotifier.java | 38 + .../config/RelayProcessorConfig.java | 14 + .../config/RestTemplateConfig.java | 13 + .../application/config/SchedulerConfig.java | 20 + .../publisher/PublisherContainers.java | 41 + .../SimpleQueuePublisherContainer.java | 41 + .../AmazonFifoSQSDestinationPublisher.java | 51 + ...AmazonStandardSQSDestinationPublisher.java | 42 + .../SubscribeQueuePublisherContainer.java | 43 + .../UnSupportedSubscribeQueuePublisher.java | 16 + .../relay/config/PublisherBeanConfig.java | 52 + .../events/relay/web/DashBoardController.java | 42 + .../relay/web/model/DestinationView.java | 18 + .../src/main/resources/application-local.yml | 40 + .../main/resources/application-production.yml | 5 + .../src/main/resources/application.yml | 5 + .../main/resources/static/assets/favicon.ico | Bin 0 -> 23462 bytes .../src/main/resources/static/css/styles.css | 11743 ++++++++++++++++ .../src/main/resources/static/js/scripts.js | 7 + .../main/resources/templates/dashboard.html | 47 + .../relay/MessageRelayApplicationTest.java | 5 + .../application/DestinationFetcherTest.java | 35 + .../DestinationStatusChangerTest.java | 41 + .../application/RelaidMessageUpdaterTest.java | 33 + .../relay/application/RelayProcessorTest.java | 109 + .../application/SchedulerClientTest.java | 26 + .../application/SchedulingClientTest.java | 30 + .../application/TopMessageFinderTest.java | 36 + .../publisher/PublisherContainersTest.java | 90 + .../SimpleQueuePublisherContainerTest.java | 53 + ...onStandardSQSDestinationPublisherTest.java | 58 + .../SubscribeQueuePublisherContainerTest.java | 4 + .../relay/web/DashBoardControllerTest.java | 61 + seoulyeok-message-relay/build.gradle | 14 + seoulyeok-message-relay/domain/build.gradle | 32 + .../publisher/DefaultPublishResult.java | 46 + .../publisher/DestinationPublisher.java | 8 + .../events/publisher/PublishResult.java | 15 + .../events/publisher/PublisherContainer.java | 13 + .../events/relay/ReceivedEventMessage.java | 10 + .../events/relaymessage/MessagePayload.java | 8 + .../events/relaymessage/PayloadVersion.java | 27 + .../events/relaymessage/RelayMessage.java | 52 + .../events/relaymessage/RelayMessageId.java | 14 + .../relaymessage/RelayMessageModel.java | 11 + .../events/relaymessage/TopRelayMessage.java | 37 + .../relaymessage/PayloadVersionTest.java | 19 + seoulyeok-message-relay/messages/build.gradle | 30 + .../messages/EventValidateException.java | 16 + .../com/trevari/messages/EventValidator.java | 20 + .../messages/GeneralMessageEnvelop.java | 17 + ...ageEnvelopPayloadDeserializeException.java | 13 + .../GeneralMessageEnvelopProcessReturn.java | 6 + .../GeneralMessageEnvelopProcessTemplate.java | 59 + .../com/trevari/messages/MessageEnvelop.java | 11 + .../java/com/trevari/messages/MessageId.java | 14 + .../trevari/messages/EventValidatorTest.java | 59 + ...eralMessageEnvelopProcessTemplateTest.java | 107 + .../persistence/build.gradle | 42 + .../events/relay/config/DataJdbcConfig.java | 47 + .../destination/AddressToStringConverter.java | 13 + .../DestinationIdToLongConverter.java | 13 + .../LongToDestinationIdConverter.java | 13 + .../destination/StringToAddressConverter.java | 13 + .../LongToRelayMessageIdConverter.java | 13 + .../MessagePayloadReadingConverter.java | 18 + .../MessagePayloadWritingConverter.java | 28 + .../RelayMessageIdToLongConverter.java | 13 + .../events/relay/util/Serializer.java | 56 + .../events/relay/util/SerializerTest.java | 33 + .../src/test/resources/test-init.sql | 19 + settings.gradle | 16 +- 136 files changed, 15894 insertions(+), 8 deletions(-) create mode 100644 codegen/.gitignore create mode 100644 codegen/build.gradle create mode 100644 codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java create mode 100644 codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig create mode 100644 codegen/src/main/resources/trevari/api.mustache create mode 100644 codegen/src/main/resources/trevari/bodyParams.mustache create mode 100644 codegen/src/main/resources/trevari/cookieParams.mustache create mode 100644 codegen/src/main/resources/trevari/formParams.mustache create mode 100644 codegen/src/main/resources/trevari/headerParams.mustache create mode 100644 codegen/src/main/resources/trevari/model.mustache create mode 100644 codegen/src/main/resources/trevari/pathParams.mustache create mode 100644 codegen/src/main/resources/trevari/pojo.mustache create mode 100644 codegen/src/main/resources/trevari/queryParams.mustache create mode 100644 refs/build.gradle create mode 100644 refs/destination/build.gradle create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java create mode 100644 refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java create mode 100644 refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java create mode 100644 refs/event/build.gradle create mode 100644 refs/event/src/main/java/org.masil.seoulyeok.events/Event.java create mode 100644 seoulyeok-message-relay/api/Dockerfile create mode 100644 seoulyeok-message-relay/api/build.gradle create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java create mode 100644 seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java create mode 100644 seoulyeok-message-relay/api/src/main/resources/application-local.yml create mode 100644 seoulyeok-message-relay/api/src/main/resources/application-production.yml create mode 100644 seoulyeok-message-relay/api/src/main/resources/application.yml create mode 100644 seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore create mode 100644 seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES create mode 100644 seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION create mode 100644 seoulyeok-message-relay/api/src/main/resources/swagger/README.md create mode 100644 seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml create mode 100644 seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java create mode 100644 seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java create mode 100644 seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java create mode 100644 seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java create mode 100644 seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java create mode 100644 seoulyeok-message-relay/api/src/main/swagger-config/.gitignore create mode 100644 seoulyeok-message-relay/api/src/main/swagger-config/api.yml create mode 100644 seoulyeok-message-relay/api/src/main/swagger-config/config.json create mode 100644 seoulyeok-message-relay/application/Dockerfile create mode 100644 seoulyeok-message-relay/application/build.gradle create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SendSlackAlertData.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SlackAlertNotifier.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RelayProcessorConfig.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RestTemplateConfig.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/SchedulerConfig.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainers.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainer.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonFifoSQSDestinationPublisher.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisher.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainer.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/UnSupportedSubscribeQueuePublisher.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/config/PublisherBeanConfig.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/DashBoardController.java create mode 100644 seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/model/DestinationView.java create mode 100644 seoulyeok-message-relay/application/src/main/resources/application-local.yml create mode 100644 seoulyeok-message-relay/application/src/main/resources/application-production.yml create mode 100644 seoulyeok-message-relay/application/src/main/resources/application.yml create mode 100644 seoulyeok-message-relay/application/src/main/resources/static/assets/favicon.ico create mode 100644 seoulyeok-message-relay/application/src/main/resources/static/css/styles.css create mode 100644 seoulyeok-message-relay/application/src/main/resources/static/js/scripts.js create mode 100644 seoulyeok-message-relay/application/src/main/resources/templates/dashboard.html create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/MessageRelayApplicationTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationFetcherTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChangerTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdaterTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelayProcessorTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulerClientTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulingClientTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/TopMessageFinderTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainersTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainerTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisherTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainerTest.java create mode 100644 seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/web/DashBoardControllerTest.java create mode 100644 seoulyeok-message-relay/build.gradle create mode 100644 seoulyeok-message-relay/domain/build.gradle create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DefaultPublishResult.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DestinationPublisher.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublishResult.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublisherContainer.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relay/ReceivedEventMessage.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/MessagePayload.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/PayloadVersion.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessage.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageId.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageModel.java create mode 100644 seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/TopRelayMessage.java create mode 100644 seoulyeok-message-relay/domain/src/test/java/org/masil/seoulyeok/events/relaymessage/PayloadVersionTest.java create mode 100644 seoulyeok-message-relay/messages/build.gradle create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidateException.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidator.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelop.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopPayloadDeserializeException.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessReturn.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplate.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageEnvelop.java create mode 100644 seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageId.java create mode 100644 seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/EventValidatorTest.java create mode 100644 seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplateTest.java create mode 100644 seoulyeok-message-relay/persistence/build.gradle create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/config/DataJdbcConfig.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/AddressToStringConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/DestinationIdToLongConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/LongToDestinationIdConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/StringToAddressConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/LongToRelayMessageIdConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadReadingConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadWritingConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/RelayMessageIdToLongConverter.java create mode 100644 seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/util/Serializer.java create mode 100644 seoulyeok-message-relay/persistence/src/test/java/org/masil/seoulyeok/events/relay/util/SerializerTest.java create mode 100644 seoulyeok-message-relay/persistence/src/test/resources/test-init.sql diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 591e1ba..ca6dedb 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,15 +7,30 @@ - - - + + + + + + + + + + + + + + + + + + - + diff --git a/.idea/modules.xml b/.idea/modules.xml index b288176..17bfa87 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,6 +2,9 @@ + + + diff --git a/codegen/.gitignore b/codegen/.gitignore new file mode 100644 index 0000000..d26fe8b --- /dev/null +++ b/codegen/.gitignore @@ -0,0 +1,42 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +.generated +resources/swagger/ +resources/swagger/** + +swagger-codegen/** diff --git a/codegen/build.gradle b/codegen/build.gradle new file mode 100644 index 0000000..01ecb11 --- /dev/null +++ b/codegen/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + + +repositories { + mavenCentral() +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + + +dependencies { + implementation "org.openapitools:openapi-generator:5.3.1" +} diff --git a/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java b/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java new file mode 100644 index 0000000..0955e74 --- /dev/null +++ b/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java @@ -0,0 +1,38 @@ +package org.masil.seoulyeok.oas.codegen.spring; + +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.languages.SpringCodegen; + +public class SpringCodeGenerator extends SpringCodegen { + + + public SpringCodeGenerator() { + super(); + templateDir = "trevari"; + } + + @Override + public String getName() { + return "trevari"; + } + + @Override + public void processOpts() { + super.processOpts(); + // imports for pojos + importMapping.remove("ApiModelProperty"); + importMapping.remove("ApiModel"); + } + + @Override + public CodegenModel fromModel(String name, Schema model) { + super.fromModel(name, model); + CodegenModel codegenModel = super.fromModel(name, model); + codegenModel.imports.remove("ApiModel"); + codegenModel.imports.remove("ApiModelProperty"); + return codegenModel; + } + + +} diff --git a/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig new file mode 100644 index 0000000..09bdeda --- /dev/null +++ b/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -0,0 +1 @@ +org.masil.seoulyeok.oas.codegen.spring.SpringCodeGenerator diff --git a/codegen/src/main/resources/trevari/api.mustache b/codegen/src/main/resources/trevari/api.mustache new file mode 100644 index 0000000..0ef4d8d --- /dev/null +++ b/codegen/src/main/resources/trevari/api.mustache @@ -0,0 +1,135 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package {{package}}; + +{{#imports}}import {{import}}; +{{/imports}} + +{{#jdk8-no-delegate}} +{{#virtualService}} +import io.virtualan.annotation.ApiVirtual; +import io.virtualan.annotation.VirtualService; +{{/virtualService}} +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +{{/jdk8-no-delegate}} +import org.springframework.http.ResponseEntity; +{{#useBeanValidation}} +import org.springframework.validation.annotation.Validated; +{{/useBeanValidation}} +{{#vendorExtensions.x-spring-paginated}} +import org.springframework.data.domain.Pageable; +{{/vendorExtensions.x-spring-paginated}} +import org.springframework.web.bind.annotation.*; +{{#jdk8-no-delegate}} + {{^reactive}} +import org.springframework.web.context.request.NativeWebRequest; + {{/reactive}} +{{/jdk8-no-delegate}} +import org.springframework.web.multipart.MultipartFile; +{{#reactive}} +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.http.codec.multipart.Part; +{{/reactive}} + + +{{#useBeanValidation}} +import javax.validation.Valid; +import javax.validation.constraints.*; +{{/useBeanValidation}} +import java.util.List; +import java.util.Map; +{{#jdk8-no-delegate}} +import java.util.Optional; +{{/jdk8-no-delegate}} +{{^jdk8-no-delegate}} + {{#useOptional}} +import java.util.Optional; + {{/useOptional}} +{{/jdk8-no-delegate}} +{{#async}} +import java.util.concurrent.{{^jdk8}}Callable{{/jdk8}}{{#jdk8}}CompletableFuture{{/jdk8}}; +{{/async}} +{{>generatedAnnotation}} +{{#useBeanValidation}} +@Validated +{{/useBeanValidation}} + +{{#operations}} +{{#virtualService}} +@VirtualService +{{/virtualService}} +@RestController +public interface {{classname}} { +{{#jdk8-default-interface}} + {{^isDelegate}} + {{^reactive}} + + default Optional getRequest() { + return Optional.empty(); + } + {{/reactive}} + {{/isDelegate}} + {{#isDelegate}} + + default {{classname}}Delegate getDelegate() { + return new {{classname}}Delegate() {}; + } + {{/isDelegate}} +{{/jdk8-default-interface}} +{{#operation}} + + /** + * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} + {{#notes}} + * {{.}} + {{/notes}} + * + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} + * or {{/-last}}{{/responses}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#virtualService}} + + {{/virtualService}} + + @{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}Mapping( + value = "{{{path}}}"{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-contentType}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}}, + consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} {{#delegate-method}}_{{/delegate-method}}{{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}},{{/-last}}{{#-last}}{{#reactive}}, {{/reactive}}{{/-last}}{{/allParams}}{{#reactive}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}}{{#unhandledException}} throws Exception{{/unhandledException}} { + {{#delegate-method}} + return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}}); + } + + // Override this method + {{#jdk8-default-interface}}default {{/jdk8-default-interface}} {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}} { + {{/delegate-method}} + {{^isDelegate}} + {{>methodBody}} + {{/isDelegate}} + {{#isDelegate}} + return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}}); + {{/isDelegate}} + }{{/jdk8-default-interface}} + +{{/operation}} +} +{{/operations}} diff --git a/codegen/src/main/resources/trevari/bodyParams.mustache b/codegen/src/main/resources/trevari/bodyParams.mustache new file mode 100644 index 0000000..e4f03e8 --- /dev/null +++ b/codegen/src/main/resources/trevari/bodyParams.mustache @@ -0,0 +1 @@ +{{#isBodyParam}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/codegen/src/main/resources/trevari/cookieParams.mustache b/codegen/src/main/resources/trevari/cookieParams.mustache new file mode 100644 index 0000000..1cab1e3 --- /dev/null +++ b/codegen/src/main/resources/trevari/cookieParams.mustache @@ -0,0 +1 @@ +{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}} @CookieValue("{{baseName}}") {{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file diff --git a/codegen/src/main/resources/trevari/formParams.mustache b/codegen/src/main/resources/trevari/formParams.mustache new file mode 100644 index 0000000..5ca0fa7 --- /dev/null +++ b/codegen/src/main/resources/trevari/formParams.mustache @@ -0,0 +1 @@ +{{#isFormParam}}{{^isFile}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{dataType}}} {{paramName}}{{/isFile}}{{#isFile}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#isArray}}List<{{/isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}} {{baseName}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/codegen/src/main/resources/trevari/headerParams.mustache b/codegen/src/main/resources/trevari/headerParams.mustache new file mode 100644 index 0000000..75a39dc --- /dev/null +++ b/codegen/src/main/resources/trevari/headerParams.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}@RequestHeader(value="{{baseName}}", required={{#required}}true{{/required}}{{^required}}false{{/required}}) {{>optionalDataType}} {{paramName}}{{/isHeaderParam}} \ No newline at end of file diff --git a/codegen/src/main/resources/trevari/model.mustache b/codegen/src/main/resources/trevari/model.mustache new file mode 100644 index 0000000..78c3c9a --- /dev/null +++ b/codegen/src/main/resources/trevari/model.mustache @@ -0,0 +1,43 @@ +package {{package}}; + +import java.util.Objects; +{{#imports}}import {{import}}; +{{/imports}} +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullable; +{{/openApiNullable}} +{{#serializableModel}} +import java.io.Serializable; +{{/serializableModel}} +{{#useBeanValidation}} +import javax.validation.Valid; +import javax.validation.constraints.*; +{{/useBeanValidation}} +{{#performBeanValidation}} +import org.hibernate.validator.constraints.*; +{{/performBeanValidation}} +{{#jackson}} +{{#withXml}} +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +{{/withXml}} +{{/jackson}} +{{#withXml}} +import javax.xml.bind.annotation.*; +{{/withXml}} +{{^parent}} +{{#hateoas}} +import org.springframework.hateoas.RepresentationModel; +{{/hateoas}} +{{/parent}} + +{{#models}} +{{#model}} +{{#isEnum}} +{{>enumOuterClass}} +{{/isEnum}} +{{^isEnum}} +{{>pojo}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/codegen/src/main/resources/trevari/pathParams.mustache b/codegen/src/main/resources/trevari/pathParams.mustache new file mode 100644 index 0000000..4bf8e25 --- /dev/null +++ b/codegen/src/main/resources/trevari/pathParams.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}@PathVariable("{{baseName}}") {{>optionalDataType}} {{paramName}}{{/isPathParam}} \ No newline at end of file diff --git a/codegen/src/main/resources/trevari/pojo.mustache b/codegen/src/main/resources/trevari/pojo.mustache new file mode 100644 index 0000000..2dcb88d --- /dev/null +++ b/codegen/src/main/resources/trevari/pojo.mustache @@ -0,0 +1,167 @@ +/** + * {{#description}}{{.}}{{/description}}{{^description}}{{classname}}{{/description}} + */{{#description}} +//{{{description}}} +{{/description}} +{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}}{{>additionalModelTypeAnnotations}} +public class {{classname}} {{#parent}}extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{#serializableModel}}implements Serializable{{/serializableModel}} { +{{#serializableModel}} + private static final long serialVersionUID = 1L; + +{{/serializableModel}} + {{#vars}} + {{#isEnum}} + {{^isContainer}} +{{>enumClass}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} +{{>enumClass}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#jackson}} + @JsonProperty("{{baseName}}"){{#withXml}} + @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"){{/withXml}} + {{/jackson}} + {{#gson}} + @SerializedName("{{baseName}}") + {{/gson}} + {{#isContainer}} + {{#useBeanValidation}}@Valid{{/useBeanValidation}} + {{#openApiNullable}} + private {{>nullableDataType}} {{name}} = {{#isNullable}}JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#required}}{{{defaultValue}}}{{/required}}{{^required}}null{{/required}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + private {{>nullableDataType}} {{name}} = {{#required}}{{{defaultValue}}}{{/required}}{{^required}}null{{/required}}; + {{/openApiNullable}} + {{/isContainer}} + {{^isContainer}} + {{#isDate}} + @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) + {{/isDate}} + {{#isDateTime}} + @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME) + {{/isDateTime}} + {{#openApiNullable}} + private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{/isContainer}} + + {{/vars}} + {{#vars}} + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + {{#openApiNullable}} + this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + this.{{name}} = {{name}}; + {{/openApiNullable}} + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#openApiNullable}} + {{^required}} + if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) { + this.{{name}} = {{#isNullable}}JsonNullable.of({{{defaultValue}}}){{/isNullable}}{{^isNullable}}{{{defaultValue}}}{{/isNullable}}; + } + {{/required}} + this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item); + {{/openApiNullable}} + {{^openApiNullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}; + } + this.{{name}}.add({{name}}Item); + {{/openApiNullable}} + return this; + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{^required}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}; + } + {{/required}} + this.{{name}}.put(key, {{name}}Item); + return this; + } + {{/isMap}} + + /** + {{#description}} + * {{{description}}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{minimum}} + {{/minimum}} + {{#maximum}} + * maximum: {{maximum}} + {{/maximum}} + * @return {{name}} + */ + {{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} + {{/vendorExtensions.x-extra-annotation}} + +{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}} public {{>nullableDataType}} {{getter}}() { + return {{name}}; + } + + public void {{setter}}({{>nullableDataType}} {{name}}) { + this.{{name}} = {{name}}; + } + + {{/vars}} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{/vars}}{{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return true;{{/hasVars}} + } + + @Override + public int hashCode() { + return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}} + {{#vars}}sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/codegen/src/main/resources/trevari/queryParams.mustache b/codegen/src/main/resources/trevari/queryParams.mustache new file mode 100644 index 0000000..c12032e --- /dev/null +++ b/codegen/src/main/resources/trevari/queryParams.mustache @@ -0,0 +1 @@ +{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}} {{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{>optionalDataType}} {{paramName}}{{/isQueryParam}} \ No newline at end of file diff --git a/refs/build.gradle b/refs/build.gradle new file mode 100644 index 0000000..112fe73 --- /dev/null +++ b/refs/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-library' +} + +group = 'org.masil.seoulyeok' + +allprojects { + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } +} diff --git a/refs/destination/build.gradle b/refs/destination/build.gradle new file mode 100644 index 0000000..dd4939a --- /dev/null +++ b/refs/destination/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java-library' + id "io.freefair.lombok" version "6.4.3" + +} + +group = 'org.masil.seoulyeok' + +dependencies { + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation 'org.assertj:assertj-core:3.20.2' + +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java new file mode 100644 index 0000000..a52391f --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java @@ -0,0 +1,9 @@ +package org.masil.seoulyeok.events.destination; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Address { + + String value; +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java new file mode 100644 index 0000000..1112464 --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java @@ -0,0 +1,53 @@ +package org.masil.seoulyeok.events.destination; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public class Destination implements DestinationTarget { + + public static Destination create(Address address, DestinationType type) { + DestinationId id = DestinationId.of(1L); + return new Destination(id, address, type, DestinationStatus.ACTIVE); + } + + public static Destination of(DestinationId id, Address address, DestinationType type) { + return new Destination(id, address, type, DestinationStatus.ACTIVE); + } + + public static Destination of(DestinationId id, Address address, DestinationType type, DestinationStatus status) { + return new Destination(id, address, type, status); + } + + private final DestinationId id; + private final Address address; + private final DestinationType type; + private DestinationStatus status; + + private Destination(DestinationId id, Address address, DestinationType type, DestinationStatus status) { + this.id = id; + this.address = address; + this.type = type; + this.status = status; + } + + @Override + public DestinationId getId() { + return id; + } + + @Override + public Address getAddress() { + return address; + } + + @Override + public DestinationType getType() { + return type; + } + + public void inactive() { + status = DestinationStatus.INACTIVE; + } +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java new file mode 100644 index 0000000..cb4185a --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java @@ -0,0 +1,14 @@ +package org.masil.seoulyeok.events.destination; + +import com.likelen.identifier.core.LongId; +import lombok.Value; + +@Value(staticConstructor = "of") +public class DestinationId implements LongId { + Long value; + + @Override + public Long get() { + return value; + } +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java new file mode 100644 index 0000000..fec556a --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java @@ -0,0 +1,5 @@ +package org.masil.seoulyeok.events.destination; + +public enum DestinationStatus { + ACTIVE, INACTIVE +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java new file mode 100644 index 0000000..c61962e --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java @@ -0,0 +1,7 @@ +package org.masil.seoulyeok.events.destination; + +public interface DestinationTarget { + DestinationId getId(); + Address getAddress(); + DestinationType getType(); +} diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java new file mode 100644 index 0000000..c7b1291 --- /dev/null +++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java @@ -0,0 +1,6 @@ +package org.masil.seoulyeok.events.destination; + +public enum DestinationType { + + SIMPLE_QUEUE, SUBSCRIBER_QUEUE +} diff --git a/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java b/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java new file mode 100644 index 0000000..97fff7d --- /dev/null +++ b/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java @@ -0,0 +1,21 @@ +package org.masil.seoulyeok.events.destination; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.masil.seoulyeok.events.destination.*; + +class DestinationTest { + + Destination sut; + + @Test + void inactive_상태로_갈_수_있다() { + sut = Destination.of(DestinationId.of(1L), Address.of("some_address"), DestinationType.SIMPLE_QUEUE); + assertThat(sut.getStatus()).isEqualTo(DestinationStatus.ACTIVE); + + sut.inactive(); + assertThat(sut.getStatus()).isEqualTo(DestinationStatus.INACTIVE); + } +} diff --git a/refs/event/build.gradle b/refs/event/build.gradle new file mode 100644 index 0000000..7c40ecc --- /dev/null +++ b/refs/event/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java-library' + id "io.freefair.lombok" version "6.4.3" + +} + +group = 'org.masil.seoulyeok' + +dependencies { + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation 'org.assertj:assertj-core:3.20.2' + +} diff --git a/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java b/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java new file mode 100644 index 0000000..6490402 --- /dev/null +++ b/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java @@ -0,0 +1,29 @@ +package org.masil.seoulyeok.events; + +import com.trevari.domain.core.DomainEventId; +import lombok.Value; + +import java.time.LocalDateTime; +import java.util.Objects; + + +@Value(staticConstructor = "of") +public class Event { + DomainEventId id; + String eventType; + String payload; + String payloadVersion; + + LocalDateTime occurredAt; + + public Event(DomainEventId id, String eventType, String payload, String payloadVersion, LocalDateTime occurredAt) { + Objects.requireNonNull(id); + Objects.requireNonNull(eventType); + Objects.requireNonNull(payloadVersion); + this.id = id; + this.eventType = eventType; + this.payload = payload; + this.payloadVersion = payloadVersion; + this.occurredAt = occurredAt; + } +} diff --git a/schema/schema.sql b/schema/schema.sql index bc70b15..9538018 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -21,3 +21,23 @@ CREATE TABLE IF NOT EXISTS pulled_offset created_at TIMESTAMPTZ DEFAULT NOW(), version INT not null ); + +CREATE TABLE IF NOT EXISTS destination +( + id BIGINT PRIMARY KEY, + address VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + status VARCHAR(50), + version BIGINT NOT NULL + ); + +CREATE TABLE IF NOT EXISTS relay_message +( + id BIGINT PRIMARY KEY, + destination_id BIGINT NOT NULL, + message_payload json, + created_at TIMESTAMPTZ, + relied_at TIMESTAMPTZ, + payload_version VARCHAR(50), + version INT NOT NULL + ); diff --git a/seoulyeok-message-relay/api/Dockerfile b/seoulyeok-message-relay/api/Dockerfile new file mode 100644 index 0000000..ad51613 --- /dev/null +++ b/seoulyeok-message-relay/api/Dockerfile @@ -0,0 +1,9 @@ +FROM amazoncorretto:11-alpine + +ARG FROM_JAR=build/libs/api.jar + +COPY ${FROM_JAR} app.jar + +EXPOSE 5000 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/seoulyeok-message-relay/api/build.gradle b/seoulyeok-message-relay/api/build.gradle new file mode 100644 index 0000000..782fcf0 --- /dev/null +++ b/seoulyeok-message-relay/api/build.gradle @@ -0,0 +1,117 @@ +import org.openapitools.generator.gradle.plugin.tasks.GenerateTask + +buildscript { + dependencies { + classpath project(':codegen') + } +} + +plugins { + id 'java-library' + id "org.springframework.boot" + id "io.spring.dependency-management" + id "org.openapi.generator" version "5.2.1" + id "io.freefair.lombok" + + +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +ext { + set('springCloudVersion', "2.4.1") +} + + +group 'org.masil.seoulyeok.events.relay' + +project.sourceSets.test.resources.srcDirs = [ "${projectDir}/src/test/resources", "${rootDir}/schema" ] + + +dependencies { + + implementation('io.awspring.cloud:spring-cloud-starter-aws-messaging:2.4.1') { + exclude group: 'com.amazonaws', module: 'aws-java-sdk-s3' + } + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-test' + + //fixed for swagger + compileOnly 'io.swagger:swagger-annotations:1.6.2' + compileOnly 'jakarta.validation:jakarta.validation-api:2.0.2' + compileOnly 'org.openapitools:jackson-databind-nullable:0.1.0' + + implementation 'org.springframework.cloud:spring-cloud-config-client:3.1.3' + + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + + implementation 'com.github.moimp:domain-core:0.0.2' + + implementation project(':seoulyeok-message-relay:messages') + + implementation 'org.json:json:20220320' +} + +def swaggerRoot = "$projectDir/src/main/swagger-config".toString() +def swaggerAPIOutput = "$projectDir/src/main".toString() + +openApiValidate { + inputSpec="$swaggerRoot/api.yml".toString() + recommend=true +} + +openApiGenerate { + inputSpec="$swaggerRoot/api.yml".toString() + generatorName = "trevari" + configFile= "$swaggerRoot/config.json".toString() + outputDir= "$swaggerAPIOutput".toString() + configOptions = [ + "sourceFolder" : "swagger-codegen" + ] +} +tasks.named("openApiGenerate") { + def outputDir = openApiGenerate.outputDir.get() + it.doFirst { + delete outputDir +"/"+ openApiGenerate.configOptions["sourceFolder"].get() + } + + it.doLast { + delete "${outputDir}/pom.xml" + delete "${outputDir}/README.md" + delete "${outputDir}/.openapi-generator-ignore" + delete "${outputDir}/.openapi-generator" + } +} + +task openApiYmlGenerate(type: GenerateTask) { + inputSpec = "$swaggerRoot/api.yml".toString() + generatorName = "openapi-yaml".toString() + outputDir = "$swaggerAPIOutput/resources/swagger/".toString() + configOptions = [ + outputFile: "openapi.yml" + ] + + it.doFirst { + delete "$swaggerAPIOutput/resources/swagger/" + } +} + +tasks.openApiGenerate.dependsOn tasks.openApiValidate +tasks.compileJava.dependsOn tasks.openApiGenerate, tasks.openApiYmlGenerate +sourceSets.main.java.srcDir "${openApiGenerate.outputDir.get()}/${openApiGenerate.configOptions['sourceFolder'].get()}" +sourceSets.main.resources.srcDir "${openApiGenerate.outputDir.get()}/src/main/resources" + + +test { + useJUnitPlatform() +} + +dependencyManagement { + imports { + mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudVersion}" + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java new file mode 100644 index 0000000..ed7a241 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java @@ -0,0 +1,23 @@ +package org.masil.seoulyeok.events.relay; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class App { + + @RestController + public static class HelloController { + + @GetMapping(value = "/annyeng") + public String hello() { + return "hello"; + } + } + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java new file mode 100644 index 0000000..a665e28 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package org.masil.seoulyeok.events.relay.api; + +import org.masil.seoulyeok.events.relay.models.Result; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + return ResponseEntity.internalServerError() + .body(new Result() + .code(-1) + .message(e.getMessage()) + ); + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java new file mode 100644 index 0000000..b2ff6f0 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java @@ -0,0 +1,21 @@ +package org.masil.seoulyeok.events.relay.api; + +import org.masil.seoulyeok.events.relay.application.MessageSender; +import org.masil.seoulyeok.events.relay.models.RelayRequest; +import org.masil.seoulyeok.events.relay.models.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class SendMessagesController implements RelayApi { + + private final MessageSender sender; + + @Override + public ResponseEntity deliveryToDestination(RelayRequest relayRequest) { + sender.doWork(relayRequest); + return ResponseEntity.ok(new Result().code(0).message("ok")); + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java new file mode 100644 index 0000000..b863821 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java @@ -0,0 +1,47 @@ +package org.masil.seoulyeok.events.relay.application; + +import com.trevari.domain.core.Serializer; +import org.masil.seoulyeok.events.relay.config.MessageConfig; +import org.masil.seoulyeok.events.relay.models.RelayRequest; +import com.trevari.messages.GeneralMessageEnvelop; +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; +import lombok.RequiredArgsConstructor; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.messaging.MessagingException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AmazonSQSMessageSender implements MessageSender { + + private final QueueMessagingTemplate queueMessagingTemplate; + private final MessageConfig config; + + @Override + public void doWork(RelayRequest relayRequest) { + String messagePayload = relayRequest.getPayload(); + if (!isValid(messagePayload)) { + throw new IllegalArgumentException("MessagePayload is not Json Type messagePayload: " + messagePayload); + } + String queue = config.getFrontControllerQueueName(); + + try { + String serialize = Serializer.getInstance().serialize(relayRequest); + + queueMessagingTemplate.convertAndSend(queue, new GeneralMessageEnvelop<>(serialize)); + } catch (MessagingException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private boolean isValid(String json) { + try { + new JSONObject(json); + } catch (JSONException e) { + return false; + } + return true; + } + +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java new file mode 100644 index 0000000..1d1aa1d --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java @@ -0,0 +1,7 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.relay.models.RelayRequest; + +public interface MessageSender { + void doWork(RelayRequest relayRequest); +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java new file mode 100644 index 0000000..cecf1c6 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java @@ -0,0 +1,24 @@ +package org.masil.seoulyeok.events.relay.config; + +import com.amazonaws.services.sqs.AmazonSQSAsync; +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AmazonSQSConfig implements MessageConfig { + + @Value("${aws.sqs.pulled-events-received}") + private String frontControllerQueueName; + + @Bean + public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync) { + return new QueueMessagingTemplate(amazonSQSAsync); + } + + @Override + public String getFrontControllerQueueName() { + return frontControllerQueueName; + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java new file mode 100644 index 0000000..e800694 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java @@ -0,0 +1,56 @@ +package org.masil.seoulyeok.events.relay.config; + +import com.trevari.identity.generator.IdGeneratorFactory; +import com.trevari.identity.generator.LongIdGenerator; +import com.trevari.identity.generator.LongIdGeneratorHolder; +import com.trevari.identity.generator.LongValueGenerator; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; + +@Configuration +@RequiredArgsConstructor +public class LongIdGeneratorFactory extends AbstractFactoryBean { + + @Getter + @Setter + @Value("${id-generator.url}") + private String url; + + private final Environment environment; + + @Override + public Class getObjectType() { + return LongIdGenerator.class; + } + + @Override + protected LongIdGenerator createInstance() { + IdGeneratorFactory idGeneratorFactory; + if (Arrays.asList(environment.getActiveProfiles()).contains("production")) { + idGeneratorFactory = new IdGeneratorFactory(url); + } else { + idGeneratorFactory = new IdGeneratorFactory(new DummyLongValueGenerator()); + } + LongIdGenerator longIdGenerator = idGeneratorFactory.create(); + LongIdGeneratorHolder.set(longIdGenerator); + return longIdGenerator; + } + + private static class DummyLongValueGenerator implements LongValueGenerator { + static final long SOURCE = 10000000; + static final long BOUND = 99999999; + + @Override + public long gen() { + return ThreadLocalRandom.current().nextLong(SOURCE, BOUND); + } + } +} diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java new file mode 100644 index 0000000..13e89c2 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java @@ -0,0 +1,6 @@ +package org.masil.seoulyeok.events.relay.config; + +public interface MessageConfig { + + String getFrontControllerQueueName(); +} diff --git a/seoulyeok-message-relay/api/src/main/resources/application-local.yml b/seoulyeok-message-relay/api/src/main/resources/application-local.yml new file mode 100644 index 0000000..d45b61e --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/application-local.yml @@ -0,0 +1,23 @@ +server: + port: 8071 + +spring: + cloud: + config: + enabled: false +cloud: + aws: + region: + static: ap-northeast-2 + stack: + auto: false + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + +id-generator: + url: "https://not-exists-idgen-url.com" + +aws: + sqs: + pulled-events-received: PULLED-EVENTS-RECEIVED-DEV diff --git a/seoulyeok-message-relay/api/src/main/resources/application-production.yml b/seoulyeok-message-relay/api/src/main/resources/application-production.yml new file mode 100644 index 0000000..bb1d06c --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/application-production.yml @@ -0,0 +1,5 @@ +spring: + application: + name: message-relay-api + config: + import: "optional:configserver:https://config.trevari.co.kr" diff --git a/seoulyeok-message-relay/api/src/main/resources/application.yml b/seoulyeok-message-relay/api/src/main/resources/application.yml new file mode 100644 index 0000000..d5c85e8 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + profiles: + active: local + + diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES new file mode 100644 index 0000000..027c423 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES @@ -0,0 +1,3 @@ +.openapi-generator-ignore +README.md +openapi.yml diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION new file mode 100644 index 0000000..7d3cdbf --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.3.1 \ No newline at end of file diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/README.md b/seoulyeok-message-relay/api/src/main/resources/swagger/README.md new file mode 100644 index 0000000..6b0f684 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/swagger/README.md @@ -0,0 +1,2 @@ +# OpenAPI YAML +This is a OpenAPI YAML built by the [openapi-generator](https://github.com/openapitools/openapi-genreator) project. \ No newline at end of file diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml b/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml new file mode 100644 index 0000000..9095dca --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml @@ -0,0 +1,105 @@ +openapi: 3.0.0 +info: + license: + name: MIT + title: notification + version: 1.0.2 +servers: +- url: / +paths: + /apis/events/relay: + post: + description: 특정 메세지를 목적지로 전달합니다 + operationId: deliveryToDestination + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RelayRequest' + description: deliveryToDestination + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Result' + description: "데이터 전송 성공 code = 0, meg: ok" + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/Result' + description: "입력이 유효하지 않습니다 code = -1, meg: fail" + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/Result' + description: "시스템 오류 발생 code = -1, meg: fail" + summary: 특정 메세지를 목적지로 전달합니다 + tags: + - relay +components: + schemas: + RelayRequest: + example: + payloadVersion: 1.0.0 + payload: "{ \"eventId\": 1 }" + destinationId: 54321 + properties: + payload: + example: "{ \"eventId\": 1 }" + type: string + destinationId: + example: 54321 + format: int64 + type: integer + payloadVersion: + example: 1.0.0 + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\\ + d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\ + +([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + type: string + required: + - destinationId + - payload + - payloadVersion + type: object + RelayClientRequest: + properties: + destinationType: + example: SIMPLE_QUEUE + type: string + messagePayload: + example: "{ \"eventId\": 1 }" + type: string + destinationName: + example: settled.fifo + type: string + payloadVersion: + example: 1.0.0 + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\\ + d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\ + +([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + type: string + required: + - destinationName + - destinationType + - messagePayload + - payloadVersion + type: object + Result: + example: + code: 1 + message: success | fail + properties: + code: + example: 1 + type: integer + message: + example: success | fail + type: string + required: + - value + type: object diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java new file mode 100644 index 0000000..baa04b1 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java @@ -0,0 +1,19 @@ +package org.masil.seoulyeok.events.relay.api; + +import org.springframework.web.context.request.NativeWebRequest; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ApiUtil { + public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { + try { + HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); + res.setCharacterEncoding("UTF-8"); + res.addHeader("Content-Type", contentType); + res.getWriter().print(example); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java new file mode 100644 index 0000000..9732fbe --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java @@ -0,0 +1,41 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package org.masil.seoulyeok.events.relay.api; + +import org.masil.seoulyeok.events.relay.models.RelayRequest; +import org.masil.seoulyeok.events.relay.models.Result; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + + +import javax.validation.Valid; + +@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator") +@Validated + +@RestController +public interface RelayApi { + + /** + * POST /apis/events/relay : 특정 메세지를 목적지로 전달합니다 + * 특정 메세지를 목적지로 전달합니다 + * + * @param relayRequest deliveryToDestination (required) + * @return 데이터 전송 성공 code = 0, meg: ok (status code 200) + * or 입력이 유효하지 않습니다 code = -1, meg: fail (status code 400) + * or 시스템 오류 발생 code = -1, meg: fail (status code 500) + */ + + @PostMapping( + value = "/apis/events/relay", + produces = { "application/json" }, + consumes = { "application/json" } + ) + ResponseEntity deliveryToDestination(@Valid @RequestBody RelayRequest relayRequest); + +} diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java new file mode 100644 index 0000000..eef27a3 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java @@ -0,0 +1,156 @@ +package org.masil.seoulyeok.events.relay.models; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import javax.validation.constraints.*; + +/** + * RelayClientRequest + */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator") +public class RelayClientRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("destinationType") + private String destinationType; + + @JsonProperty("messagePayload") + private String messagePayload; + + @JsonProperty("destinationName") + private String destinationName; + + @JsonProperty("payloadVersion") + private String payloadVersion; + + public RelayClientRequest destinationType(String destinationType) { + this.destinationType = destinationType; + return this; + } + + /** + * Get destinationType + * @return destinationType + */ + + @NotNull + + + public String getDestinationType() { + return destinationType; + } + + public void setDestinationType(String destinationType) { + this.destinationType = destinationType; + } + + public RelayClientRequest messagePayload(String messagePayload) { + this.messagePayload = messagePayload; + return this; + } + + /** + * Get messagePayload + * @return messagePayload + */ + + @NotNull + + + public String getMessagePayload() { + return messagePayload; + } + + public void setMessagePayload(String messagePayload) { + this.messagePayload = messagePayload; + } + + public RelayClientRequest destinationName(String destinationName) { + this.destinationName = destinationName; + return this; + } + + /** + * Get destinationName + * @return destinationName + */ + + @NotNull + + + public String getDestinationName() { + return destinationName; + } + + public void setDestinationName(String destinationName) { + this.destinationName = destinationName; + } + + public RelayClientRequest payloadVersion(String payloadVersion) { + this.payloadVersion = payloadVersion; + return this; + } + + /** + * Get payloadVersion + * @return payloadVersion + */ + + @NotNull + +@Pattern(regexp = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$") + public String getPayloadVersion() { + return payloadVersion; + } + + public void setPayloadVersion(String payloadVersion) { + this.payloadVersion = payloadVersion; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RelayClientRequest relayClientRequest = (RelayClientRequest) o; + return Objects.equals(this.destinationType, relayClientRequest.destinationType) && + Objects.equals(this.messagePayload, relayClientRequest.messagePayload) && + Objects.equals(this.destinationName, relayClientRequest.destinationName) && + Objects.equals(this.payloadVersion, relayClientRequest.payloadVersion); + } + + @Override + public int hashCode() { + return Objects.hash(destinationType, messagePayload, destinationName, payloadVersion); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class RelayClientRequest {\n"); + + sb.append(" destinationType: ").append(toIndentedString(destinationType)).append("\n"); + sb.append(" messagePayload: ").append(toIndentedString(messagePayload)).append("\n"); + sb.append(" destinationName: ").append(toIndentedString(destinationName)).append("\n"); + sb.append(" payloadVersion: ").append(toIndentedString(payloadVersion)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java new file mode 100644 index 0000000..e25fa5c --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java @@ -0,0 +1,130 @@ +package org.masil.seoulyeok.events.relay.models; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import javax.validation.constraints.*; + +/** + * RelayRequest + */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator") +public class RelayRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("payload") + private String payload; + + @JsonProperty("destinationId") + private Long destinationId; + + @JsonProperty("payloadVersion") + private String payloadVersion; + + public RelayRequest payload(String payload) { + this.payload = payload; + return this; + } + + /** + * Get payload + * @return payload + */ + + @NotNull + + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public RelayRequest destinationId(Long destinationId) { + this.destinationId = destinationId; + return this; + } + + /** + * Get destinationId + * @return destinationId + */ + + @NotNull + + + public Long getDestinationId() { + return destinationId; + } + + public void setDestinationId(Long destinationId) { + this.destinationId = destinationId; + } + + public RelayRequest payloadVersion(String payloadVersion) { + this.payloadVersion = payloadVersion; + return this; + } + + /** + * Get payloadVersion + * @return payloadVersion + */ + + @NotNull + +@Pattern(regexp = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$") + public String getPayloadVersion() { + return payloadVersion; + } + + public void setPayloadVersion(String payloadVersion) { + this.payloadVersion = payloadVersion; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RelayRequest relayRequest = (RelayRequest) o; + return Objects.equals(this.payload, relayRequest.payload) && + Objects.equals(this.destinationId, relayRequest.destinationId) && + Objects.equals(this.payloadVersion, relayRequest.payloadVersion); + } + + @Override + public int hashCode() { + return Objects.hash(payload, destinationId, payloadVersion); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class RelayRequest {\n"); + + sb.append(" payload: ").append(toIndentedString(payload)).append("\n"); + sb.append(" destinationId: ").append(toIndentedString(destinationId)).append("\n"); + sb.append(" payloadVersion: ").append(toIndentedString(payloadVersion)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java new file mode 100644 index 0000000..bf002ca --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java @@ -0,0 +1,101 @@ +package org.masil.seoulyeok.events.relay.models; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * Result + */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator") +public class Result implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("code") + private Integer code; + + @JsonProperty("message") + private String message; + + public Result code(Integer code) { + this.code = code; + return this; + } + + /** + * Get code + * @return code + */ + + + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public Result message(String message) { + this.message = message; + return this; + } + + /** + * Get message + * @return message + */ + + + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Result result = (Result) o; + return Objects.equals(this.code, result.code) && + Objects.equals(this.message, result.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Result {\n"); + + sb.append(" code: ").append(toIndentedString(code)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore b/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore new file mode 100644 index 0000000..aa6d44c --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore @@ -0,0 +1 @@ +.generated \ No newline at end of file diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/api.yml b/seoulyeok-message-relay/api/src/main/swagger-config/api.yml new file mode 100644 index 0000000..4ee1c33 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-config/api.yml @@ -0,0 +1,97 @@ +openapi: "3.0.0" +info: + version: 1.0.2 + title: notification + license: + name: MIT +paths: + /apis/events/relay: + post: + tags: + - relay + summary: "특정 메세지를 목적지로 전달합니다" + description: "특정 메세지를 목적지로 전달합니다" + operationId: "deliveryToDestination" + requestBody: + description: "deliveryToDestination" + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RelayRequest" + + responses: + '200': + description: "데이터 전송 성공 code = 0, meg: ok" + content: + application/json: + schema: + $ref: "#/components/schemas/Result" + '400': + description: "입력이 유효하지 않습니다 code = -1, meg: fail" + content: + application/json: + schema: + $ref: "#/components/schemas/Result" + + '500': + description: "시스템 오류 발생 code = -1, meg: fail" + content: + application/json: + schema: + $ref: "#/components/schemas/Result" + +components: + schemas: + RelayRequest: + type: "object" + required: [ + payload, + destinationId, + payloadVersion + ] + properties: + payload: + type: 'string' + example: "{ \"eventId\": 1 }" + destinationId: + type: 'integer' + format: 'int64' + example: '54321' + payloadVersion: + type: 'string' + example: "1.0.0" + pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + RelayClientRequest: + type: "object" + required: [ + destinationType, + messagePayload, + destinationName, + payloadVersion + ] + properties: + destinationType: + type: 'string' + example: "SIMPLE_QUEUE" + messagePayload: + type: 'string' + example: "{ \"eventId\": 1 }" + destinationName: + type: 'string' + example: 'settled.fifo' + payloadVersion: + type: 'string' + example: "1.0.0" + pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + Result: + type: "object" + required: + - value + properties: + code: + type: "integer" + example: 1 + message: + type: "string" + example: "success | fail" diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/config.json b/seoulyeok-message-relay/api/src/main/swagger-config/config.json new file mode 100644 index 0000000..399cee6 --- /dev/null +++ b/seoulyeok-message-relay/api/src/main/swagger-config/config.json @@ -0,0 +1,23 @@ +{ + + "library": "spring-boot", + "interfaceOnly" : true, + "basePackage" : "com.trevari.events.relay", + "configPackage" : "com.trevari.events.relay.config", + "modelPackage": "com.trevari.events.relay.models", + "apiPackage": "com.trevari.events.relay.api", + "invokerPackage": "com.trevari.events.relay.api", + "useTags" : true, + "skipDefaultInterface" : true, + "dateLibrary": "java8", + "useBeanValidation" : true, + "serializableModel" : true, + "implicitHeaders" : false, + "swaggerDocketConfig": false, + "hideGenerationTimestamp": true, + + "useSpringfox": false, + "reactive": false, + "openApiNullable" : false + +} diff --git a/seoulyeok-message-relay/application/Dockerfile b/seoulyeok-message-relay/application/Dockerfile new file mode 100644 index 0000000..d5b3b2a --- /dev/null +++ b/seoulyeok-message-relay/application/Dockerfile @@ -0,0 +1,9 @@ +FROM amazoncorretto:11-alpine + +ARG FROM_JAR=build/libs/message-relay.jar + +COPY ${FROM_JAR} app.jar + +EXPOSE 5000 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/seoulyeok-message-relay/application/build.gradle b/seoulyeok-message-relay/application/build.gradle new file mode 100644 index 0000000..f69d328 --- /dev/null +++ b/seoulyeok-message-relay/application/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'java' + id "io.freefair.lombok" + id "org.springframework.boot" + id "io.spring.dependency-management" + +} + +group 'org.masil.seoulyeok.events.relay' + +dependencies { + + implementation project(':seoulyeok-message-relay:domain') + implementation project(':seoulyeok-message-relay:persistence') + implementation project(':refs:destination') + implementation project(':refs:event') + + implementation('io.awspring.cloud:spring-cloud-starter-aws-messaging:2.4.1') { + exclude group: 'com.amazonaws', module: 'aws-java-sdk-s3' + } + + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + + // for repository test + testImplementation 'io.zonky.test:embedded-database-spring-test:2.1.0' + testImplementation 'org.mockito:mockito-inline:4.6.1' + testImplementation 'org.awaitility:awaitility:4.2.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' + + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-devtools' + + implementation 'org.springframework.cloud:spring-cloud-config-client:3.1.3' + +} + +test { + useJUnitPlatform() +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java new file mode 100644 index 0000000..5588640 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java @@ -0,0 +1,22 @@ +package org.masil.seoulyeok.events.relay; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class MessageRelayApplication { + + @RestController + public static class HelloController { + @GetMapping(value = {"/annyeng"}) + public String hello() { + return "hello"; + } + } + + public static void main(String[] args) { + SpringApplication.run(MessageRelayApplication.class, args); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java new file mode 100644 index 0000000..76e3f09 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java @@ -0,0 +1,19 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class DestinationFetcher { + + private final LoadDestinationPort loadPort; + + public List findAllActiveDestination() { + return loadPort.loadAllActiveDestination(); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java new file mode 100644 index 0000000..d1fe458 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java @@ -0,0 +1,23 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relay.port.out.CommandDestinationPort; +import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DestinationStatusChanger { + + private final LoadDestinationPort loadPort; + private final CommandDestinationPort commandPort; + + public void inactivateBy(DestinationId destinationId) { + Destination destination = loadPort.loadById(destinationId); + destination.inactive(); + + commandPort.update(destination); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java new file mode 100644 index 0000000..232f494 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java @@ -0,0 +1,18 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.relay.port.out.RelayMessageMarkPort; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RelaidMessageUpdater { + + private final RelayMessageMarkPort messageMarkPort; + + public boolean setMarkRelaidMessage(RelayMessageId relayMessageId, LocalDateTime publish) { + return messageMarkPort.setReliedMark(relayMessageId, publish); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java new file mode 100644 index 0000000..76355eb --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java @@ -0,0 +1,57 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.relay.application.alert.SlackAlertNotifier; +import org.masil.seoulyeok.events.relay.application.config.RelayProcessorConfig; +import org.masil.seoulyeok.events.relay.application.publisher.PublisherContainers; +import lombok.RequiredArgsConstructor; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RelayProcessor { + + private final DestinationFetcher fetcher; + private final TopMessageFinder topMessageFinder; + private final PublisherContainers publisherContainers; + private final RelayProcessorConfig relayProcessorConfig; + private final RelaidMessageUpdater updater; + private final DestinationStatusChanger changer; + private final SlackAlertNotifier notifier; + + public void doProcess() { + List activeDestinations = fetcher.findAllActiveDestination(); + int batchSize = relayProcessorConfig.getBatchSize(); + for (Destination activeDestination : activeDestinations) { + for (int i = 0; i < batchSize; i++) { + TopRelayMessage topRelayMessage = topMessageFinder.find(activeDestination.getId()); + if (TopRelayMessage.EMPTY.equals(topRelayMessage)) { + break; + } + PublishResult result = this.publish(topRelayMessage, activeDestination); + if (!result.isSuccess()) { + handleOnFailure(result); + break; + } + handleOnSuccess(result); + } + } + } + + private void handleOnSuccess(PublishResult result) { + updater.setMarkRelaidMessage(result.getRelayMessageId(), result.getPublishedAt()); + } + + private void handleOnFailure(PublishResult result) { + changer.inactivateBy(result.getDestinationId()); + notifier.doNotify(result.getRelayMessageId(), result.getDestinationId()); + } + + private PublishResult publish(TopRelayMessage topRelayMessage, Destination destination) { + return publisherContainers.publish(topRelayMessage, destination); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java new file mode 100644 index 0000000..12586e4 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java @@ -0,0 +1,19 @@ +package org.masil.seoulyeok.events.relay.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SchedulerClient { + private final RelayProcessor processor; + + @Scheduled(fixedDelay = 5000) + public void execute() { + + processor.doProcess(); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java new file mode 100644 index 0000000..b68735b --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java @@ -0,0 +1,22 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relay.port.out.QueryRelayMessagePort; +import lombok.RequiredArgsConstructor; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TopMessageFinder { + + private final QueryRelayMessagePort queryRelayMessagePort; + + public TopRelayMessage find(DestinationId destinationId) { + if (!queryRelayMessagePort.existsByDestinationId(destinationId)) { + return TopRelayMessage.EMPTY; + } + return queryRelayMessagePort.findTopMessageByDestinationId(destinationId); + } +} + diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java new file mode 100644 index 0000000..df25bca --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java @@ -0,0 +1,38 @@ +package org.masil.seoulyeok.events.relay.application.alert; + +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class FakeSlackNotifier { + + public static final String RELAY_FAIL_MESSAGE = ":exclamation: Message Relay 에 실패하였습니다.\n ```\nmessageId => [%s], destinationId => [%s]```"; + + @Value("${alert.slack.channel}") + private String ALERT_SLACK_CHANNEL; + + private final RestTemplate rest; + + public String doNotify(RelayMessageId messageId, DestinationId destinationId) { + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + Map body = new HashMap<>(); + body.put("text", getAlertMessage(messageId, destinationId)); + HttpEntity> request = new HttpEntity<>(body, headers); + return rest.postForObject(ALERT_SLACK_CHANNEL, request, String.class); + } + + private String getAlertMessage(RelayMessageId messageId, DestinationId destinationId) { + return String.format(RELAY_FAIL_MESSAGE, messageId, destinationId); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SendSlackAlertData.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SendSlackAlertData.java new file mode 100644 index 0000000..1c059fe --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SendSlackAlertData.java @@ -0,0 +1,11 @@ +package org.masil.seoulyeok.events.relay.application.alert; + +import java.util.List; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SendSlackAlertData { + String contents; + List to; + String from; +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SlackAlertNotifier.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SlackAlertNotifier.java new file mode 100644 index 0000000..81810c6 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/SlackAlertNotifier.java @@ -0,0 +1,38 @@ +package org.masil.seoulyeok.events.relay.application.alert; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +@Slf4j +public class SlackAlertNotifier { + + public static final String RELAY_FAIL_MESSAGE = ":exclamation: Message Relay 에 실패하였습니다.\n ```\nmessageId => [%s], destinationId => [%s]```"; + public static final String FROM = "message-relay"; + + @Value("${alert.notifier.url}") + private String NOTIFIER_URL; + + @Value("${alert.slack.channel}") + private String ALERT_SLACK_CHANNEL; + + private final RestTemplate rest; + + public void doNotify(RelayMessageId messageId, DestinationId destinationId) { + SendSlackAlertData request = SendSlackAlertData.of(getAlertMessage(messageId, destinationId), + List.of(ALERT_SLACK_CHANNEL), FROM); + String response = rest.postForObject(NOTIFIER_URL + "/slack", request, String.class); + log.info(response); + } + + private String getAlertMessage(RelayMessageId messageId, DestinationId destinationId) { + return String.format(RELAY_FAIL_MESSAGE, messageId, destinationId); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RelayProcessorConfig.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RelayProcessorConfig.java new file mode 100644 index 0000000..36b0323 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RelayProcessorConfig.java @@ -0,0 +1,14 @@ +package org.masil.seoulyeok.events.relay.application.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RelayProcessorConfig { + + @Value("${relay.batch.size}") + @Getter + private int batchSize; + +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RestTemplateConfig.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RestTemplateConfig.java new file mode 100644 index 0000000..8843936 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration + public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + } diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/SchedulerConfig.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/SchedulerConfig.java new file mode 100644 index 0000000..ef6d1a6 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/config/SchedulerConfig.java @@ -0,0 +1,20 @@ +package org.masil.seoulyeok.events.relay.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + + @Bean + public TaskScheduler poolScheduler() { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2); + threadPoolTaskScheduler.setThreadNamePrefix("trevari-Scheduler-threadpool"); + return threadPoolTaskScheduler; + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainers.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainers.java new file mode 100644 index 0000000..817a7c2 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainers.java @@ -0,0 +1,41 @@ +package org.masil.seoulyeok.events.relay.application.publisher; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import lombok.RequiredArgsConstructor; +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.publisher.PublisherContainer; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +@RequiredArgsConstructor +public class PublisherContainers { + + private static final String NOT_FOUND_MESSAGE = "not exists Publisher Container type of [%s]"; + + private final List containers = new ArrayList<>(); + + public PublishResult publish(TopRelayMessage message, + DestinationTarget destination) { + PublisherContainer container = findByDestinationType(destination.getType()); + return container.publish(message, destination); + } + + public void add(PublisherContainer container) { + containers.add(container); + } + + protected int getContainerSize() { + return containers.size(); + } + + private PublisherContainer findByDestinationType(DestinationType type) { + return containers.stream() + .filter(container -> container.isSupportedType(type)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException(String.format(NOT_FOUND_MESSAGE, type))); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainer.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainer.java new file mode 100644 index 0000000..81ef296 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainer.java @@ -0,0 +1,41 @@ +package org.masil.seoulyeok.events.relay.application.publisher.simplequeue; + +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.publisher.DestinationPublisher; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.publisher.PublisherContainer; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +import java.util.ArrayList; +import java.util.List; + +public class SimpleQueuePublisherContainer implements PublisherContainer { + + private final List publishers = new ArrayList<>(); + + @Override + public PublishResult publish(TopRelayMessage message, DestinationTarget destination) { + DestinationPublisher publisher = find(destination); + //TODO Spec & Condition + return publisher.execute(message, destination); + } + + private DestinationPublisher find(DestinationTarget message) { + return publishers.get(0); + } + + @Override + public void add(DestinationPublisher publisher) { + publishers.add(publisher); + } + + @Override + public boolean isSupportedType(DestinationType type) { + return DestinationType.SIMPLE_QUEUE.equals(type); + } + + protected int getContainerSize() { + return publishers.size(); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonFifoSQSDestinationPublisher.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonFifoSQSDestinationPublisher.java new file mode 100644 index 0000000..f873751 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonFifoSQSDestinationPublisher.java @@ -0,0 +1,51 @@ +package org.masil.seoulyeok.events.relay.application.publisher.simplequeue.aws; + +import com.masil.commons.clocks.Clocks; +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.publisher.DefaultPublishResult; +import org.masil.seoulyeok.events.publisher.DestinationPublisher; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.relaymessage.RelayMessageModel; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.springframework.messaging.MessagingException; + +@RequiredArgsConstructor +@Slf4j +public class AmazonFifoSQSDestinationPublisher implements DestinationPublisher { + + private static final String PUBLISH_FAILED_MESSAGE = "Amazon Fifo SQS publish failed [message => {} , destination => {}, e => {}]"; + private static final String UNEXPECTED_PUBLISH_FAILED_MESSAGE = "unexpected exception occurred [message => {} , destination => {}, e => {}]"; + private final QueueMessagingTemplate queueMessagingTemplate; + + @Override + public PublishResult execute(TopRelayMessage message, DestinationTarget destination) { + try { + log.info("assigned message publish => {} !!", message); + + Map headers = new HashMap<>(); + headers.put("message-group-id", String.valueOf(message.getDestinationId().getValue())); + headers.put("message-deduplication-id", String.valueOf(message.getId().getValue())); + queueMessagingTemplate.convertAndSend(destination.getAddress().getValue(), convert(message), headers); + return DefaultPublishResult.success(message.getId(), message.getDestinationId(), Clocks.now()); + } catch (MessagingException e) { + log.info(PUBLISH_FAILED_MESSAGE, message, destination, e); + return DefaultPublishResult.fail(message.getId(), message.getDestinationId(), Clocks.now()); + } catch (Exception e) { + log.info(UNEXPECTED_PUBLISH_FAILED_MESSAGE, message, destination, e); + return DefaultPublishResult.fail(message.getId(), message.getDestinationId(), Clocks.now()); + } + } + + private RelayMessageModel convert(TopRelayMessage message) { + return RelayMessageModel.of( + message.getId().get(), + message.getMessagePayload().getValue(), + message.getCreateAt().toString(), + message.getPayloadVersion().getValue()); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisher.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisher.java new file mode 100644 index 0000000..3a0879f --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisher.java @@ -0,0 +1,42 @@ +package org.masil.seoulyeok.events.relay.application.publisher.simplequeue.aws; + +import com.masil.commons.clocks.Clocks; +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.publisher.DefaultPublishResult; +import org.masil.seoulyeok.events.publisher.DestinationPublisher; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.relaymessage.RelayMessageModel; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.springframework.messaging.MessagingException; + +@RequiredArgsConstructor +@Slf4j +public class AmazonStandardSQSDestinationPublisher implements DestinationPublisher { + + private static final String PUBLISH_FAILED_MESSAGE = "Amazon SQS publish failed [message => {} , destination => {}]"; + private final QueueMessagingTemplate queueMessagingTemplate; + + @Override + public PublishResult execute(TopRelayMessage message, DestinationTarget destination) { + try { + log.info("assigned message publish => {} !!", message); + queueMessagingTemplate.convertAndSend(destination.getAddress().getValue(), convert(message)); + return DefaultPublishResult.success(message.getId(), message.getDestinationId(), Clocks.now()); + } catch (MessagingException e) { + log.info(PUBLISH_FAILED_MESSAGE, message, destination); + return DefaultPublishResult.fail(message.getId(), message.getDestinationId(), Clocks.now()); + } + } + + private RelayMessageModel convert(TopRelayMessage message) { + return RelayMessageModel.of( + message.getId().get(), + message.getMessagePayload().getValue(), + message.getCreateAt().toString(), + message.getPayloadVersion().getValue()); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainer.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainer.java new file mode 100644 index 0000000..24a11cf --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainer.java @@ -0,0 +1,43 @@ +package org.masil.seoulyeok.events.relay.application.publisher.subscribequeue; + + +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.publisher.DestinationPublisher; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.publisher.PublisherContainer; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +import java.util.ArrayList; +import java.util.List; + +public class SubscribeQueuePublisherContainer implements PublisherContainer { + + private final List publishers = new ArrayList<>(); + + @Override + public PublishResult publish(TopRelayMessage message, DestinationTarget destination) { + DestinationPublisher publisher = find(); + //TODO Spec & Condition + return publisher.execute(message, destination); + } + + @Override + public void add(DestinationPublisher publisher) { + publishers.add(publisher); + } + + @Override + public boolean isSupportedType(DestinationType type) { + return DestinationType.SUBSCRIBER_QUEUE.equals(type); + } + + protected int getContainerSize() { + return publishers.size(); + } + + private DestinationPublisher find() { + // TODO change, 현재는 한가지 type (sqs) 만 존재하므로 하드코딩됨 + return publishers.get(0); + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/UnSupportedSubscribeQueuePublisher.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/UnSupportedSubscribeQueuePublisher.java new file mode 100644 index 0000000..5fe4f72 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/UnSupportedSubscribeQueuePublisher.java @@ -0,0 +1,16 @@ +package org.masil.seoulyeok.events.relay.application.publisher.subscribequeue; + +import lombok.extern.slf4j.Slf4j; +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.publisher.DestinationPublisher; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +@Slf4j +public class UnSupportedSubscribeQueuePublisher implements DestinationPublisher { + @Override + public PublishResult execute(TopRelayMessage message, DestinationTarget destination) { + log.info("unsupported publisher executed, message => {}, destination Target => {}", message, destination); + return null; + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/config/PublisherBeanConfig.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/config/PublisherBeanConfig.java new file mode 100644 index 0000000..f241671 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/config/PublisherBeanConfig.java @@ -0,0 +1,52 @@ +package org.masil.seoulyeok.events.relay.config; + +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.masil.seoulyeok.events.relay.application.publisher.PublisherContainers; +import org.masil.seoulyeok.events.relay.application.publisher.simplequeue.SimpleQueuePublisherContainer; +import org.masil.seoulyeok.events.relay.application.publisher.simplequeue.aws.AmazonFifoSQSDestinationPublisher; +import org.masil.seoulyeok.events.relay.application.publisher.subscribequeue.SubscribeQueuePublisherContainer; +import org.masil.seoulyeok.events.relay.application.publisher.subscribequeue.UnSupportedSubscribeQueuePublisher; +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class PublisherBeanConfig { + + @Bean + public QueueMessagingTemplate messagingTemplate() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + return new QueueMessagingTemplate(AmazonSQSAsyncClientBuilder.defaultClient(), null, objectMapper); + } + + + @Bean + public PublisherContainers publisherContainers(QueueMessagingTemplate queueMessagingTemplate) { + PublisherContainers containers = new PublisherContainers(); + + containers.add(getSimpleQueuePublisherContainer(queueMessagingTemplate)); + containers.add(getSubscribeQueuePublisherContainer()); + + return containers; + } + + private SimpleQueuePublisherContainer getSimpleQueuePublisherContainer( + QueueMessagingTemplate queueMessagingTemplate) { + + SimpleQueuePublisherContainer container = new SimpleQueuePublisherContainer(); + container.add(new AmazonFifoSQSDestinationPublisher(queueMessagingTemplate)); + + return container; + } + + private SubscribeQueuePublisherContainer getSubscribeQueuePublisherContainer() { + SubscribeQueuePublisherContainer container = new SubscribeQueuePublisherContainer(); + // TODO add SNS or Kafka ... + container.add(new UnSupportedSubscribeQueuePublisher()); + return container; + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/DashBoardController.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/DashBoardController.java new file mode 100644 index 0000000..0ace71b --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/DashBoardController.java @@ -0,0 +1,42 @@ +package org.masil.seoulyeok.events.relay.web; + +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relay.port.out.LoadViewDestinationPort; +import org.masil.seoulyeok.events.relay.port.out.LoadViewRelayMessagePort; +import org.masil.seoulyeok.events.relay.web.model.DestinationView; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Controller +@RequiredArgsConstructor +public class DashBoardController { + + private final LoadViewDestinationPort loadWebViewDestinationPort; + private final LoadViewRelayMessagePort loadViewRelayMessagePort; + private DestinationId id; + + @GetMapping(value = "/dashboard") + public ModelAndView games() { + List destinations = loadWebViewDestinationPort.findAllDestination(); + + List destinationViews = destinations.stream() + .map(destination -> { + id = destination.getId(); + long leg = loadViewRelayMessagePort.getLegByDestinationId(id); + LocalDateTime latestReliedMessage = loadViewRelayMessagePort.getLatestReliedMessageBy(id); + return DestinationView.of(id.getValue(), destination.getAddress().getValue(), destination.getType(), destination.getStatus(), leg, latestReliedMessage); + }).collect(Collectors.toUnmodifiableList()); + + ModelAndView modelAndView = new ModelAndView(); + modelAndView.setViewName("dashboard"); + modelAndView.addObject("destinationViews", destinationViews); + return modelAndView; + } +} diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/model/DestinationView.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/model/DestinationView.java new file mode 100644 index 0000000..477981a --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/web/model/DestinationView.java @@ -0,0 +1,18 @@ +package org.masil.seoulyeok.events.relay.web.model; + +import lombok.Value; +import org.masil.seoulyeok.events.destination.DestinationStatus; +import org.masil.seoulyeok.events.destination.DestinationType; + +import java.time.LocalDateTime; + +@Value(staticConstructor = "of") +public class DestinationView { + + Long id; + String address; + DestinationType type; + DestinationStatus status; + Long lag; + LocalDateTime latestReliedAt; +} diff --git a/seoulyeok-message-relay/application/src/main/resources/application-local.yml b/seoulyeok-message-relay/application/src/main/resources/application-local.yml new file mode 100644 index 0000000..8b91e21 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/application-local.yml @@ -0,0 +1,40 @@ +server: + port: 8073 +spring: + cloud: + config: + enabled: false + datasource: + url: jdbc:postgresql://localhost:6543/message-relay + username: message-relay + password: dpdlxlatm!3@4 + web: + resources: + static-locations: classpath:/static + devtools: + livereload: + enabled: true + +cloud: + aws: + region: + static: ap-northeast-2 + stack: + auto: false + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + +id-generator: + url: "https://not-exists-idgen-url.com" + +alert: + notifier: + url: http://localhost:8929/apis + slack: + channel: C02LDAM3A76 + + +relay: + batch: + size: 100 diff --git a/seoulyeok-message-relay/application/src/main/resources/application-production.yml b/seoulyeok-message-relay/application/src/main/resources/application-production.yml new file mode 100644 index 0000000..e3473cd --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/application-production.yml @@ -0,0 +1,5 @@ +spring: + application: + name: message-relay + config: + import: "optional:configserver:https://config.trevari.co.kr" \ No newline at end of file diff --git a/seoulyeok-message-relay/application/src/main/resources/application.yml b/seoulyeok-message-relay/application/src/main/resources/application.yml new file mode 100644 index 0000000..d5c85e8 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + profiles: + active: local + + diff --git a/seoulyeok-message-relay/application/src/main/resources/static/assets/favicon.ico b/seoulyeok-message-relay/application/src/main/resources/static/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9356735ca323587d5730a74bd017d9cb2153479b GIT binary patch literal 23462 zcmeI2Ym8l0701uz4!r`E5nmsKGBw!J7NUL^oNvCW6eRJ5T2QE1Vib%Li7*|q0n*&DMgTO~-9{XjsE?EY|;o%Gf&I*P@A7F%Gk z1r~A(tWf385-t_C2zLk%3S+|mde?_^MUNdn{E3_Ia-1r7u5hEUM`+Ig#NQyCE5JJ+ zq^?S=6K)m8g_*RE7Yi@^X9zs+aFZoU_|wAUf>+0W=pWPbJ|!F}91ikPCHaidkG6>A zZ0Y%0Asw>9nEM+cwmx&=cenCcbBMLrM&;A5@o1)P`jT?H@DOOPLFMpMA*DV~$^RPR zfyB7{8TTvh+rqBI_?zW_tZ*>o1U>W-A+JN|`m+447fu&y!g+${zfZP{1+JdYCuM^N zK6v5xe2e;r<#)U=m$F(9ds-;wVU?-F_kAf3|nsE*~qWO@Y`y%a+gg11j$b_>Rg}7IM2=@N&xa-I}aI>p0PI1xp*b6oIi7z=l@^X$fqoPn;Qi9iu~nrOZI+!kBsA0 zK zP@rt&RkcQje>Q~iGPKUTPxh*I;`@C;u5mXX!k(DDo)ym-j@Ku1PV)PM6Ave2 z;>p5H#oHpu zIJ2{Oo|FElNDlhOzZ@Rc8ZpAW!~ALGg6=~xB!VF@EHkk ztt$A1imWHRd|I;Q>$?y~*>-^x9%oZF)-f^St-tpx*jgWjIGOW^v+|-HV`aM>Ee>>5 z^Gx{}p|O;GOYA#@_X)wui*fKk1fHKeJgohO5_nj*Soy#cGk;o-dV`b`4sYz73NPO?--wmp zEjUC7b?`VsdkTyCW+|;)SnJG0{-|J!_ohVB>m44x&zRqbnBS~C(48j43S+;wH~B^^ ziQ{**^j4009e+>#PKO8X^iemPq_j5SMBhzwu$G~U|by@r2C#d)-q=k8FDZ0w1wICSr+h%KAH zzY&|$NxJH|2>X>Nd0$*>(W5bkDZv?0x_QHNr!8L7m%p; z&K(|4nqN)Qf0ocuP#%ASbgEIsSPFn`y8%*uiH6ePL-Vsu<9LJx1a6AFdrO~n1Ht* z{AD7K=YG2kR{dCSrbuW%cvgUDHqRNptmjyDIgmYwmGUl!{}RhNpbMRatqjC*e&wBI zZVtp*rVmJIwH4=AseQD}PhZ&Y@Kg5`VN9^{+!I0Kor`krL|ev6=v?$&u_TUPs4pyc zVk|ZF@#df#t$>m)bl(1sl5glX)JHdk0wZjk(OP|BZ$xa-<#dM!?(>R{PMQBB;jN7r zuT#YP9fvt`CbC1pR$I;H?>PLP6(gbF2TzP4==p?xA3QFBr|8Q6hhxmM+UoqGV?}wH z>|UR^wIa4?=IqqU1>Lnp$BOdm08dK2ijVQypWNP3bgU>}C%ae2brrEib6q!&Ib6I8 z))p}m8awZbAuxRIkC#j;fiYk)@uuP)oG2qNtC9d8`uxxW{Vs^EEz?Ee<5JZbMED&m=I z7`v_b7o_Jm`;Z(hBWpS<9v$_I{f6Zll+VfPJqP`-g{t;qt>JO3mn|e0$mqp>D-AZCkF_#O- zl|o*(^7UESigO?R{kwv<7cR!rpXREa^r()IU)A`P=SUs42&V}(fo~+Ym9LD~pBKD& z0RQy@Jn+FA(?-NGJ{8}aZs$xG?|&wg>l?a%LjHV*I}+n^PQKHtg(nl^neX~Fj*B{4 zSbv4qt9d;S#+AG-imu7rI5LMZo*#_%@SfRJKBowWqA-ShT=<9Jtbxtpa}dvo>Fp#THm>fd$wC?Oa+~5asg9 z@EI{du4{2TCXg30$T8oQXcM<2_?8C{Y0nS=#z(pgHa1Xe)jH)?y>+aHuui$zXbtJ7=MiF?gUzArif(yys5v}3k`1HJhDZ4`)G3dQ zjx>jxlqa(`<<_9e6ScPZhQ=CG<#Ed6^>(d2HZ)$R+?Z5(aH3ou8=S1S#|I~C^1(d4 ztRU%IL+BgrUb)epXtcAgeyThyrT&hhFP5{Opr5UBk-u0T>gMo@+zR=3>)5T=^!iS< zgV$E$b=t7oo@x==ywlFb_AkDd$!zLl5*M^Ak(-LFa%-GcrtmH!WhNMF4G literal 0 HcmV?d00001 diff --git a/seoulyeok-message-relay/application/src/main/resources/static/css/styles.css b/seoulyeok-message-relay/application/src/main/resources/static/css/styles.css new file mode 100644 index 0000000..302c84f --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/static/css/styles.css @@ -0,0 +1,11743 @@ +@charset "UTF-8"; +/*! +* Start Bootstrap - Heroic Features v5.0.1 (https://startbootstrap.com/template/heroic-features) +* Copyright 2013-2021 Start Bootstrap +* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-heroic-features/blob/master/LICENSE) +*/ +/*! + * Bootstrap v5.0.1 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-font-sans-serif); + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #fff; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + background-color: currentColor; + border: 0; + opacity: 0.25; +} + +hr:not([size]) { + height: 1px; +} + +h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; +} + +h1, .h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1, .h1 { + font-size: 2.5rem; + } +} + +h2, .h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2, .h2 { + font-size: 2rem; + } +} + +h3, .h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3, .h3 { + font-size: 1.75rem; + } +} + +h4, .h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4, .h4 { + font-size: 1.5rem; + } +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-bs-original-title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small, .small { + font-size: 0.875em; +} + +mark, .mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: #0d6efd; + text-decoration: underline; +} +a:hover { + color: #0a58ca; +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; + direction: ltr /* rtl:ignore */; + unicode-bidi: bidi-override; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: #d63384; + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 0.875em; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} +kbd kbd { + padding: 0; + font-size: 1em; + font-weight: 700; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: #6c757d; + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]::-webkit-calendar-picker-indicator { + display: none; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: calc(1.275rem + 0.3vw); + line-height: inherit; +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + outline-offset: -2px; + -webkit-appearance: textfield; +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::file-selector-button { + font: inherit; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: calc(1.625rem + 4.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-1 { + font-size: 5rem; + } +} + +.display-2 { + font-size: calc(1.575rem + 3.9vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-2 { + font-size: 4.5rem; + } +} + +.display-3 { + font-size: calc(1.525rem + 3.3vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-3 { + font-size: 4rem; + } +} + +.display-4 { + font-size: calc(1.475rem + 2.7vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-4 { + font-size: 3.5rem; + } +} + +.display-5 { + font-size: calc(1.425rem + 2.1vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-5 { + font-size: 3rem; + } +} + +.display-6 { + font-size: calc(1.375rem + 1.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-6 { + font-size: 2.5rem; + } +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 0.875em; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} +.blockquote > :last-child { + margin-bottom: 0; +} + +.blockquote-footer { + margin-top: -1rem; + margin-bottom: 1rem; + font-size: 0.875em; + color: #6c757d; +} +.blockquote-footer::before { + content: "— "; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 0.875em; + color: #6c757d; +} + +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + width: 100%; + padding-right: var(--bs-gutter-x, 0.75rem); + padding-left: var(--bs-gutter-x, 0.75rem); + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(var(--bs-gutter-y) * -1); + margin-right: calc(var(--bs-gutter-x) / -2); + margin-left: calc(var(--bs-gutter-x) / -2); +} +.row > * { + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) / 2); + padding-left: calc(var(--bs-gutter-x) / 2); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0%; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.3333333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.6666666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.3333333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.6666666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.3333333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.6666666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.3333333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.6666666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.3333333333%; +} + +.offset-2 { + margin-left: 16.6666666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.3333333333%; +} + +.offset-5 { + margin-left: 41.6666666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.3333333333%; +} + +.offset-8 { + margin-left: 66.6666666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.3333333333%; +} + +.offset-11 { + margin-left: 91.6666666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + + .col-sm-1 { + flex: 0 0 auto; + width: 8.3333333333%; + } + + .col-sm-2 { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-sm-4 { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .col-sm-5 { + flex: 0 0 auto; + width: 41.6666666667%; + } + + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-sm-7 { + flex: 0 0 auto; + width: 58.3333333333%; + } + + .col-sm-8 { + flex: 0 0 auto; + width: 66.6666666667%; + } + + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-sm-10 { + flex: 0 0 auto; + width: 83.3333333333%; + } + + .col-sm-11 { + flex: 0 0 auto; + width: 91.6666666667%; + } + + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-sm-0 { + margin-left: 0; + } + + .offset-sm-1 { + margin-left: 8.3333333333%; + } + + .offset-sm-2 { + margin-left: 16.6666666667%; + } + + .offset-sm-3 { + margin-left: 25%; + } + + .offset-sm-4 { + margin-left: 33.3333333333%; + } + + .offset-sm-5 { + margin-left: 41.6666666667%; + } + + .offset-sm-6 { + margin-left: 50%; + } + + .offset-sm-7 { + margin-left: 58.3333333333%; + } + + .offset-sm-8 { + margin-left: 66.6666666667%; + } + + .offset-sm-9 { + margin-left: 75%; + } + + .offset-sm-10 { + margin-left: 83.3333333333%; + } + + .offset-sm-11 { + margin-left: 91.6666666667%; + } + + .g-sm-0, +.gx-sm-0 { + --bs-gutter-x: 0; + } + + .g-sm-0, +.gy-sm-0 { + --bs-gutter-y: 0; + } + + .g-sm-1, +.gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + + .g-sm-1, +.gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + + .g-sm-2, +.gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + + .g-sm-2, +.gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + + .g-sm-3, +.gx-sm-3 { + --bs-gutter-x: 1rem; + } + + .g-sm-3, +.gy-sm-3 { + --bs-gutter-y: 1rem; + } + + .g-sm-4, +.gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + + .g-sm-4, +.gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + + .g-sm-5, +.gx-sm-5 { + --bs-gutter-x: 3rem; + } + + .g-sm-5, +.gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + + .col-md-1 { + flex: 0 0 auto; + width: 8.3333333333%; + } + + .col-md-2 { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-md-4 { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .col-md-5 { + flex: 0 0 auto; + width: 41.6666666667%; + } + + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-md-7 { + flex: 0 0 auto; + width: 58.3333333333%; + } + + .col-md-8 { + flex: 0 0 auto; + width: 66.6666666667%; + } + + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-md-10 { + flex: 0 0 auto; + width: 83.3333333333%; + } + + .col-md-11 { + flex: 0 0 auto; + width: 91.6666666667%; + } + + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-md-0 { + margin-left: 0; + } + + .offset-md-1 { + margin-left: 8.3333333333%; + } + + .offset-md-2 { + margin-left: 16.6666666667%; + } + + .offset-md-3 { + margin-left: 25%; + } + + .offset-md-4 { + margin-left: 33.3333333333%; + } + + .offset-md-5 { + margin-left: 41.6666666667%; + } + + .offset-md-6 { + margin-left: 50%; + } + + .offset-md-7 { + margin-left: 58.3333333333%; + } + + .offset-md-8 { + margin-left: 66.6666666667%; + } + + .offset-md-9 { + margin-left: 75%; + } + + .offset-md-10 { + margin-left: 83.3333333333%; + } + + .offset-md-11 { + margin-left: 91.6666666667%; + } + + .g-md-0, +.gx-md-0 { + --bs-gutter-x: 0; + } + + .g-md-0, +.gy-md-0 { + --bs-gutter-y: 0; + } + + .g-md-1, +.gx-md-1 { + --bs-gutter-x: 0.25rem; + } + + .g-md-1, +.gy-md-1 { + --bs-gutter-y: 0.25rem; + } + + .g-md-2, +.gx-md-2 { + --bs-gutter-x: 0.5rem; + } + + .g-md-2, +.gy-md-2 { + --bs-gutter-y: 0.5rem; + } + + .g-md-3, +.gx-md-3 { + --bs-gutter-x: 1rem; + } + + .g-md-3, +.gy-md-3 { + --bs-gutter-y: 1rem; + } + + .g-md-4, +.gx-md-4 { + --bs-gutter-x: 1.5rem; + } + + .g-md-4, +.gy-md-4 { + --bs-gutter-y: 1.5rem; + } + + .g-md-5, +.gx-md-5 { + --bs-gutter-x: 3rem; + } + + .g-md-5, +.gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + + .col-lg-1 { + flex: 0 0 auto; + width: 8.3333333333%; + } + + .col-lg-2 { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-lg-4 { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .col-lg-5 { + flex: 0 0 auto; + width: 41.6666666667%; + } + + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-lg-7 { + flex: 0 0 auto; + width: 58.3333333333%; + } + + .col-lg-8 { + flex: 0 0 auto; + width: 66.6666666667%; + } + + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-lg-10 { + flex: 0 0 auto; + width: 83.3333333333%; + } + + .col-lg-11 { + flex: 0 0 auto; + width: 91.6666666667%; + } + + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-lg-0 { + margin-left: 0; + } + + .offset-lg-1 { + margin-left: 8.3333333333%; + } + + .offset-lg-2 { + margin-left: 16.6666666667%; + } + + .offset-lg-3 { + margin-left: 25%; + } + + .offset-lg-4 { + margin-left: 33.3333333333%; + } + + .offset-lg-5 { + margin-left: 41.6666666667%; + } + + .offset-lg-6 { + margin-left: 50%; + } + + .offset-lg-7 { + margin-left: 58.3333333333%; + } + + .offset-lg-8 { + margin-left: 66.6666666667%; + } + + .offset-lg-9 { + margin-left: 75%; + } + + .offset-lg-10 { + margin-left: 83.3333333333%; + } + + .offset-lg-11 { + margin-left: 91.6666666667%; + } + + .g-lg-0, +.gx-lg-0 { + --bs-gutter-x: 0; + } + + .g-lg-0, +.gy-lg-0 { + --bs-gutter-y: 0; + } + + .g-lg-1, +.gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + + .g-lg-1, +.gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + + .g-lg-2, +.gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + + .g-lg-2, +.gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + + .g-lg-3, +.gx-lg-3 { + --bs-gutter-x: 1rem; + } + + .g-lg-3, +.gy-lg-3 { + --bs-gutter-y: 1rem; + } + + .g-lg-4, +.gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + + .g-lg-4, +.gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + + .g-lg-5, +.gx-lg-5 { + --bs-gutter-x: 3rem; + } + + .g-lg-5, +.gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xl-1 { + flex: 0 0 auto; + width: 8.3333333333%; + } + + .col-xl-2 { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xl-4 { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .col-xl-5 { + flex: 0 0 auto; + width: 41.6666666667%; + } + + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xl-7 { + flex: 0 0 auto; + width: 58.3333333333%; + } + + .col-xl-8 { + flex: 0 0 auto; + width: 66.6666666667%; + } + + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xl-10 { + flex: 0 0 auto; + width: 83.3333333333%; + } + + .col-xl-11 { + flex: 0 0 auto; + width: 91.6666666667%; + } + + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xl-0 { + margin-left: 0; + } + + .offset-xl-1 { + margin-left: 8.3333333333%; + } + + .offset-xl-2 { + margin-left: 16.6666666667%; + } + + .offset-xl-3 { + margin-left: 25%; + } + + .offset-xl-4 { + margin-left: 33.3333333333%; + } + + .offset-xl-5 { + margin-left: 41.6666666667%; + } + + .offset-xl-6 { + margin-left: 50%; + } + + .offset-xl-7 { + margin-left: 58.3333333333%; + } + + .offset-xl-8 { + margin-left: 66.6666666667%; + } + + .offset-xl-9 { + margin-left: 75%; + } + + .offset-xl-10 { + margin-left: 83.3333333333%; + } + + .offset-xl-11 { + margin-left: 91.6666666667%; + } + + .g-xl-0, +.gx-xl-0 { + --bs-gutter-x: 0; + } + + .g-xl-0, +.gy-xl-0 { + --bs-gutter-y: 0; + } + + .g-xl-1, +.gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xl-1, +.gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xl-2, +.gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xl-2, +.gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xl-3, +.gx-xl-3 { + --bs-gutter-x: 1rem; + } + + .g-xl-3, +.gy-xl-3 { + --bs-gutter-y: 1rem; + } + + .g-xl-4, +.gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xl-4, +.gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xl-5, +.gx-xl-5 { + --bs-gutter-x: 3rem; + } + + .g-xl-5, +.gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + + .col-xxl-1 { + flex: 0 0 auto; + width: 8.3333333333%; + } + + .col-xxl-2 { + flex: 0 0 auto; + width: 16.6666666667%; + } + + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + + .col-xxl-4 { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .col-xxl-5 { + flex: 0 0 auto; + width: 41.6666666667%; + } + + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + + .col-xxl-7 { + flex: 0 0 auto; + width: 58.3333333333%; + } + + .col-xxl-8 { + flex: 0 0 auto; + width: 66.6666666667%; + } + + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + + .col-xxl-10 { + flex: 0 0 auto; + width: 83.3333333333%; + } + + .col-xxl-11 { + flex: 0 0 auto; + width: 91.6666666667%; + } + + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + + .offset-xxl-0 { + margin-left: 0; + } + + .offset-xxl-1 { + margin-left: 8.3333333333%; + } + + .offset-xxl-2 { + margin-left: 16.6666666667%; + } + + .offset-xxl-3 { + margin-left: 25%; + } + + .offset-xxl-4 { + margin-left: 33.3333333333%; + } + + .offset-xxl-5 { + margin-left: 41.6666666667%; + } + + .offset-xxl-6 { + margin-left: 50%; + } + + .offset-xxl-7 { + margin-left: 58.3333333333%; + } + + .offset-xxl-8 { + margin-left: 66.6666666667%; + } + + .offset-xxl-9 { + margin-left: 75%; + } + + .offset-xxl-10 { + margin-left: 83.3333333333%; + } + + .offset-xxl-11 { + margin-left: 91.6666666667%; + } + + .g-xxl-0, +.gx-xxl-0 { + --bs-gutter-x: 0; + } + + .g-xxl-0, +.gy-xxl-0 { + --bs-gutter-y: 0; + } + + .g-xxl-1, +.gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + + .g-xxl-1, +.gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + + .g-xxl-2, +.gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + + .g-xxl-2, +.gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + + .g-xxl-3, +.gx-xxl-3 { + --bs-gutter-x: 1rem; + } + + .g-xxl-3, +.gy-xxl-3 { + --bs-gutter-y: 1rem; + } + + .g-xxl-4, +.gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + + .g-xxl-4, +.gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + + .g-xxl-5, +.gx-xxl-5 { + --bs-gutter-x: 3rem; + } + + .g-xxl-5, +.gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.table { + --bs-table-bg: transparent; + --bs-table-accent-bg: transparent; + --bs-table-striped-color: #212529; + --bs-table-striped-bg: rgba(0, 0, 0, 0.05); + --bs-table-active-color: #212529; + --bs-table-active-bg: rgba(0, 0, 0, 0.1); + --bs-table-hover-color: #212529; + --bs-table-hover-bg: rgba(0, 0, 0, 0.075); + width: 100%; + margin-bottom: 1rem; + color: #212529; + vertical-align: top; + border-color: #dee2e6; +} +.table > :not(caption) > * > * { + padding: 0.5rem 0.5rem; + background-color: var(--bs-table-bg); + border-bottom-width: 1px; + box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg); +} +.table > tbody { + vertical-align: inherit; +} +.table > thead { + vertical-align: bottom; +} +.table > :not(:last-child) > :last-child > * { + border-bottom-color: currentColor; +} + +.caption-top { + caption-side: top; +} + +.table-sm > :not(caption) > * > * { + padding: 0.25rem 0.25rem; +} + +.table-bordered > :not(caption) > * { + border-width: 1px 0; +} +.table-bordered > :not(caption) > * > * { + border-width: 0 1px; +} + +.table-borderless > :not(caption) > * > * { + border-bottom-width: 0; +} + +.table-striped > tbody > tr:nth-of-type(odd) { + --bs-table-accent-bg: var(--bs-table-striped-bg); + color: var(--bs-table-striped-color); +} + +.table-active { + --bs-table-accent-bg: var(--bs-table-active-bg); + color: var(--bs-table-active-color); +} + +.table-hover > tbody > tr:hover { + --bs-table-accent-bg: var(--bs-table-hover-bg); + color: var(--bs-table-hover-color); +} + +.table-primary { + --bs-table-bg: #cfe2ff; + --bs-table-striped-bg: #c5d7f2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bacbe6; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfd1ec; + --bs-table-hover-color: #000; + color: #000; + border-color: #bacbe6; +} + +.table-secondary { + --bs-table-bg: #e2e3e5; + --bs-table-striped-bg: #d7d8da; + --bs-table-striped-color: #000; + --bs-table-active-bg: #cbccce; + --bs-table-active-color: #000; + --bs-table-hover-bg: #d1d2d4; + --bs-table-hover-color: #000; + color: #000; + border-color: #cbccce; +} + +.table-success { + --bs-table-bg: #d1e7dd; + --bs-table-striped-bg: #c7dbd2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bcd0c7; + --bs-table-active-color: #000; + --bs-table-hover-bg: #c1d6cc; + --bs-table-hover-color: #000; + color: #000; + border-color: #bcd0c7; +} + +.table-info { + --bs-table-bg: #cff4fc; + --bs-table-striped-bg: #c5e8ef; + --bs-table-striped-color: #000; + --bs-table-active-bg: #badce3; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfe2e9; + --bs-table-hover-color: #000; + color: #000; + border-color: #badce3; +} + +.table-warning { + --bs-table-bg: #fff3cd; + --bs-table-striped-bg: #f2e7c3; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e6dbb9; + --bs-table-active-color: #000; + --bs-table-hover-bg: #ece1be; + --bs-table-hover-color: #000; + color: #000; + border-color: #e6dbb9; +} + +.table-danger { + --bs-table-bg: #f8d7da; + --bs-table-striped-bg: #eccccf; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfc2c4; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5c7ca; + --bs-table-hover-color: #000; + color: #000; + border-color: #dfc2c4; +} + +.table-light { + --bs-table-bg: #f8f9fa; + --bs-table-striped-bg: #ecedee; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfe0e1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5e6e7; + --bs-table-hover-color: #000; + color: #000; + border-color: #dfe0e1; +} + +.table-dark { + --bs-table-bg: #212529; + --bs-table-striped-bg: #2c3034; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #373b3e; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #323539; + --bs-table-hover-color: #fff; + color: #fff; + border-color: #373b3e; +} + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 767.98px) { + .table-responsive-md { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 991.98px) { + .table-responsive-lg { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1199.98px) { + .table-responsive-xl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1399.98px) { + .table-responsive-xxl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +.form-label { + margin-bottom: 0.5rem; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; +} + +.form-text { + margin-top: 0.25rem; + font-size: 0.875em; + color: #6c757d; +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control[type=file] { + overflow: hidden; +} +.form-control[type=file]:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control:focus { + color: #212529; + background-color: #fff; + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-control::-webkit-date-and-time-value { + height: 1.5em; +} +.form-control::-moz-placeholder { + color: #6c757d; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #6c757d; + opacity: 1; +} +.form-control::placeholder { + color: #6c757d; + opacity: 1; +} +.form-control:disabled, .form-control[readonly] { + background-color: #e9ecef; + opacity: 1; +} +.form-control::file-selector-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: #212529; + background-color: #e9ecef; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control::file-selector-button { + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: #dde0e3; +} +.form-control::-webkit-file-upload-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: #212529; + background-color: #e9ecef; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control::-webkit-file-upload-button { + -webkit-transition: none; + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { + background-color: #dde0e3; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + line-height: 1.5; + color: #212529; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + min-height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} +.form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} +.form-control-sm::-webkit-file-upload-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} + +.form-control-lg { + min-height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} +.form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} +.form-control-lg::-webkit-file-upload-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} + +textarea.form-control { + min-height: calc(1.5em + 0.75rem + 2px); +} +textarea.form-control-sm { + min-height: calc(1.5em + 0.5rem + 2px); +} +textarea.form-control-lg { + min-height: calc(1.5em + 1rem + 2px); +} + +.form-control-color { + max-width: 3rem; + height: auto; + padding: 0.375rem; +} +.form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control-color::-moz-color-swatch { + height: 1.5em; + border-radius: 0.25rem; +} +.form-control-color::-webkit-color-swatch { + height: 1.5em; + border-radius: 0.25rem; +} + +.form-select { + display: block; + width: 100%; + padding: 0.375rem 2.25rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #fff; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.form-select:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; +} +.form-select:disabled { + background-color: #e9ecef; +} +.form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #212529; +} + +.form-select-sm { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.form-select-lg { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.form-check { + display: block; + min-height: 1.5rem; + padding-left: 1.5em; + margin-bottom: 0.125rem; +} +.form-check .form-check-input { + float: left; + margin-left: -1.5em; +} + +.form-check-input { + width: 1em; + height: 1em; + margin-top: 0.25em; + vertical-align: top; + background-color: #fff; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: 1px solid rgba(0, 0, 0, 0.25); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-print-color-adjust: exact; + color-adjust: exact; +} +.form-check-input[type=checkbox] { + border-radius: 0.25em; +} +.form-check-input[type=radio] { + border-radius: 50%; +} +.form-check-input:active { + filter: brightness(90%); +} +.form-check-input:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-check-input:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} +.form-check-input:checked[type=checkbox] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); +} +.form-check-input:checked[type=radio] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-check-input[type=checkbox]:indeterminate { + background-color: #0d6efd; + border-color: #0d6efd; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); +} +.form-check-input:disabled { + pointer-events: none; + filter: none; + opacity: 0.5; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + opacity: 0.5; +} + +.form-switch { + padding-left: 2.5em; +} +.form-switch .form-check-input { + width: 2em; + margin-left: -2.5em; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + background-position: left center; + border-radius: 2em; + transition: background-position 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-switch .form-check-input { + transition: none; + } +} +.form-switch .form-check-input:focus { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e"); +} +.form-switch .form-check-input:checked { + background-position: right center; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} + +.form-check-inline { + display: inline-block; + margin-right: 1rem; +} + +.btn-check { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + filter: none; + opacity: 0.65; +} + +.form-range { + width: 100%; + height: 1.5rem; + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.form-range:focus { + outline: 0; +} +.form-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range::-moz-focus-outer { + border: 0; +} +.form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; + } +} +.form-range::-webkit-slider-thumb:active { + background-color: #b6d4fe; +} +.form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -moz-appearance: none; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } +} +.form-range::-moz-range-thumb:active { + background-color: #b6d4fe; +} +.form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.form-range:disabled { + pointer-events: none; +} +.form-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; +} +.form-range:disabled::-moz-range-thumb { + background-color: #adb5bd; +} + +.form-floating { + position: relative; +} +.form-floating > .form-control, +.form-floating > .form-select { + height: calc(3.5rem + 2px); + padding: 1rem 0.75rem; +} +.form-floating > label { + position: absolute; + top: 0; + left: 0; + height: 100%; + padding: 1rem 0.75rem; + pointer-events: none; + border: 1px solid transparent; + transform-origin: 0 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-floating > label { + transition: none; + } +} +.form-floating > .form-control::-moz-placeholder { + color: transparent; +} +.form-floating > .form-control:-ms-input-placeholder { + color: transparent; +} +.form-floating > .form-control::placeholder { + color: transparent; +} +.form-floating > .form-control:not(:-moz-placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:not(:-ms-input-placeholder) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:not(:-ms-input-placeholder) ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-select ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:-webkit-autofill ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} +.input-group > .form-control, +.input-group > .form-select { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} +.input-group > .form-control:focus, +.input-group > .form-select:focus { + z-index: 3; +} +.input-group .btn { + position: relative; + z-index: 2; +} +.input-group .btn:focus { + z-index: 3; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: center; + white-space: nowrap; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group-lg > .form-control, +.input-group-lg > .form-select, +.input-group-lg > .input-group-text, +.input-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control, +.input-group-sm > .form-select, +.input-group-sm > .input-group-text, +.input-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.input-group-lg > .form-select, +.input-group-sm > .form-select { + padding-right: 3rem; +} + +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #198754; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: rgba(25, 135, 84, 0.9); + border-radius: 0.25rem; +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #198754; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #198754; + box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:valid, .form-select.is-valid { + border-color: #198754; +} +.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + padding-right: 4.125rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: #198754; + box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25); +} + +.was-validated .form-check-input:valid, .form-check-input.is-valid { + border-color: #198754; +} +.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: #198754; +} +.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25); +} +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #198754; +} + +.form-check-inline .form-check-input ~ .valid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, +.was-validated .input-group .form-select:valid, +.input-group .form-select.is-valid { + z-index: 1; +} +.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, +.was-validated .input-group .form-select:valid:focus, +.input-group .form-select.is-valid:focus { + z-index: 3; +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:invalid, .form-select.is-invalid { + border-color: #dc3545; +} +.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + padding-right: 4.125rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-check-input:invalid, .form-check-input.is-invalid { + border-color: #dc3545; +} +.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: #dc3545; +} +.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25); +} +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.form-check-inline .form-check-input ~ .invalid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, +.was-validated .input-group .form-select:invalid, +.input-group .form-select.is-invalid { + z-index: 2; +} +.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, +.was-validated .input-group .form-select:invalid:focus, +.input-group .form-select.is-invalid:focus { + z-index: 3; +} + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: #212529; +} +.btn-check:focus + .btn, .btn:focus { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + pointer-events: none; + opacity: 0.65; +} + +.btn-primary { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} +.btn-primary:hover { + color: #fff; + background-color: #0b5ed7; + border-color: #0a58ca; +} +.btn-check:focus + .btn-primary, .btn-primary:focus { + color: #fff; + background-color: #0b5ed7; + border-color: #0a58ca; + box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); +} +.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0a58ca; + border-color: #0a53be; +} +.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); +} +.btn-primary:disabled, .btn-primary.disabled { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-secondary { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-secondary:hover { + color: #fff; + background-color: #5c636a; + border-color: #565e64; +} +.btn-check:focus + .btn-secondary, .btn-secondary:focus { + color: #fff; + background-color: #5c636a; + border-color: #565e64; + box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5); +} +.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #565e64; + border-color: #51585e; +} +.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5); +} +.btn-secondary:disabled, .btn-secondary.disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-success { + color: #fff; + background-color: #198754; + border-color: #198754; +} +.btn-success:hover { + color: #fff; + background-color: #157347; + border-color: #146c43; +} +.btn-check:focus + .btn-success, .btn-success:focus { + color: #fff; + background-color: #157347; + border-color: #146c43; + box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5); +} +.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #146c43; + border-color: #13653f; +} +.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5); +} +.btn-success:disabled, .btn-success.disabled { + color: #fff; + background-color: #198754; + border-color: #198754; +} + +.btn-info { + color: #000; + background-color: #0dcaf0; + border-color: #0dcaf0; +} +.btn-info:hover { + color: #000; + background-color: #31d2f2; + border-color: #25cff2; +} +.btn-check:focus + .btn-info, .btn-info:focus { + color: #000; + background-color: #31d2f2; + border-color: #25cff2; + box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5); +} +.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle { + color: #000; + background-color: #3dd5f3; + border-color: #25cff2; +} +.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5); +} +.btn-info:disabled, .btn-info.disabled { + color: #000; + background-color: #0dcaf0; + border-color: #0dcaf0; +} + +.btn-warning { + color: #000; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-warning:hover { + color: #000; + background-color: #ffca2c; + border-color: #ffc720; +} +.btn-check:focus + .btn-warning, .btn-warning:focus { + color: #000; + background-color: #ffca2c; + border-color: #ffc720; + box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5); +} +.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle { + color: #000; + background-color: #ffcd39; + border-color: #ffc720; +} +.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5); +} +.btn-warning:disabled, .btn-warning.disabled { + color: #000; + background-color: #ffc107; + border-color: #ffc107; +} + +.btn-danger { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} +.btn-danger:hover { + color: #fff; + background-color: #bb2d3b; + border-color: #b02a37; +} +.btn-check:focus + .btn-danger, .btn-danger:focus { + color: #fff; + background-color: #bb2d3b; + border-color: #b02a37; + box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5); +} +.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #b02a37; + border-color: #a52834; +} +.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5); +} +.btn-danger:disabled, .btn-danger.disabled { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-light { + color: #000; + background-color: #f8f9fa; + border-color: #f8f9fa; +} +.btn-light:hover { + color: #000; + background-color: #f9fafb; + border-color: #f9fafb; +} +.btn-check:focus + .btn-light, .btn-light:focus { + color: #000; + background-color: #f9fafb; + border-color: #f9fafb; + box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5); +} +.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle { + color: #000; + background-color: #f9fafb; + border-color: #f9fafb; +} +.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5); +} +.btn-light:disabled, .btn-light.disabled { + color: #000; + background-color: #f8f9fa; + border-color: #f8f9fa; +} + +.btn-dark { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-dark:hover { + color: #fff; + background-color: #1c1f23; + border-color: #1a1e21; +} +.btn-check:focus + .btn-dark, .btn-dark:focus { + color: #fff; + background-color: #1c1f23; + border-color: #1a1e21; + box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); +} +.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1a1e21; + border-color: #191c1f; +} +.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); +} +.btn-dark:disabled, .btn-dark.disabled { + color: #fff; + background-color: #212529; + border-color: #212529; +} + +.btn-outline-primary { + color: #0d6efd; + border-color: #0d6efd; +} +.btn-outline-primary:hover { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} +.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5); +} +.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} +.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5); +} +.btn-outline-primary:disabled, .btn-outline-primary.disabled { + color: #0d6efd; + background-color: transparent; +} + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { + box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5); +} +.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} +.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5); +} +.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: #6c757d; + background-color: transparent; +} + +.btn-outline-success { + color: #198754; + border-color: #198754; +} +.btn-outline-success:hover { + color: #fff; + background-color: #198754; + border-color: #198754; +} +.btn-check:focus + .btn-outline-success, .btn-outline-success:focus { + box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5); +} +.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { + color: #fff; + background-color: #198754; + border-color: #198754; +} +.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5); +} +.btn-outline-success:disabled, .btn-outline-success.disabled { + color: #198754; + background-color: transparent; +} + +.btn-outline-info { + color: #0dcaf0; + border-color: #0dcaf0; +} +.btn-outline-info:hover { + color: #000; + background-color: #0dcaf0; + border-color: #0dcaf0; +} +.btn-check:focus + .btn-outline-info, .btn-outline-info:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5); +} +.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { + color: #000; + background-color: #0dcaf0; + border-color: #0dcaf0; +} +.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5); +} +.btn-outline-info:disabled, .btn-outline-info.disabled { + color: #0dcaf0; + background-color: transparent; +} + +.btn-outline-warning { + color: #ffc107; + border-color: #ffc107; +} +.btn-outline-warning:hover { + color: #000; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); +} +.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { + color: #000; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); +} +.btn-outline-warning:disabled, .btn-outline-warning.disabled { + color: #ffc107; + background-color: transparent; +} + +.btn-outline-danger { + color: #dc3545; + border-color: #dc3545; +} +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} +.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { + box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5); +} +.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} +.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5); +} +.btn-outline-danger:disabled, .btn-outline-danger.disabled { + color: #dc3545; + background-color: transparent; +} + +.btn-outline-light { + color: #f8f9fa; + border-color: #f8f9fa; +} +.btn-outline-light:hover { + color: #000; + background-color: #f8f9fa; + border-color: #f8f9fa; +} +.btn-check:focus + .btn-outline-light, .btn-outline-light:focus { + box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5); +} +.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { + color: #000; + background-color: #f8f9fa; + border-color: #f8f9fa; +} +.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5); +} +.btn-outline-light:disabled, .btn-outline-light.disabled { + color: #f8f9fa; + background-color: transparent; +} + +.btn-outline-dark { + color: #212529; + border-color: #212529; +} +.btn-outline-dark:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-dark:disabled, .btn-outline-dark.disabled { + color: #212529; + background-color: transparent; +} + +.btn-link { + font-weight: 400; + color: #0d6efd; + text-decoration: underline; +} +.btn-link:hover { + color: #0a58ca; +} +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.2rem; +} + +.fade { + transition: opacity 0.15s linear; +} +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropend, +.dropdown, +.dropstart { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + z-index: 1000; + display: none; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0; + font-size: 1rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} +.dropdown-menu[data-bs-popper] { + top: 100%; + left: 0; + margin-top: 0.125rem; +} + +.dropdown-menu-start { + --bs-position: start; +} +.dropdown-menu-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; +} + +.dropdown-menu-end { + --bs-position: end; +} +.dropdown-menu-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-start { + --bs-position: start; + } + .dropdown-menu-sm-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; + } + + .dropdown-menu-sm-end { + --bs-position: end; + } + .dropdown-menu-sm-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; + } +} +@media (min-width: 768px) { + .dropdown-menu-md-start { + --bs-position: start; + } + .dropdown-menu-md-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; + } + + .dropdown-menu-md-end { + --bs-position: end; + } + .dropdown-menu-md-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; + } +} +@media (min-width: 992px) { + .dropdown-menu-lg-start { + --bs-position: start; + } + .dropdown-menu-lg-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; + } + + .dropdown-menu-lg-end { + --bs-position: end; + } + .dropdown-menu-lg-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; + } +} +@media (min-width: 1200px) { + .dropdown-menu-xl-start { + --bs-position: start; + } + .dropdown-menu-xl-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; + } + + .dropdown-menu-xl-end { + --bs-position: end; + } + .dropdown-menu-xl-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; + } +} +@media (min-width: 1400px) { + .dropdown-menu-xxl-start { + --bs-position: start; + } + .dropdown-menu-xxl-start[data-bs-popper] { + right: auto /* rtl:ignore */; + left: 0 /* rtl:ignore */; + } + + .dropdown-menu-xxl-end { + --bs-position: end; + } + .dropdown-menu-xxl-end[data-bs-popper] { + right: 0 /* rtl:ignore */; + left: auto /* rtl:ignore */; + } +} +.dropup .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; +} +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropend .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; +} +.dropend .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} +.dropend .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropend .dropdown-toggle::after { + vertical-align: 0; +} + +.dropstart .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; +} +.dropstart .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} +.dropstart .dropdown-toggle::after { + display: none; +} +.dropstart .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} +.dropstart .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropstart .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid rgba(0, 0, 0, 0.15); +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; +} +.dropdown-item:hover, .dropdown-item:focus { + color: #1e2125; + background-color: #e9ecef; +} +.dropdown-item.active, .dropdown-item:active { + color: #fff; + text-decoration: none; + background-color: #0d6efd; +} +.dropdown-item.disabled, .dropdown-item:disabled { + color: #adb5bd; + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: 0.5rem 1rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1rem; + color: #212529; +} + +.dropdown-menu-dark { + color: #dee2e6; + background-color: #343a40; + border-color: rgba(0, 0, 0, 0.15); +} +.dropdown-menu-dark .dropdown-item { + color: #dee2e6; +} +.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus { + color: #fff; + background-color: rgba(255, 255, 255, 0.15); +} +.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active { + color: #fff; + background-color: #0d6efd; +} +.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled { + color: #adb5bd; +} +.dropdown-menu-dark .dropdown-divider { + border-color: rgba(0, 0, 0, 0.15); +} +.dropdown-menu-dark .dropdown-item-text { + color: #dee2e6; +} +.dropdown-menu-dark .dropdown-header { + color: #adb5bd; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 1 1 auto; +} +.btn-group > .btn-check:checked + .btn, +.btn-group > .btn-check:focus + .btn, +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn-check:checked + .btn, +.btn-group-vertical > .btn-check:focus + .btn, +.btn-group-vertical > .btn:hover, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:nth-child(n+3), +.btn-group > :not(.btn-check) + .btn, +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} +.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after { + margin-left: 0; +} +.dropstart .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn ~ .btn, +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; + color: #0d6efd; + text-decoration: none; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .nav-link { + transition: none; + } +} +.nav-link:hover, .nav-link:focus { + color: #0a58ca; +} +.nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; +} + +.nav-tabs { + border-bottom: 1px solid #dee2e6; +} +.nav-tabs .nav-link { + margin-bottom: -1px; + background: none; + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; + isolation: isolate; +} +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + background: none; + border: 0; + border-radius: 0.25rem; +} +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #0d6efd; +} + +.nav-fill > .nav-link, +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified > .nav-link, +.nav-justified .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +.nav-fill .nav-item .nav-link, +.nav-justified .nav-item .nav-link { + width: 100%; +} + +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.navbar > .container, +.navbar > .container-fluid, +.navbar > .container-sm, +.navbar > .container-md, +.navbar > .container-lg, +.navbar > .container-xl, +.navbar > .container-xxl { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; +} +.navbar-brand { + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + text-decoration: none; + white-space: nowrap; +} +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} +.navbar-nav .dropdown-menu { + position: static; +} + +.navbar-text { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; + transition: box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .navbar-toggler { + transition: none; + } +} +.navbar-toggler:hover { + text-decoration: none; +} +.navbar-toggler:focus { + text-decoration: none; + outline: 0; + box-shadow: 0 0 0 0.25rem; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + background-repeat: no-repeat; + background-position: center; + background-size: 100%; +} + +.navbar-nav-scroll { + max-height: var(--bs-scroll-height, 75vh); + overflow-y: auto; +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } +} +@media (min-width: 1400px) { + .navbar-expand-xxl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xxl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xxl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xxl .navbar-toggler { + display: none; + } +} +.navbar-expand { + flex-wrap: nowrap; + justify-content: flex-start; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.55); +} +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.55); + border-color: rgba(0, 0, 0, 0.1); +} +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.55); +} +.navbar-light .navbar-text a, +.navbar-light .navbar-text a:hover, +.navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.55); +} +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.55); + border-color: rgba(255, 255, 255, 0.1); +} +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.55); +} +.navbar-dark .navbar-text a, +.navbar-dark .navbar-text a:hover, +.navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.card > .list-group { + border-top: inherit; + border-bottom: inherit; +} +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; +} + +.card-body { + flex: 1 1 auto; + padding: 1rem 1rem; +} + +.card-title { + margin-bottom: 0.5rem; +} + +.card-subtitle { + margin-top: -0.25rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} +.card-link + .card-link { + margin-left: 1rem; +} + +.card-header { + padding: 0.5rem 1rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-footer { + padding: 0.5rem 1rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.5rem; + margin-bottom: -0.5rem; + margin-left: -0.5rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.5rem; + margin-left: -0.5rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1rem; + border-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-group > .card { + margin-bottom: 0.75rem; +} +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, +.card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, +.card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, +.card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, +.card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: 1rem 1.25rem; + font-size: 1rem; + color: #212529; + text-align: left; + background-color: #fff; + border: 0; + border-radius: 0; + overflow-anchor: none; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .accordion-button { + transition: none; + } +} +.accordion-button:not(.collapsed) { + color: #0c63e4; + background-color: #e7f1ff; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125); +} +.accordion-button:not(.collapsed)::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + transform: rotate(-180deg); +} +.accordion-button::after { + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + margin-left: auto; + content: ""; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-size: 1.25rem; + transition: transform 0.2s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .accordion-button::after { + transition: none; + } +} +.accordion-button:hover { + z-index: 2; +} +.accordion-button:focus { + z-index: 3; + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} +.accordion-item:first-of-type { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.accordion-item:first-of-type .accordion-button { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} +.accordion-item:not(:first-of-type) { + border-top: 0; +} +.accordion-item:last-of-type { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} +.accordion-item:last-of-type .accordion-button.collapsed { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} +.accordion-item:last-of-type .accordion-collapse { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.accordion-body { + padding: 1rem 1.25rem; +} + +.accordion-flush .accordion-collapse { + border-width: 0; +} +.accordion-flush .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} +.accordion-flush .accordion-item:first-child { + border-top: 0; +} +.accordion-flush .accordion-item:last-child { + border-bottom: 0; +} +.accordion-flush .accordion-item .accordion-button { + border-radius: 0; +} + +.breadcrumb { + display: flex; + flex-wrap: wrap; + padding: 0 0; + margin-bottom: 1rem; + list-style: none; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: 0.5rem; + color: #6c757d; + content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; +} +.breadcrumb-item.active { + color: #6c757d; +} + +.pagination { + display: flex; + padding-left: 0; + list-style: none; +} + +.page-link { + position: relative; + display: block; + color: #0d6efd; + text-decoration: none; + background-color: #fff; + border: 1px solid #dee2e6; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .page-link { + transition: none; + } +} +.page-link:hover { + z-index: 2; + color: #0a58ca; + background-color: #e9ecef; + border-color: #dee2e6; +} +.page-link:focus { + z-index: 3; + color: #0a58ca; + background-color: #e9ecef; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.page-item:not(:first-child) .page-link { + margin-left: -1px; +} +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} +.page-item.disabled .page-link { + color: #6c757d; + pointer-events: none; + background-color: #fff; + border-color: #dee2e6; +} + +.page-link { + padding: 0.375rem 0.75rem; +} + +.page-item:first-child .page-link { + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.25rem; +} +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.35em 0.65em; + font-size: 0.75em; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.alert { + position: relative; + padding: 1rem 1rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 3rem; +} +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1.25rem 1rem; +} + +.alert-primary { + color: #084298; + background-color: #cfe2ff; + border-color: #b6d4fe; +} +.alert-primary .alert-link { + color: #06357a; +} + +.alert-secondary { + color: #41464b; + background-color: #e2e3e5; + border-color: #d3d6d8; +} +.alert-secondary .alert-link { + color: #34383c; +} + +.alert-success { + color: #0f5132; + background-color: #d1e7dd; + border-color: #badbcc; +} +.alert-success .alert-link { + color: #0c4128; +} + +.alert-info { + color: #055160; + background-color: #cff4fc; + border-color: #b6effb; +} +.alert-info .alert-link { + color: #04414d; +} + +.alert-warning { + color: #664d03; + background-color: #fff3cd; + border-color: #ffecb5; +} +.alert-warning .alert-link { + color: #523e02; +} + +.alert-danger { + color: #842029; + background-color: #f8d7da; + border-color: #f5c2c7; +} +.alert-danger .alert-link { + color: #6a1a21; +} + +.alert-light { + color: #636464; + background-color: #fefefe; + border-color: #fdfdfe; +} +.alert-light .alert-link { + color: #4f5050; +} + +.alert-dark { + color: #141619; + background-color: #d3d3d4; + border-color: #bcbebf; +} +.alert-dark .alert-link { + color: #101214; +} + +@-webkit-keyframes progress-bar-stripes { + 0% { + background-position-x: 1rem; + } +} + +@keyframes progress-bar-stripes { + 0% { + background-position-x: 1rem; + } +} +.progress { + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #0d6efd; + transition: width 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + -webkit-animation: 1s linear infinite progress-bar-stripes; + animation: 1s linear infinite progress-bar-stripes; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + -webkit-animation: none; + animation: none; + } +} + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: 0.25rem; +} + +.list-group-numbered { + list-style-type: none; + counter-reset: section; +} +.list-group-numbered > li::before { + content: counters(section, ".") ". "; + counter-increment: section; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; +} +.list-group-item-action:active { + color: #212529; + background-color: #e9ecef; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.5rem 1rem; + color: #212529; + text-decoration: none; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} +.list-group-item.disabled, .list-group-item:disabled { + color: #6c757d; + pointer-events: none; + background-color: #fff; +} +.list-group-item.active { + z-index: 2; + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} +.list-group-item + .list-group-item { + border-top-width: 0; +} +.list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; +} + +.list-group-horizontal { + flex-direction: row; +} +.list-group-horizontal > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} +.list-group-horizontal > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; +} +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + flex-direction: row; + } + .list-group-horizontal-sm > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 768px) { + .list-group-horizontal-md { + flex-direction: row; + } + .list-group-horizontal-md > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 992px) { + .list-group-horizontal-lg { + flex-direction: row; + } + .list-group-horizontal-lg > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 1200px) { + .list-group-horizontal-xl { + flex-direction: row; + } + .list-group-horizontal-xl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +@media (min-width: 1400px) { + .list-group-horizontal-xxl { + flex-direction: row; + } + .list-group-horizontal-xxl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; + } +} +.list-group-flush { + border-radius: 0; +} +.list-group-flush > .list-group-item { + border-width: 0 0 1px; +} +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + color: #084298; + background-color: #cfe2ff; +} +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #084298; + background-color: #bacbe6; +} +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #084298; + border-color: #084298; +} + +.list-group-item-secondary { + color: #41464b; + background-color: #e2e3e5; +} +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #41464b; + background-color: #cbccce; +} +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #41464b; + border-color: #41464b; +} + +.list-group-item-success { + color: #0f5132; + background-color: #d1e7dd; +} +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #0f5132; + background-color: #bcd0c7; +} +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #0f5132; + border-color: #0f5132; +} + +.list-group-item-info { + color: #055160; + background-color: #cff4fc; +} +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #055160; + background-color: #badce3; +} +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #055160; + border-color: #055160; +} + +.list-group-item-warning { + color: #664d03; + background-color: #fff3cd; +} +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #664d03; + background-color: #e6dbb9; +} +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #664d03; + border-color: #664d03; +} + +.list-group-item-danger { + color: #842029; + background-color: #f8d7da; +} +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #842029; + background-color: #dfc2c4; +} +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #842029; + border-color: #842029; +} + +.list-group-item-light { + color: #636464; + background-color: #fefefe; +} +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #636464; + background-color: #e5e5e5; +} +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #636464; + border-color: #636464; +} + +.list-group-item-dark { + color: #141619; + background-color: #d3d3d4; +} +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #141619; + background-color: #bebebf; +} +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #141619; + border-color: #141619; +} + +.btn-close { + box-sizing: content-box; + width: 1em; + height: 1em; + padding: 0.25em 0.25em; + color: #000; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + border: 0; + border-radius: 0.25rem; + opacity: 0.5; +} +.btn-close:hover { + color: #000; + text-decoration: none; + opacity: 0.75; +} +.btn-close:focus { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + opacity: 1; +} +.btn-close:disabled, .btn-close.disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0.25; +} + +.btn-close-white { + filter: invert(1) grayscale(100%) brightness(200%); +} + +.toast { + width: 350px; + max-width: 100%; + font-size: 0.875rem; + pointer-events: auto; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} +.toast:not(.showing):not(.show) { + opacity: 0; +} +.toast.hide { + display: none; +} + +.toast-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + max-width: 100%; + pointer-events: none; +} +.toast-container > :not(:last-child) { + margin-bottom: 0.75rem; +} + +.toast-header { + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} +.toast-header .btn-close { + margin-right: -0.375rem; + margin-left: 0.75rem; +} + +.toast-body { + padding: 0.75rem; + word-wrap: break-word; +} + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1060; + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); +} +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} +.modal.show .modal-dialog { + transform: none; +} +.modal.modal-static .modal-dialog { + transform: scale(1.02); +} + +.modal-dialog-scrollable { + height: calc(100% - 1rem); +} +.modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; +} +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - 1rem); +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #dee2e6; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} +.modal-header .btn-close { + padding: 0.5rem 0.5rem; + margin: -0.5rem -0.5rem -0.5rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: flex; + flex-wrap: wrap; + flex-shrink: 0; + align-items: center; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); +} +.modal-footer > * { + margin: 0.25rem; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + + .modal-dialog-scrollable { + height: calc(100% - 3.5rem); + } + + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); + } + + .modal-sm { + max-width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg, +.modal-xl { + max-width: 800px; + } +} +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; + } +} +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; +} +.modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; +} +.modal-fullscreen .modal-header { + border-radius: 0; +} +.modal-fullscreen .modal-body { + overflow-y: auto; +} +.modal-fullscreen .modal-footer { + border-radius: 0; +} + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-header { + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; + } + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; + } +} +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-md-down .modal-header { + border-radius: 0; + } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; + } + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; + } +} +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-header { + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; + } + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; + } +} +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-header { + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; + } + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; + } +} +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-header { + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; + } + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; + } +} +.tooltip { + position: absolute; + z-index: 1080; + display: block; + margin: 0; + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + opacity: 0; +} +.tooltip.show { + opacity: 0.9; +} +.tooltip .tooltip-arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} +.tooltip .tooltip-arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] { + padding: 0.4rem 0; +} +.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { + bottom: 0; +} +.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { + top: -1px; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] { + padding: 0 0.4rem; +} +.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} +.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { + right: -1px; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] { + padding: 0.4rem 0; +} +.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { + top: 0; +} +.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { + bottom: -1px; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] { + padding: 0 0.4rem; +} +.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} +.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { + left: -1px; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0 /* rtl:ignore */; + z-index: 1070; + display: block; + max-width: 276px; + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} +.popover .popover-arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; +} +.popover .popover-arrow::before, .popover .popover-arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { + bottom: calc(-0.5rem - 1px); +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { + top: calc(-0.5rem - 1px); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} +.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f0f0f0; +} + +.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} +.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 1rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f0f0f0; + border-bottom: 1px solid #d8d8d8; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 1rem 1rem; + color: #212529; +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +/* rtl:begin:ignore */ +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +/* rtl:end:ignore */ +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; +} +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-start, +.carousel-fade .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; +} +.carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + border: 0; + opacity: 0.5; + transition: opacity 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, +.carousel-control-next { + transition: none; + } +} +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 2rem; + height: 2rem; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + margin-right: 15%; + margin-bottom: 1rem; + margin-left: 15%; + list-style: none; +} +.carousel-indicators [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: 30px; + height: 3px; + padding: 0; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-indicators [data-bs-target] { + transition: none; + } +} +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 1.25rem; + left: 15%; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + color: #fff; + text-align: center; +} + +.carousel-dark .carousel-control-prev-icon, +.carousel-dark .carousel-control-next-icon { + filter: invert(1) grayscale(100); +} +.carousel-dark .carousel-indicators [data-bs-target] { + background-color: #000; +} +.carousel-dark .carousel-caption { + color: #000; +} + +@-webkit-keyframes spinner-border { + to { + transform: rotate(360deg) /* rtl:ignore */; + } +} + +@keyframes spinner-border { + to { + transform: rotate(360deg) /* rtl:ignore */; + } +} +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: 0.75s linear infinite spinner-border; + animation: 0.75s linear infinite spinner-border; +} + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +@-webkit-keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} + +@keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + -webkit-animation: 0.75s linear infinite spinner-grow; + animation: 0.75s linear infinite spinner-grow; +} + +.spinner-grow-sm { + width: 1rem; + height: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + .spinner-border, +.spinner-grow { + -webkit-animation-duration: 1.5s; + animation-duration: 1.5s; + } +} +.offcanvas { + position: fixed; + bottom: 0; + z-index: 1050; + display: flex; + flex-direction: column; + max-width: 100%; + visibility: hidden; + background-color: #fff; + background-clip: padding-box; + outline: 0; + transition: transform 0.3s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .offcanvas { + transition: none; + } +} + +.offcanvas-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1rem; +} +.offcanvas-header .btn-close { + padding: 0.5rem 0.5rem; + margin: -0.5rem -0.5rem -0.5rem auto; +} + +.offcanvas-title { + margin-bottom: 0; + line-height: 1.5; +} + +.offcanvas-body { + flex-grow: 1; + padding: 1rem 1rem; + overflow-y: auto; +} + +.offcanvas-start { + top: 0; + left: 0; + width: 400px; + border-right: 1px solid rgba(0, 0, 0, 0.2); + transform: translateX(-100%); +} + +.offcanvas-end { + top: 0; + right: 0; + width: 400px; + border-left: 1px solid rgba(0, 0, 0, 0.2); + transform: translateX(100%); +} + +.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: 30vh; + max-height: 100%; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + transform: translateY(-100%); +} + +.offcanvas-bottom { + right: 0; + left: 0; + height: 30vh; + max-height: 100%; + border-top: 1px solid rgba(0, 0, 0, 0.2); + transform: translateY(100%); +} + +.offcanvas.show { + transform: none; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.link-primary { + color: #0d6efd; +} +.link-primary:hover, .link-primary:focus { + color: #0a58ca; +} + +.link-secondary { + color: #6c757d; +} +.link-secondary:hover, .link-secondary:focus { + color: #565e64; +} + +.link-success { + color: #198754; +} +.link-success:hover, .link-success:focus { + color: #146c43; +} + +.link-info { + color: #0dcaf0; +} +.link-info:hover, .link-info:focus { + color: #3dd5f3; +} + +.link-warning { + color: #ffc107; +} +.link-warning:hover, .link-warning:focus { + color: #ffcd39; +} + +.link-danger { + color: #dc3545; +} +.link-danger:hover, .link-danger:focus { + color: #b02a37; +} + +.link-light { + color: #f8f9fa; +} +.link-light:hover, .link-light:focus { + color: #f9fafb; +} + +.link-dark { + color: #212529; +} +.link-dark:hover, .link-dark:focus { + color: #1a1e21; +} + +.ratio { + position: relative; + width: 100%; +} +.ratio::before { + display: block; + padding-top: var(--bs-aspect-ratio); + content: ""; +} +.ratio > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.ratio-1x1 { + --bs-aspect-ratio: 100%; +} + +.ratio-4x3 { + --bs-aspect-ratio: calc(3 / 4 * 100%); +} + +.ratio-16x9 { + --bs-aspect-ratio: calc(9 / 16 * 100%); +} + +.ratio-21x9 { + --bs-aspect-ratio: calc(9 / 21 * 100%); +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +.sticky-top { + position: sticky; + top: 0; + z-index: 1020; +} + +@media (min-width: 576px) { + .sticky-sm-top { + position: sticky; + top: 0; + z-index: 1020; + } +} +@media (min-width: 768px) { + .sticky-md-top { + position: sticky; + top: 0; + z-index: 1020; + } +} +@media (min-width: 992px) { + .sticky-lg-top { + position: sticky; + top: 0; + z-index: 1020; + } +} +@media (min-width: 1200px) { + .sticky-xl-top { + position: sticky; + top: 0; + z-index: 1020; + } +} +@media (min-width: 1400px) { + .sticky-xxl-top { + position: sticky; + top: 0; + z-index: 1020; + } +} +.visually-hidden, +.visually-hidden-focusable:not(:focus):not(:focus-within) { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.float-start { + float: left !important; +} + +.float-end { + float: right !important; +} + +.float-none { + float: none !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.overflow-visible { + overflow: visible !important; +} + +.overflow-scroll { + overflow: scroll !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: sticky !important; +} + +.top-0 { + top: 0 !important; +} + +.top-50 { + top: 50% !important; +} + +.top-100 { + top: 100% !important; +} + +.bottom-0 { + bottom: 0 !important; +} + +.bottom-50 { + bottom: 50% !important; +} + +.bottom-100 { + bottom: 100% !important; +} + +.start-0 { + left: 0 !important; +} + +.start-50 { + left: 50% !important; +} + +.start-100 { + left: 100% !important; +} + +.end-0 { + right: 0 !important; +} + +.end-50 { + right: 50% !important; +} + +.end-100 { + right: 100% !important; +} + +.translate-middle { + transform: translate(-50%, -50%) !important; +} + +.translate-middle-x { + transform: translateX(-50%) !important; +} + +.translate-middle-y { + transform: translateY(-50%) !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top { + border-top: 1px solid #dee2e6 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-end { + border-right: 1px solid #dee2e6 !important; +} + +.border-end-0 { + border-right: 0 !important; +} + +.border-bottom { + border-bottom: 1px solid #dee2e6 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-start { + border-left: 1px solid #dee2e6 !important; +} + +.border-start-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #0d6efd !important; +} + +.border-secondary { + border-color: #6c757d !important; +} + +.border-success { + border-color: #198754 !important; +} + +.border-info { + border-color: #0dcaf0 !important; +} + +.border-warning { + border-color: #ffc107 !important; +} + +.border-danger { + border-color: #dc3545 !important; +} + +.border-light { + border-color: #f8f9fa !important; +} + +.border-dark { + border-color: #212529 !important; +} + +.border-white { + border-color: #fff !important; +} + +.border-1 { + border-width: 1px !important; +} + +.border-2 { + border-width: 2px !important; +} + +.border-3 { + border-width: 3px !important; +} + +.border-4 { + border-width: 4px !important; +} + +.border-5 { + border-width: 5px !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.vw-100 { + width: 100vw !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.vh-100 { + height: 100vh !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.gap-0 { + gap: 0 !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 1rem !important; +} + +.gap-4 { + gap: 1.5rem !important; +} + +.gap-5 { + gap: 3rem !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mx-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; +} + +.mx-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; +} + +.mx-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; +} + +.mx-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; +} + +.mx-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; +} + +.my-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; +} + +.my-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; +} + +.my-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; +} + +.my-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; +} + +.my-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; +} + +.mt-n1 { + margin-top: -0.25rem !important; +} + +.mt-n2 { + margin-top: -0.5rem !important; +} + +.mt-n3 { + margin-top: -1rem !important; +} + +.mt-n4 { + margin-top: -1.5rem !important; +} + +.mt-n5 { + margin-top: -3rem !important; +} + +.me-n1 { + margin-right: -0.25rem !important; +} + +.me-n2 { + margin-right: -0.5rem !important; +} + +.me-n3 { + margin-right: -1rem !important; +} + +.me-n4 { + margin-right: -1.5rem !important; +} + +.me-n5 { + margin-right: -3rem !important; +} + +.mb-n1 { + margin-bottom: -0.25rem !important; +} + +.mb-n2 { + margin-bottom: -0.5rem !important; +} + +.mb-n3 { + margin-bottom: -1rem !important; +} + +.mb-n4 { + margin-bottom: -1.5rem !important; +} + +.mb-n5 { + margin-bottom: -3rem !important; +} + +.ms-n1 { + margin-left: -0.25rem !important; +} + +.ms-n2 { + margin-left: -0.5rem !important; +} + +.ms-n3 { + margin-left: -1rem !important; +} + +.ms-n4 { + margin-left: -1.5rem !important; +} + +.ms-n5 { + margin-left: -3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +.font-monospace { + font-family: var(--bs-font-monospace) !important; +} + +.fs-1 { + font-size: calc(1.375rem + 1.5vw) !important; +} + +.fs-2 { + font-size: calc(1.325rem + 0.9vw) !important; +} + +.fs-3 { + font-size: calc(1.3rem + 0.6vw) !important; +} + +.fs-4 { + font-size: calc(1.275rem + 0.3vw) !important; +} + +.fs-5 { + font-size: 1.25rem !important; +} + +.fs-6 { + font-size: 1rem !important; +} + +.fst-italic { + font-style: italic !important; +} + +.fst-normal { + font-style: normal !important; +} + +.fw-light { + font-weight: 300 !important; +} + +.fw-lighter { + font-weight: lighter !important; +} + +.fw-normal { + font-weight: 400 !important; +} + +.fw-bold { + font-weight: 700 !important; +} + +.fw-bolder { + font-weight: bolder !important; +} + +.lh-1 { + line-height: 1 !important; +} + +.lh-sm { + line-height: 1.25 !important; +} + +.lh-base { + line-height: 1.5 !important; +} + +.lh-lg { + line-height: 2 !important; +} + +.text-start { + text-align: left !important; +} + +.text-end { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-decoration-underline { + text-decoration: underline !important; +} + +.text-decoration-line-through { + text-decoration: line-through !important; +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +/* rtl:begin:remove */ +.text-break { + word-wrap: break-word !important; + word-break: break-word !important; +} + +/* rtl:end:remove */ +.text-primary { + color: #0d6efd !important; +} + +.text-secondary { + color: #6c757d !important; +} + +.text-success { + color: #198754 !important; +} + +.text-info { + color: #0dcaf0 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +.text-light { + color: #f8f9fa !important; +} + +.text-dark { + color: #212529 !important; +} + +.text-white { + color: #fff !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-reset { + color: inherit !important; +} + +.bg-primary { + background-color: #0d6efd !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +.bg-success { + background-color: #198754 !important; +} + +.bg-info { + background-color: #0dcaf0 !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +.bg-dark { + background-color: #212529 !important; +} + +.bg-body { + background-color: #fff !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.bg-gradient { + background-image: var(--bs-gradient) !important; +} + +.user-select-all { + -webkit-user-select: all !important; + -moz-user-select: all !important; + user-select: all !important; +} + +.user-select-auto { + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + -ms-user-select: auto !important; + user-select: auto !important; +} + +.user-select-none { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; +} + +.pe-none { + pointer-events: none !important; +} + +.pe-auto { + pointer-events: auto !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.rounded-1 { + border-radius: 0.2rem !important; +} + +.rounded-2 { + border-radius: 0.25rem !important; +} + +.rounded-3 { + border-radius: 0.3rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: 50rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-end { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-start { + border-bottom-left-radius: 0.25rem !important; + border-top-left-radius: 0.25rem !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media (min-width: 576px) { + .float-sm-start { + float: left !important; + } + + .float-sm-end { + float: right !important; + } + + .float-sm-none { + float: none !important; + } + + .d-sm-inline { + display: inline !important; + } + + .d-sm-inline-block { + display: inline-block !important; + } + + .d-sm-block { + display: block !important; + } + + .d-sm-grid { + display: grid !important; + } + + .d-sm-table { + display: table !important; + } + + .d-sm-table-row { + display: table-row !important; + } + + .d-sm-table-cell { + display: table-cell !important; + } + + .d-sm-flex { + display: flex !important; + } + + .d-sm-inline-flex { + display: inline-flex !important; + } + + .d-sm-none { + display: none !important; + } + + .flex-sm-fill { + flex: 1 1 auto !important; + } + + .flex-sm-row { + flex-direction: row !important; + } + + .flex-sm-column { + flex-direction: column !important; + } + + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-sm-wrap { + flex-wrap: wrap !important; + } + + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .gap-sm-0 { + gap: 0 !important; + } + + .gap-sm-1 { + gap: 0.25rem !important; + } + + .gap-sm-2 { + gap: 0.5rem !important; + } + + .gap-sm-3 { + gap: 1rem !important; + } + + .gap-sm-4 { + gap: 1.5rem !important; + } + + .gap-sm-5 { + gap: 3rem !important; + } + + .justify-content-sm-start { + justify-content: flex-start !important; + } + + .justify-content-sm-end { + justify-content: flex-end !important; + } + + .justify-content-sm-center { + justify-content: center !important; + } + + .justify-content-sm-between { + justify-content: space-between !important; + } + + .justify-content-sm-around { + justify-content: space-around !important; + } + + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + + .align-items-sm-start { + align-items: flex-start !important; + } + + .align-items-sm-end { + align-items: flex-end !important; + } + + .align-items-sm-center { + align-items: center !important; + } + + .align-items-sm-baseline { + align-items: baseline !important; + } + + .align-items-sm-stretch { + align-items: stretch !important; + } + + .align-content-sm-start { + align-content: flex-start !important; + } + + .align-content-sm-end { + align-content: flex-end !important; + } + + .align-content-sm-center { + align-content: center !important; + } + + .align-content-sm-between { + align-content: space-between !important; + } + + .align-content-sm-around { + align-content: space-around !important; + } + + .align-content-sm-stretch { + align-content: stretch !important; + } + + .align-self-sm-auto { + align-self: auto !important; + } + + .align-self-sm-start { + align-self: flex-start !important; + } + + .align-self-sm-end { + align-self: flex-end !important; + } + + .align-self-sm-center { + align-self: center !important; + } + + .align-self-sm-baseline { + align-self: baseline !important; + } + + .align-self-sm-stretch { + align-self: stretch !important; + } + + .order-sm-first { + order: -1 !important; + } + + .order-sm-0 { + order: 0 !important; + } + + .order-sm-1 { + order: 1 !important; + } + + .order-sm-2 { + order: 2 !important; + } + + .order-sm-3 { + order: 3 !important; + } + + .order-sm-4 { + order: 4 !important; + } + + .order-sm-5 { + order: 5 !important; + } + + .order-sm-last { + order: 6 !important; + } + + .m-sm-0 { + margin: 0 !important; + } + + .m-sm-1 { + margin: 0.25rem !important; + } + + .m-sm-2 { + margin: 0.5rem !important; + } + + .m-sm-3 { + margin: 1rem !important; + } + + .m-sm-4 { + margin: 1.5rem !important; + } + + .m-sm-5 { + margin: 3rem !important; + } + + .m-sm-auto { + margin: auto !important; + } + + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-sm-0 { + margin-top: 0 !important; + } + + .mt-sm-1 { + margin-top: 0.25rem !important; + } + + .mt-sm-2 { + margin-top: 0.5rem !important; + } + + .mt-sm-3 { + margin-top: 1rem !important; + } + + .mt-sm-4 { + margin-top: 1.5rem !important; + } + + .mt-sm-5 { + margin-top: 3rem !important; + } + + .mt-sm-auto { + margin-top: auto !important; + } + + .me-sm-0 { + margin-right: 0 !important; + } + + .me-sm-1 { + margin-right: 0.25rem !important; + } + + .me-sm-2 { + margin-right: 0.5rem !important; + } + + .me-sm-3 { + margin-right: 1rem !important; + } + + .me-sm-4 { + margin-right: 1.5rem !important; + } + + .me-sm-5 { + margin-right: 3rem !important; + } + + .me-sm-auto { + margin-right: auto !important; + } + + .mb-sm-0 { + margin-bottom: 0 !important; + } + + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + + .mb-sm-3 { + margin-bottom: 1rem !important; + } + + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + + .mb-sm-5 { + margin-bottom: 3rem !important; + } + + .mb-sm-auto { + margin-bottom: auto !important; + } + + .ms-sm-0 { + margin-left: 0 !important; + } + + .ms-sm-1 { + margin-left: 0.25rem !important; + } + + .ms-sm-2 { + margin-left: 0.5rem !important; + } + + .ms-sm-3 { + margin-left: 1rem !important; + } + + .ms-sm-4 { + margin-left: 1.5rem !important; + } + + .ms-sm-5 { + margin-left: 3rem !important; + } + + .ms-sm-auto { + margin-left: auto !important; + } + + .m-sm-n1 { + margin: -0.25rem !important; + } + + .m-sm-n2 { + margin: -0.5rem !important; + } + + .m-sm-n3 { + margin: -1rem !important; + } + + .m-sm-n4 { + margin: -1.5rem !important; + } + + .m-sm-n5 { + margin: -3rem !important; + } + + .mx-sm-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; + } + + .mx-sm-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; + } + + .mx-sm-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; + } + + .mx-sm-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; + } + + .mx-sm-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; + } + + .my-sm-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; + } + + .my-sm-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; + } + + .my-sm-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; + } + + .my-sm-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; + } + + .my-sm-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; + } + + .mt-sm-n1 { + margin-top: -0.25rem !important; + } + + .mt-sm-n2 { + margin-top: -0.5rem !important; + } + + .mt-sm-n3 { + margin-top: -1rem !important; + } + + .mt-sm-n4 { + margin-top: -1.5rem !important; + } + + .mt-sm-n5 { + margin-top: -3rem !important; + } + + .me-sm-n1 { + margin-right: -0.25rem !important; + } + + .me-sm-n2 { + margin-right: -0.5rem !important; + } + + .me-sm-n3 { + margin-right: -1rem !important; + } + + .me-sm-n4 { + margin-right: -1.5rem !important; + } + + .me-sm-n5 { + margin-right: -3rem !important; + } + + .mb-sm-n1 { + margin-bottom: -0.25rem !important; + } + + .mb-sm-n2 { + margin-bottom: -0.5rem !important; + } + + .mb-sm-n3 { + margin-bottom: -1rem !important; + } + + .mb-sm-n4 { + margin-bottom: -1.5rem !important; + } + + .mb-sm-n5 { + margin-bottom: -3rem !important; + } + + .ms-sm-n1 { + margin-left: -0.25rem !important; + } + + .ms-sm-n2 { + margin-left: -0.5rem !important; + } + + .ms-sm-n3 { + margin-left: -1rem !important; + } + + .ms-sm-n4 { + margin-left: -1.5rem !important; + } + + .ms-sm-n5 { + margin-left: -3rem !important; + } + + .p-sm-0 { + padding: 0 !important; + } + + .p-sm-1 { + padding: 0.25rem !important; + } + + .p-sm-2 { + padding: 0.5rem !important; + } + + .p-sm-3 { + padding: 1rem !important; + } + + .p-sm-4 { + padding: 1.5rem !important; + } + + .p-sm-5 { + padding: 3rem !important; + } + + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-sm-0 { + padding-top: 0 !important; + } + + .pt-sm-1 { + padding-top: 0.25rem !important; + } + + .pt-sm-2 { + padding-top: 0.5rem !important; + } + + .pt-sm-3 { + padding-top: 1rem !important; + } + + .pt-sm-4 { + padding-top: 1.5rem !important; + } + + .pt-sm-5 { + padding-top: 3rem !important; + } + + .pe-sm-0 { + padding-right: 0 !important; + } + + .pe-sm-1 { + padding-right: 0.25rem !important; + } + + .pe-sm-2 { + padding-right: 0.5rem !important; + } + + .pe-sm-3 { + padding-right: 1rem !important; + } + + .pe-sm-4 { + padding-right: 1.5rem !important; + } + + .pe-sm-5 { + padding-right: 3rem !important; + } + + .pb-sm-0 { + padding-bottom: 0 !important; + } + + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + + .pb-sm-3 { + padding-bottom: 1rem !important; + } + + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + + .pb-sm-5 { + padding-bottom: 3rem !important; + } + + .ps-sm-0 { + padding-left: 0 !important; + } + + .ps-sm-1 { + padding-left: 0.25rem !important; + } + + .ps-sm-2 { + padding-left: 0.5rem !important; + } + + .ps-sm-3 { + padding-left: 1rem !important; + } + + .ps-sm-4 { + padding-left: 1.5rem !important; + } + + .ps-sm-5 { + padding-left: 3rem !important; + } + + .text-sm-start { + text-align: left !important; + } + + .text-sm-end { + text-align: right !important; + } + + .text-sm-center { + text-align: center !important; + } +} +@media (min-width: 768px) { + .float-md-start { + float: left !important; + } + + .float-md-end { + float: right !important; + } + + .float-md-none { + float: none !important; + } + + .d-md-inline { + display: inline !important; + } + + .d-md-inline-block { + display: inline-block !important; + } + + .d-md-block { + display: block !important; + } + + .d-md-grid { + display: grid !important; + } + + .d-md-table { + display: table !important; + } + + .d-md-table-row { + display: table-row !important; + } + + .d-md-table-cell { + display: table-cell !important; + } + + .d-md-flex { + display: flex !important; + } + + .d-md-inline-flex { + display: inline-flex !important; + } + + .d-md-none { + display: none !important; + } + + .flex-md-fill { + flex: 1 1 auto !important; + } + + .flex-md-row { + flex-direction: row !important; + } + + .flex-md-column { + flex-direction: column !important; + } + + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-md-grow-0 { + flex-grow: 0 !important; + } + + .flex-md-grow-1 { + flex-grow: 1 !important; + } + + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-md-wrap { + flex-wrap: wrap !important; + } + + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .gap-md-0 { + gap: 0 !important; + } + + .gap-md-1 { + gap: 0.25rem !important; + } + + .gap-md-2 { + gap: 0.5rem !important; + } + + .gap-md-3 { + gap: 1rem !important; + } + + .gap-md-4 { + gap: 1.5rem !important; + } + + .gap-md-5 { + gap: 3rem !important; + } + + .justify-content-md-start { + justify-content: flex-start !important; + } + + .justify-content-md-end { + justify-content: flex-end !important; + } + + .justify-content-md-center { + justify-content: center !important; + } + + .justify-content-md-between { + justify-content: space-between !important; + } + + .justify-content-md-around { + justify-content: space-around !important; + } + + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + + .align-items-md-start { + align-items: flex-start !important; + } + + .align-items-md-end { + align-items: flex-end !important; + } + + .align-items-md-center { + align-items: center !important; + } + + .align-items-md-baseline { + align-items: baseline !important; + } + + .align-items-md-stretch { + align-items: stretch !important; + } + + .align-content-md-start { + align-content: flex-start !important; + } + + .align-content-md-end { + align-content: flex-end !important; + } + + .align-content-md-center { + align-content: center !important; + } + + .align-content-md-between { + align-content: space-between !important; + } + + .align-content-md-around { + align-content: space-around !important; + } + + .align-content-md-stretch { + align-content: stretch !important; + } + + .align-self-md-auto { + align-self: auto !important; + } + + .align-self-md-start { + align-self: flex-start !important; + } + + .align-self-md-end { + align-self: flex-end !important; + } + + .align-self-md-center { + align-self: center !important; + } + + .align-self-md-baseline { + align-self: baseline !important; + } + + .align-self-md-stretch { + align-self: stretch !important; + } + + .order-md-first { + order: -1 !important; + } + + .order-md-0 { + order: 0 !important; + } + + .order-md-1 { + order: 1 !important; + } + + .order-md-2 { + order: 2 !important; + } + + .order-md-3 { + order: 3 !important; + } + + .order-md-4 { + order: 4 !important; + } + + .order-md-5 { + order: 5 !important; + } + + .order-md-last { + order: 6 !important; + } + + .m-md-0 { + margin: 0 !important; + } + + .m-md-1 { + margin: 0.25rem !important; + } + + .m-md-2 { + margin: 0.5rem !important; + } + + .m-md-3 { + margin: 1rem !important; + } + + .m-md-4 { + margin: 1.5rem !important; + } + + .m-md-5 { + margin: 3rem !important; + } + + .m-md-auto { + margin: auto !important; + } + + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-md-0 { + margin-top: 0 !important; + } + + .mt-md-1 { + margin-top: 0.25rem !important; + } + + .mt-md-2 { + margin-top: 0.5rem !important; + } + + .mt-md-3 { + margin-top: 1rem !important; + } + + .mt-md-4 { + margin-top: 1.5rem !important; + } + + .mt-md-5 { + margin-top: 3rem !important; + } + + .mt-md-auto { + margin-top: auto !important; + } + + .me-md-0 { + margin-right: 0 !important; + } + + .me-md-1 { + margin-right: 0.25rem !important; + } + + .me-md-2 { + margin-right: 0.5rem !important; + } + + .me-md-3 { + margin-right: 1rem !important; + } + + .me-md-4 { + margin-right: 1.5rem !important; + } + + .me-md-5 { + margin-right: 3rem !important; + } + + .me-md-auto { + margin-right: auto !important; + } + + .mb-md-0 { + margin-bottom: 0 !important; + } + + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + + .mb-md-3 { + margin-bottom: 1rem !important; + } + + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + + .mb-md-5 { + margin-bottom: 3rem !important; + } + + .mb-md-auto { + margin-bottom: auto !important; + } + + .ms-md-0 { + margin-left: 0 !important; + } + + .ms-md-1 { + margin-left: 0.25rem !important; + } + + .ms-md-2 { + margin-left: 0.5rem !important; + } + + .ms-md-3 { + margin-left: 1rem !important; + } + + .ms-md-4 { + margin-left: 1.5rem !important; + } + + .ms-md-5 { + margin-left: 3rem !important; + } + + .ms-md-auto { + margin-left: auto !important; + } + + .m-md-n1 { + margin: -0.25rem !important; + } + + .m-md-n2 { + margin: -0.5rem !important; + } + + .m-md-n3 { + margin: -1rem !important; + } + + .m-md-n4 { + margin: -1.5rem !important; + } + + .m-md-n5 { + margin: -3rem !important; + } + + .mx-md-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; + } + + .mx-md-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; + } + + .mx-md-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; + } + + .mx-md-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; + } + + .mx-md-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; + } + + .my-md-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; + } + + .my-md-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; + } + + .my-md-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; + } + + .my-md-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; + } + + .my-md-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; + } + + .mt-md-n1 { + margin-top: -0.25rem !important; + } + + .mt-md-n2 { + margin-top: -0.5rem !important; + } + + .mt-md-n3 { + margin-top: -1rem !important; + } + + .mt-md-n4 { + margin-top: -1.5rem !important; + } + + .mt-md-n5 { + margin-top: -3rem !important; + } + + .me-md-n1 { + margin-right: -0.25rem !important; + } + + .me-md-n2 { + margin-right: -0.5rem !important; + } + + .me-md-n3 { + margin-right: -1rem !important; + } + + .me-md-n4 { + margin-right: -1.5rem !important; + } + + .me-md-n5 { + margin-right: -3rem !important; + } + + .mb-md-n1 { + margin-bottom: -0.25rem !important; + } + + .mb-md-n2 { + margin-bottom: -0.5rem !important; + } + + .mb-md-n3 { + margin-bottom: -1rem !important; + } + + .mb-md-n4 { + margin-bottom: -1.5rem !important; + } + + .mb-md-n5 { + margin-bottom: -3rem !important; + } + + .ms-md-n1 { + margin-left: -0.25rem !important; + } + + .ms-md-n2 { + margin-left: -0.5rem !important; + } + + .ms-md-n3 { + margin-left: -1rem !important; + } + + .ms-md-n4 { + margin-left: -1.5rem !important; + } + + .ms-md-n5 { + margin-left: -3rem !important; + } + + .p-md-0 { + padding: 0 !important; + } + + .p-md-1 { + padding: 0.25rem !important; + } + + .p-md-2 { + padding: 0.5rem !important; + } + + .p-md-3 { + padding: 1rem !important; + } + + .p-md-4 { + padding: 1.5rem !important; + } + + .p-md-5 { + padding: 3rem !important; + } + + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-md-0 { + padding-top: 0 !important; + } + + .pt-md-1 { + padding-top: 0.25rem !important; + } + + .pt-md-2 { + padding-top: 0.5rem !important; + } + + .pt-md-3 { + padding-top: 1rem !important; + } + + .pt-md-4 { + padding-top: 1.5rem !important; + } + + .pt-md-5 { + padding-top: 3rem !important; + } + + .pe-md-0 { + padding-right: 0 !important; + } + + .pe-md-1 { + padding-right: 0.25rem !important; + } + + .pe-md-2 { + padding-right: 0.5rem !important; + } + + .pe-md-3 { + padding-right: 1rem !important; + } + + .pe-md-4 { + padding-right: 1.5rem !important; + } + + .pe-md-5 { + padding-right: 3rem !important; + } + + .pb-md-0 { + padding-bottom: 0 !important; + } + + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + + .pb-md-3 { + padding-bottom: 1rem !important; + } + + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + + .pb-md-5 { + padding-bottom: 3rem !important; + } + + .ps-md-0 { + padding-left: 0 !important; + } + + .ps-md-1 { + padding-left: 0.25rem !important; + } + + .ps-md-2 { + padding-left: 0.5rem !important; + } + + .ps-md-3 { + padding-left: 1rem !important; + } + + .ps-md-4 { + padding-left: 1.5rem !important; + } + + .ps-md-5 { + padding-left: 3rem !important; + } + + .text-md-start { + text-align: left !important; + } + + .text-md-end { + text-align: right !important; + } + + .text-md-center { + text-align: center !important; + } +} +@media (min-width: 992px) { + .float-lg-start { + float: left !important; + } + + .float-lg-end { + float: right !important; + } + + .float-lg-none { + float: none !important; + } + + .d-lg-inline { + display: inline !important; + } + + .d-lg-inline-block { + display: inline-block !important; + } + + .d-lg-block { + display: block !important; + } + + .d-lg-grid { + display: grid !important; + } + + .d-lg-table { + display: table !important; + } + + .d-lg-table-row { + display: table-row !important; + } + + .d-lg-table-cell { + display: table-cell !important; + } + + .d-lg-flex { + display: flex !important; + } + + .d-lg-inline-flex { + display: inline-flex !important; + } + + .d-lg-none { + display: none !important; + } + + .flex-lg-fill { + flex: 1 1 auto !important; + } + + .flex-lg-row { + flex-direction: row !important; + } + + .flex-lg-column { + flex-direction: column !important; + } + + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-lg-wrap { + flex-wrap: wrap !important; + } + + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .gap-lg-0 { + gap: 0 !important; + } + + .gap-lg-1 { + gap: 0.25rem !important; + } + + .gap-lg-2 { + gap: 0.5rem !important; + } + + .gap-lg-3 { + gap: 1rem !important; + } + + .gap-lg-4 { + gap: 1.5rem !important; + } + + .gap-lg-5 { + gap: 3rem !important; + } + + .justify-content-lg-start { + justify-content: flex-start !important; + } + + .justify-content-lg-end { + justify-content: flex-end !important; + } + + .justify-content-lg-center { + justify-content: center !important; + } + + .justify-content-lg-between { + justify-content: space-between !important; + } + + .justify-content-lg-around { + justify-content: space-around !important; + } + + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + + .align-items-lg-start { + align-items: flex-start !important; + } + + .align-items-lg-end { + align-items: flex-end !important; + } + + .align-items-lg-center { + align-items: center !important; + } + + .align-items-lg-baseline { + align-items: baseline !important; + } + + .align-items-lg-stretch { + align-items: stretch !important; + } + + .align-content-lg-start { + align-content: flex-start !important; + } + + .align-content-lg-end { + align-content: flex-end !important; + } + + .align-content-lg-center { + align-content: center !important; + } + + .align-content-lg-between { + align-content: space-between !important; + } + + .align-content-lg-around { + align-content: space-around !important; + } + + .align-content-lg-stretch { + align-content: stretch !important; + } + + .align-self-lg-auto { + align-self: auto !important; + } + + .align-self-lg-start { + align-self: flex-start !important; + } + + .align-self-lg-end { + align-self: flex-end !important; + } + + .align-self-lg-center { + align-self: center !important; + } + + .align-self-lg-baseline { + align-self: baseline !important; + } + + .align-self-lg-stretch { + align-self: stretch !important; + } + + .order-lg-first { + order: -1 !important; + } + + .order-lg-0 { + order: 0 !important; + } + + .order-lg-1 { + order: 1 !important; + } + + .order-lg-2 { + order: 2 !important; + } + + .order-lg-3 { + order: 3 !important; + } + + .order-lg-4 { + order: 4 !important; + } + + .order-lg-5 { + order: 5 !important; + } + + .order-lg-last { + order: 6 !important; + } + + .m-lg-0 { + margin: 0 !important; + } + + .m-lg-1 { + margin: 0.25rem !important; + } + + .m-lg-2 { + margin: 0.5rem !important; + } + + .m-lg-3 { + margin: 1rem !important; + } + + .m-lg-4 { + margin: 1.5rem !important; + } + + .m-lg-5 { + margin: 3rem !important; + } + + .m-lg-auto { + margin: auto !important; + } + + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-lg-0 { + margin-top: 0 !important; + } + + .mt-lg-1 { + margin-top: 0.25rem !important; + } + + .mt-lg-2 { + margin-top: 0.5rem !important; + } + + .mt-lg-3 { + margin-top: 1rem !important; + } + + .mt-lg-4 { + margin-top: 1.5rem !important; + } + + .mt-lg-5 { + margin-top: 3rem !important; + } + + .mt-lg-auto { + margin-top: auto !important; + } + + .me-lg-0 { + margin-right: 0 !important; + } + + .me-lg-1 { + margin-right: 0.25rem !important; + } + + .me-lg-2 { + margin-right: 0.5rem !important; + } + + .me-lg-3 { + margin-right: 1rem !important; + } + + .me-lg-4 { + margin-right: 1.5rem !important; + } + + .me-lg-5 { + margin-right: 3rem !important; + } + + .me-lg-auto { + margin-right: auto !important; + } + + .mb-lg-0 { + margin-bottom: 0 !important; + } + + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + + .mb-lg-3 { + margin-bottom: 1rem !important; + } + + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + + .mb-lg-5 { + margin-bottom: 3rem !important; + } + + .mb-lg-auto { + margin-bottom: auto !important; + } + + .ms-lg-0 { + margin-left: 0 !important; + } + + .ms-lg-1 { + margin-left: 0.25rem !important; + } + + .ms-lg-2 { + margin-left: 0.5rem !important; + } + + .ms-lg-3 { + margin-left: 1rem !important; + } + + .ms-lg-4 { + margin-left: 1.5rem !important; + } + + .ms-lg-5 { + margin-left: 3rem !important; + } + + .ms-lg-auto { + margin-left: auto !important; + } + + .m-lg-n1 { + margin: -0.25rem !important; + } + + .m-lg-n2 { + margin: -0.5rem !important; + } + + .m-lg-n3 { + margin: -1rem !important; + } + + .m-lg-n4 { + margin: -1.5rem !important; + } + + .m-lg-n5 { + margin: -3rem !important; + } + + .mx-lg-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; + } + + .mx-lg-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; + } + + .mx-lg-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; + } + + .mx-lg-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; + } + + .mx-lg-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; + } + + .my-lg-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; + } + + .my-lg-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; + } + + .my-lg-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; + } + + .my-lg-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; + } + + .my-lg-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; + } + + .mt-lg-n1 { + margin-top: -0.25rem !important; + } + + .mt-lg-n2 { + margin-top: -0.5rem !important; + } + + .mt-lg-n3 { + margin-top: -1rem !important; + } + + .mt-lg-n4 { + margin-top: -1.5rem !important; + } + + .mt-lg-n5 { + margin-top: -3rem !important; + } + + .me-lg-n1 { + margin-right: -0.25rem !important; + } + + .me-lg-n2 { + margin-right: -0.5rem !important; + } + + .me-lg-n3 { + margin-right: -1rem !important; + } + + .me-lg-n4 { + margin-right: -1.5rem !important; + } + + .me-lg-n5 { + margin-right: -3rem !important; + } + + .mb-lg-n1 { + margin-bottom: -0.25rem !important; + } + + .mb-lg-n2 { + margin-bottom: -0.5rem !important; + } + + .mb-lg-n3 { + margin-bottom: -1rem !important; + } + + .mb-lg-n4 { + margin-bottom: -1.5rem !important; + } + + .mb-lg-n5 { + margin-bottom: -3rem !important; + } + + .ms-lg-n1 { + margin-left: -0.25rem !important; + } + + .ms-lg-n2 { + margin-left: -0.5rem !important; + } + + .ms-lg-n3 { + margin-left: -1rem !important; + } + + .ms-lg-n4 { + margin-left: -1.5rem !important; + } + + .ms-lg-n5 { + margin-left: -3rem !important; + } + + .p-lg-0 { + padding: 0 !important; + } + + .p-lg-1 { + padding: 0.25rem !important; + } + + .p-lg-2 { + padding: 0.5rem !important; + } + + .p-lg-3 { + padding: 1rem !important; + } + + .p-lg-4 { + padding: 1.5rem !important; + } + + .p-lg-5 { + padding: 3rem !important; + } + + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-lg-0 { + padding-top: 0 !important; + } + + .pt-lg-1 { + padding-top: 0.25rem !important; + } + + .pt-lg-2 { + padding-top: 0.5rem !important; + } + + .pt-lg-3 { + padding-top: 1rem !important; + } + + .pt-lg-4 { + padding-top: 1.5rem !important; + } + + .pt-lg-5 { + padding-top: 3rem !important; + } + + .pe-lg-0 { + padding-right: 0 !important; + } + + .pe-lg-1 { + padding-right: 0.25rem !important; + } + + .pe-lg-2 { + padding-right: 0.5rem !important; + } + + .pe-lg-3 { + padding-right: 1rem !important; + } + + .pe-lg-4 { + padding-right: 1.5rem !important; + } + + .pe-lg-5 { + padding-right: 3rem !important; + } + + .pb-lg-0 { + padding-bottom: 0 !important; + } + + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + + .pb-lg-3 { + padding-bottom: 1rem !important; + } + + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + + .pb-lg-5 { + padding-bottom: 3rem !important; + } + + .ps-lg-0 { + padding-left: 0 !important; + } + + .ps-lg-1 { + padding-left: 0.25rem !important; + } + + .ps-lg-2 { + padding-left: 0.5rem !important; + } + + .ps-lg-3 { + padding-left: 1rem !important; + } + + .ps-lg-4 { + padding-left: 1.5rem !important; + } + + .ps-lg-5 { + padding-left: 3rem !important; + } + + .text-lg-start { + text-align: left !important; + } + + .text-lg-end { + text-align: right !important; + } + + .text-lg-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .float-xl-start { + float: left !important; + } + + .float-xl-end { + float: right !important; + } + + .float-xl-none { + float: none !important; + } + + .d-xl-inline { + display: inline !important; + } + + .d-xl-inline-block { + display: inline-block !important; + } + + .d-xl-block { + display: block !important; + } + + .d-xl-grid { + display: grid !important; + } + + .d-xl-table { + display: table !important; + } + + .d-xl-table-row { + display: table-row !important; + } + + .d-xl-table-cell { + display: table-cell !important; + } + + .d-xl-flex { + display: flex !important; + } + + .d-xl-inline-flex { + display: inline-flex !important; + } + + .d-xl-none { + display: none !important; + } + + .flex-xl-fill { + flex: 1 1 auto !important; + } + + .flex-xl-row { + flex-direction: row !important; + } + + .flex-xl-column { + flex-direction: column !important; + } + + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xl-wrap { + flex-wrap: wrap !important; + } + + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .gap-xl-0 { + gap: 0 !important; + } + + .gap-xl-1 { + gap: 0.25rem !important; + } + + .gap-xl-2 { + gap: 0.5rem !important; + } + + .gap-xl-3 { + gap: 1rem !important; + } + + .gap-xl-4 { + gap: 1.5rem !important; + } + + .gap-xl-5 { + gap: 3rem !important; + } + + .justify-content-xl-start { + justify-content: flex-start !important; + } + + .justify-content-xl-end { + justify-content: flex-end !important; + } + + .justify-content-xl-center { + justify-content: center !important; + } + + .justify-content-xl-between { + justify-content: space-between !important; + } + + .justify-content-xl-around { + justify-content: space-around !important; + } + + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xl-start { + align-items: flex-start !important; + } + + .align-items-xl-end { + align-items: flex-end !important; + } + + .align-items-xl-center { + align-items: center !important; + } + + .align-items-xl-baseline { + align-items: baseline !important; + } + + .align-items-xl-stretch { + align-items: stretch !important; + } + + .align-content-xl-start { + align-content: flex-start !important; + } + + .align-content-xl-end { + align-content: flex-end !important; + } + + .align-content-xl-center { + align-content: center !important; + } + + .align-content-xl-between { + align-content: space-between !important; + } + + .align-content-xl-around { + align-content: space-around !important; + } + + .align-content-xl-stretch { + align-content: stretch !important; + } + + .align-self-xl-auto { + align-self: auto !important; + } + + .align-self-xl-start { + align-self: flex-start !important; + } + + .align-self-xl-end { + align-self: flex-end !important; + } + + .align-self-xl-center { + align-self: center !important; + } + + .align-self-xl-baseline { + align-self: baseline !important; + } + + .align-self-xl-stretch { + align-self: stretch !important; + } + + .order-xl-first { + order: -1 !important; + } + + .order-xl-0 { + order: 0 !important; + } + + .order-xl-1 { + order: 1 !important; + } + + .order-xl-2 { + order: 2 !important; + } + + .order-xl-3 { + order: 3 !important; + } + + .order-xl-4 { + order: 4 !important; + } + + .order-xl-5 { + order: 5 !important; + } + + .order-xl-last { + order: 6 !important; + } + + .m-xl-0 { + margin: 0 !important; + } + + .m-xl-1 { + margin: 0.25rem !important; + } + + .m-xl-2 { + margin: 0.5rem !important; + } + + .m-xl-3 { + margin: 1rem !important; + } + + .m-xl-4 { + margin: 1.5rem !important; + } + + .m-xl-5 { + margin: 3rem !important; + } + + .m-xl-auto { + margin: auto !important; + } + + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xl-0 { + margin-top: 0 !important; + } + + .mt-xl-1 { + margin-top: 0.25rem !important; + } + + .mt-xl-2 { + margin-top: 0.5rem !important; + } + + .mt-xl-3 { + margin-top: 1rem !important; + } + + .mt-xl-4 { + margin-top: 1.5rem !important; + } + + .mt-xl-5 { + margin-top: 3rem !important; + } + + .mt-xl-auto { + margin-top: auto !important; + } + + .me-xl-0 { + margin-right: 0 !important; + } + + .me-xl-1 { + margin-right: 0.25rem !important; + } + + .me-xl-2 { + margin-right: 0.5rem !important; + } + + .me-xl-3 { + margin-right: 1rem !important; + } + + .me-xl-4 { + margin-right: 1.5rem !important; + } + + .me-xl-5 { + margin-right: 3rem !important; + } + + .me-xl-auto { + margin-right: auto !important; + } + + .mb-xl-0 { + margin-bottom: 0 !important; + } + + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xl-3 { + margin-bottom: 1rem !important; + } + + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xl-5 { + margin-bottom: 3rem !important; + } + + .mb-xl-auto { + margin-bottom: auto !important; + } + + .ms-xl-0 { + margin-left: 0 !important; + } + + .ms-xl-1 { + margin-left: 0.25rem !important; + } + + .ms-xl-2 { + margin-left: 0.5rem !important; + } + + .ms-xl-3 { + margin-left: 1rem !important; + } + + .ms-xl-4 { + margin-left: 1.5rem !important; + } + + .ms-xl-5 { + margin-left: 3rem !important; + } + + .ms-xl-auto { + margin-left: auto !important; + } + + .m-xl-n1 { + margin: -0.25rem !important; + } + + .m-xl-n2 { + margin: -0.5rem !important; + } + + .m-xl-n3 { + margin: -1rem !important; + } + + .m-xl-n4 { + margin: -1.5rem !important; + } + + .m-xl-n5 { + margin: -3rem !important; + } + + .mx-xl-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; + } + + .mx-xl-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; + } + + .mx-xl-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; + } + + .mx-xl-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; + } + + .mx-xl-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; + } + + .my-xl-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; + } + + .my-xl-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; + } + + .my-xl-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; + } + + .my-xl-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; + } + + .my-xl-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; + } + + .mt-xl-n1 { + margin-top: -0.25rem !important; + } + + .mt-xl-n2 { + margin-top: -0.5rem !important; + } + + .mt-xl-n3 { + margin-top: -1rem !important; + } + + .mt-xl-n4 { + margin-top: -1.5rem !important; + } + + .mt-xl-n5 { + margin-top: -3rem !important; + } + + .me-xl-n1 { + margin-right: -0.25rem !important; + } + + .me-xl-n2 { + margin-right: -0.5rem !important; + } + + .me-xl-n3 { + margin-right: -1rem !important; + } + + .me-xl-n4 { + margin-right: -1.5rem !important; + } + + .me-xl-n5 { + margin-right: -3rem !important; + } + + .mb-xl-n1 { + margin-bottom: -0.25rem !important; + } + + .mb-xl-n2 { + margin-bottom: -0.5rem !important; + } + + .mb-xl-n3 { + margin-bottom: -1rem !important; + } + + .mb-xl-n4 { + margin-bottom: -1.5rem !important; + } + + .mb-xl-n5 { + margin-bottom: -3rem !important; + } + + .ms-xl-n1 { + margin-left: -0.25rem !important; + } + + .ms-xl-n2 { + margin-left: -0.5rem !important; + } + + .ms-xl-n3 { + margin-left: -1rem !important; + } + + .ms-xl-n4 { + margin-left: -1.5rem !important; + } + + .ms-xl-n5 { + margin-left: -3rem !important; + } + + .p-xl-0 { + padding: 0 !important; + } + + .p-xl-1 { + padding: 0.25rem !important; + } + + .p-xl-2 { + padding: 0.5rem !important; + } + + .p-xl-3 { + padding: 1rem !important; + } + + .p-xl-4 { + padding: 1.5rem !important; + } + + .p-xl-5 { + padding: 3rem !important; + } + + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xl-0 { + padding-top: 0 !important; + } + + .pt-xl-1 { + padding-top: 0.25rem !important; + } + + .pt-xl-2 { + padding-top: 0.5rem !important; + } + + .pt-xl-3 { + padding-top: 1rem !important; + } + + .pt-xl-4 { + padding-top: 1.5rem !important; + } + + .pt-xl-5 { + padding-top: 3rem !important; + } + + .pe-xl-0 { + padding-right: 0 !important; + } + + .pe-xl-1 { + padding-right: 0.25rem !important; + } + + .pe-xl-2 { + padding-right: 0.5rem !important; + } + + .pe-xl-3 { + padding-right: 1rem !important; + } + + .pe-xl-4 { + padding-right: 1.5rem !important; + } + + .pe-xl-5 { + padding-right: 3rem !important; + } + + .pb-xl-0 { + padding-bottom: 0 !important; + } + + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xl-3 { + padding-bottom: 1rem !important; + } + + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xl-5 { + padding-bottom: 3rem !important; + } + + .ps-xl-0 { + padding-left: 0 !important; + } + + .ps-xl-1 { + padding-left: 0.25rem !important; + } + + .ps-xl-2 { + padding-left: 0.5rem !important; + } + + .ps-xl-3 { + padding-left: 1rem !important; + } + + .ps-xl-4 { + padding-left: 1.5rem !important; + } + + .ps-xl-5 { + padding-left: 3rem !important; + } + + .text-xl-start { + text-align: left !important; + } + + .text-xl-end { + text-align: right !important; + } + + .text-xl-center { + text-align: center !important; + } +} +@media (min-width: 1400px) { + .float-xxl-start { + float: left !important; + } + + .float-xxl-end { + float: right !important; + } + + .float-xxl-none { + float: none !important; + } + + .d-xxl-inline { + display: inline !important; + } + + .d-xxl-inline-block { + display: inline-block !important; + } + + .d-xxl-block { + display: block !important; + } + + .d-xxl-grid { + display: grid !important; + } + + .d-xxl-table { + display: table !important; + } + + .d-xxl-table-row { + display: table-row !important; + } + + .d-xxl-table-cell { + display: table-cell !important; + } + + .d-xxl-flex { + display: flex !important; + } + + .d-xxl-inline-flex { + display: inline-flex !important; + } + + .d-xxl-none { + display: none !important; + } + + .flex-xxl-fill { + flex: 1 1 auto !important; + } + + .flex-xxl-row { + flex-direction: row !important; + } + + .flex-xxl-column { + flex-direction: column !important; + } + + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + + .gap-xxl-0 { + gap: 0 !important; + } + + .gap-xxl-1 { + gap: 0.25rem !important; + } + + .gap-xxl-2 { + gap: 0.5rem !important; + } + + .gap-xxl-3 { + gap: 1rem !important; + } + + .gap-xxl-4 { + gap: 1.5rem !important; + } + + .gap-xxl-5 { + gap: 3rem !important; + } + + .justify-content-xxl-start { + justify-content: flex-start !important; + } + + .justify-content-xxl-end { + justify-content: flex-end !important; + } + + .justify-content-xxl-center { + justify-content: center !important; + } + + .justify-content-xxl-between { + justify-content: space-between !important; + } + + .justify-content-xxl-around { + justify-content: space-around !important; + } + + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + + .align-items-xxl-start { + align-items: flex-start !important; + } + + .align-items-xxl-end { + align-items: flex-end !important; + } + + .align-items-xxl-center { + align-items: center !important; + } + + .align-items-xxl-baseline { + align-items: baseline !important; + } + + .align-items-xxl-stretch { + align-items: stretch !important; + } + + .align-content-xxl-start { + align-content: flex-start !important; + } + + .align-content-xxl-end { + align-content: flex-end !important; + } + + .align-content-xxl-center { + align-content: center !important; + } + + .align-content-xxl-between { + align-content: space-between !important; + } + + .align-content-xxl-around { + align-content: space-around !important; + } + + .align-content-xxl-stretch { + align-content: stretch !important; + } + + .align-self-xxl-auto { + align-self: auto !important; + } + + .align-self-xxl-start { + align-self: flex-start !important; + } + + .align-self-xxl-end { + align-self: flex-end !important; + } + + .align-self-xxl-center { + align-self: center !important; + } + + .align-self-xxl-baseline { + align-self: baseline !important; + } + + .align-self-xxl-stretch { + align-self: stretch !important; + } + + .order-xxl-first { + order: -1 !important; + } + + .order-xxl-0 { + order: 0 !important; + } + + .order-xxl-1 { + order: 1 !important; + } + + .order-xxl-2 { + order: 2 !important; + } + + .order-xxl-3 { + order: 3 !important; + } + + .order-xxl-4 { + order: 4 !important; + } + + .order-xxl-5 { + order: 5 !important; + } + + .order-xxl-last { + order: 6 !important; + } + + .m-xxl-0 { + margin: 0 !important; + } + + .m-xxl-1 { + margin: 0.25rem !important; + } + + .m-xxl-2 { + margin: 0.5rem !important; + } + + .m-xxl-3 { + margin: 1rem !important; + } + + .m-xxl-4 { + margin: 1.5rem !important; + } + + .m-xxl-5 { + margin: 3rem !important; + } + + .m-xxl-auto { + margin: auto !important; + } + + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + + .mt-xxl-0 { + margin-top: 0 !important; + } + + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + + .mt-xxl-3 { + margin-top: 1rem !important; + } + + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + + .mt-xxl-5 { + margin-top: 3rem !important; + } + + .mt-xxl-auto { + margin-top: auto !important; + } + + .me-xxl-0 { + margin-right: 0 !important; + } + + .me-xxl-1 { + margin-right: 0.25rem !important; + } + + .me-xxl-2 { + margin-right: 0.5rem !important; + } + + .me-xxl-3 { + margin-right: 1rem !important; + } + + .me-xxl-4 { + margin-right: 1.5rem !important; + } + + .me-xxl-5 { + margin-right: 3rem !important; + } + + .me-xxl-auto { + margin-right: auto !important; + } + + .mb-xxl-0 { + margin-bottom: 0 !important; + } + + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + + .mb-xxl-auto { + margin-bottom: auto !important; + } + + .ms-xxl-0 { + margin-left: 0 !important; + } + + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + + .ms-xxl-3 { + margin-left: 1rem !important; + } + + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + + .ms-xxl-5 { + margin-left: 3rem !important; + } + + .ms-xxl-auto { + margin-left: auto !important; + } + + .m-xxl-n1 { + margin: -0.25rem !important; + } + + .m-xxl-n2 { + margin: -0.5rem !important; + } + + .m-xxl-n3 { + margin: -1rem !important; + } + + .m-xxl-n4 { + margin: -1.5rem !important; + } + + .m-xxl-n5 { + margin: -3rem !important; + } + + .mx-xxl-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; + } + + .mx-xxl-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; + } + + .mx-xxl-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; + } + + .mx-xxl-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; + } + + .mx-xxl-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; + } + + .my-xxl-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; + } + + .my-xxl-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; + } + + .my-xxl-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; + } + + .my-xxl-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; + } + + .my-xxl-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; + } + + .mt-xxl-n1 { + margin-top: -0.25rem !important; + } + + .mt-xxl-n2 { + margin-top: -0.5rem !important; + } + + .mt-xxl-n3 { + margin-top: -1rem !important; + } + + .mt-xxl-n4 { + margin-top: -1.5rem !important; + } + + .mt-xxl-n5 { + margin-top: -3rem !important; + } + + .me-xxl-n1 { + margin-right: -0.25rem !important; + } + + .me-xxl-n2 { + margin-right: -0.5rem !important; + } + + .me-xxl-n3 { + margin-right: -1rem !important; + } + + .me-xxl-n4 { + margin-right: -1.5rem !important; + } + + .me-xxl-n5 { + margin-right: -3rem !important; + } + + .mb-xxl-n1 { + margin-bottom: -0.25rem !important; + } + + .mb-xxl-n2 { + margin-bottom: -0.5rem !important; + } + + .mb-xxl-n3 { + margin-bottom: -1rem !important; + } + + .mb-xxl-n4 { + margin-bottom: -1.5rem !important; + } + + .mb-xxl-n5 { + margin-bottom: -3rem !important; + } + + .ms-xxl-n1 { + margin-left: -0.25rem !important; + } + + .ms-xxl-n2 { + margin-left: -0.5rem !important; + } + + .ms-xxl-n3 { + margin-left: -1rem !important; + } + + .ms-xxl-n4 { + margin-left: -1.5rem !important; + } + + .ms-xxl-n5 { + margin-left: -3rem !important; + } + + .p-xxl-0 { + padding: 0 !important; + } + + .p-xxl-1 { + padding: 0.25rem !important; + } + + .p-xxl-2 { + padding: 0.5rem !important; + } + + .p-xxl-3 { + padding: 1rem !important; + } + + .p-xxl-4 { + padding: 1.5rem !important; + } + + .p-xxl-5 { + padding: 3rem !important; + } + + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + + .pt-xxl-0 { + padding-top: 0 !important; + } + + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + + .pt-xxl-3 { + padding-top: 1rem !important; + } + + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + + .pt-xxl-5 { + padding-top: 3rem !important; + } + + .pe-xxl-0 { + padding-right: 0 !important; + } + + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + + .pe-xxl-3 { + padding-right: 1rem !important; + } + + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + + .pe-xxl-5 { + padding-right: 3rem !important; + } + + .pb-xxl-0 { + padding-bottom: 0 !important; + } + + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + + .ps-xxl-0 { + padding-left: 0 !important; + } + + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + + .ps-xxl-3 { + padding-left: 1rem !important; + } + + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + + .ps-xxl-5 { + padding-left: 3rem !important; + } + + .text-xxl-start { + text-align: left !important; + } + + .text-xxl-end { + text-align: right !important; + } + + .text-xxl-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .fs-1 { + font-size: 2.5rem !important; + } + + .fs-2 { + font-size: 2rem !important; + } + + .fs-3 { + font-size: 1.75rem !important; + } + + .fs-4 { + font-size: 1.5rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + + .d-print-inline-block { + display: inline-block !important; + } + + .d-print-block { + display: block !important; + } + + .d-print-grid { + display: grid !important; + } + + .d-print-table { + display: table !important; + } + + .d-print-table-row { + display: table-row !important; + } + + .d-print-table-cell { + display: table-cell !important; + } + + .d-print-flex { + display: flex !important; + } + + .d-print-inline-flex { + display: inline-flex !important; + } + + .d-print-none { + display: none !important; + } +} +.feature { + display: inline-flex; + align-items: center; + justify-content: center; + height: 4rem; + width: 4rem; + font-size: 2rem; +} \ No newline at end of file diff --git a/seoulyeok-message-relay/application/src/main/resources/static/js/scripts.js b/seoulyeok-message-relay/application/src/main/resources/static/js/scripts.js new file mode 100644 index 0000000..7a19fc7 --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/static/js/scripts.js @@ -0,0 +1,7 @@ +/*! +* Start Bootstrap - Heroic Features v5.0.1 (https://startbootstrap.com/template/heroic-features) +* Copyright 2013-2021 Start Bootstrap +* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-heroic-features/blob/master/LICENSE) +*/ +// This file is intentionally blank +// Use this file to add JavaScript to your project \ No newline at end of file diff --git a/seoulyeok-message-relay/application/src/main/resources/templates/dashboard.html b/seoulyeok-message-relay/application/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000..6a6e24c --- /dev/null +++ b/seoulyeok-message-relay/application/src/main/resources/templates/dashboard.html @@ -0,0 +1,47 @@ + + + + + + + + + Message Relay Monitoring + + + + + + + +

Message Relay Monitoring

+ + + + + + + + + + + + + + + + + + + + + + + +

+ + +
ID목적지 이름목적지 타입목적지 상태최신 수신한 날짜Lag
+
+ diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/MessageRelayApplicationTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/MessageRelayApplicationTest.java new file mode 100644 index 0000000..b97a2fb --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/MessageRelayApplicationTest.java @@ -0,0 +1,5 @@ +package org.masil.seoulyeok.events.relay; + +class MessageRelayApplicationTest { + +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationFetcherTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationFetcherTest.java new file mode 100644 index 0000000..483d9d0 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationFetcherTest.java @@ -0,0 +1,35 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class DestinationFetcherTest { + + @InjectMocks + DestinationFetcher sut; + + @Mock + LoadDestinationPort loadPort; + + @Test + void Destination_을_조회하면_active_상태의_destination_을_가져온다() { + List destinations = new ArrayList<>(); + given(loadPort.loadAllActiveDestination()).willReturn(destinations); + + List actual = sut.findAllActiveDestination(); + + assertThat(actual).isEqualTo(actual); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChangerTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChangerTest.java new file mode 100644 index 0000000..53f5f15 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChangerTest.java @@ -0,0 +1,41 @@ +package org.masil.seoulyeok.events.relay.application; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.relay.port.out.CommandDestinationPort; +import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DestinationStatusChangerTest { + public static final DestinationId ANY_DESTINATION_ID = DestinationId.of(1L); + public static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, Address.of("some address"), + DestinationType.SIMPLE_QUEUE); + + @InjectMocks + DestinationStatusChanger sut; + + @Mock + LoadDestinationPort loadPort; + + @Mock + CommandDestinationPort commandPort; + + @Test + void inactive_를_요청하면_조회후_status_를_변경하고_저장한다() { + given(loadPort.loadById(ANY_DESTINATION_ID)).willReturn(ANY_DESTINATION); + + sut.inactivateBy(ANY_DESTINATION_ID); + + verify(commandPort).update(ANY_DESTINATION); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdaterTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdaterTest.java new file mode 100644 index 0000000..a724b32 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdaterTest.java @@ -0,0 +1,33 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.relay.port.out.RelayMessageMarkPort; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class RelaidMessageUpdaterTest { + + private static final LocalDateTime ANY_PUBLISHED_AT = LocalDateTime.of(2022, 1, 1, 1, 1); + private static final RelayMessageId ANY_RELAY_MESSAGE_ID = RelayMessageId.of(1L); + @InjectMocks + RelaidMessageUpdater sut; + + @Mock + RelayMessageMarkPort port; + + @Test + void markTest() { + + sut.setMarkRelaidMessage(ANY_RELAY_MESSAGE_ID, ANY_PUBLISHED_AT); + + verify(port).setReliedMark(ANY_RELAY_MESSAGE_ID, ANY_PUBLISHED_AT); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelayProcessorTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelayProcessorTest.java new file mode 100644 index 0000000..d483cc0 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/RelayProcessorTest.java @@ -0,0 +1,109 @@ +package org.masil.seoulyeok.events.relay.application; + +import com.likelen.identifier.generator.DummyLongValueGenerator; +import com.likelen.identifier.generator.IdGeneratorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.destination.*; +import org.masil.seoulyeok.events.publisher.DefaultPublishResult; +import org.masil.seoulyeok.events.relay.application.alert.SlackAlertNotifier; +import org.masil.seoulyeok.events.relay.application.config.RelayProcessorConfig; +import org.masil.seoulyeok.events.relay.application.publisher.PublisherContainers; +import org.masil.seoulyeok.events.relaymessage.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class RelayProcessorTest { + + public static final LocalDateTime ANY_PUBLISHED_AT = LocalDateTime.of(2022, 1, 1, 1, 1); + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(1L); + public static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, Address.of("any address"), + DestinationType.SIMPLE_QUEUE, DestinationStatus.ACTIVE); + public static final MessagePayload ANY_MESSAGE_PAYLOAD = MessagePayload.of("messagePayload1"); + public static final PayloadVersion ANY_PAYLOAD_VERSION = PayloadVersion.ZERO; + @InjectMocks + RelayProcessor sut; + + @Mock + DestinationFetcher fetcher; + + @Mock + TopMessageFinder topMessageFinder; + + @Mock + PublisherContainers publisherContainers; + + @Mock + RelayProcessorConfig relayProcessorConfig; + + @Mock + RelaidMessageUpdater updater; + @Mock + DestinationStatusChanger changer; + @Mock + SlackAlertNotifier notifier; + + + @BeforeAll + static void beforeAll() { + new IdGeneratorFactory(new DummyLongValueGenerator()); + + } + + @Test + void relay_success() { + RelayMessage message = RelayMessage.create(ANY_MESSAGE_PAYLOAD, ANY_DESTINATION_ID, ANY_PAYLOAD_VERSION); + TopRelayMessage topRelayMessage = TopRelayMessage.createByMessage(message); + RelayMessageId id = message.getId(); + + DefaultPublishResult success = DefaultPublishResult.success(id, ANY_DESTINATION_ID, ANY_PUBLISHED_AT); + + given(fetcher.findAllActiveDestination()).willReturn(getDestinations()); + given(topMessageFinder.find(ANY_DESTINATION_ID)).willReturn(topRelayMessage); + given(publisherContainers.publish(topRelayMessage, ANY_DESTINATION)).willReturn(success); + given(relayProcessorConfig.getBatchSize()).willReturn(1); + + sut.doProcess(); + + verify(updater).setMarkRelaidMessage(eq(id), any(LocalDateTime.class)); + } + + @Test + void relay_fail() { + RelayMessage message = RelayMessage.create(ANY_MESSAGE_PAYLOAD, ANY_DESTINATION_ID, ANY_PAYLOAD_VERSION); + TopRelayMessage topRelayMessage = TopRelayMessage.createByMessage(message); + RelayMessageId id = message.getId(); + + DefaultPublishResult fail = DefaultPublishResult.fail(id, ANY_DESTINATION_ID, ANY_PUBLISHED_AT); + + given(fetcher.findAllActiveDestination()).willReturn(getDestinations()); + given(topMessageFinder.find(ANY_DESTINATION_ID)).willReturn(topRelayMessage); + given(publisherContainers.publish(topRelayMessage, ANY_DESTINATION)).willReturn(fail); + given(relayProcessorConfig.getBatchSize()).willReturn(1); + + sut.doProcess(); + + verifyNoInteractions(updater); + verify(changer).inactivateBy(ANY_DESTINATION_ID); + verify(notifier).doNotify(id, ANY_DESTINATION_ID); + } + + private List getDestinations() { + List destinations = new ArrayList<>(); + destinations.add(ANY_DESTINATION); + return destinations; + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulerClientTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulerClientTest.java new file mode 100644 index 0000000..868039e --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulerClientTest.java @@ -0,0 +1,26 @@ +package org.masil.seoulyeok.events.relay.application; + +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SchedulerClientTest { + + @InjectMocks + SchedulerClient sut; + + @Mock + RelayProcessor relayProcessor; + + @Test + void execute() { + sut.execute(); + + verify(relayProcessor).doProcess(); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulingClientTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulingClientTest.java new file mode 100644 index 0000000..539968a --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/SchedulingClientTest.java @@ -0,0 +1,30 @@ +package org.masil.seoulyeok.events.relay.application; + +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + +import java.time.Duration; + +import org.masil.seoulyeok.events.relay.application.config.SchedulerConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +@SpringJUnitConfig(SchedulerConfig.class) +class SchedulingClientTest { + + @SpyBean + SchedulerClient sut; + + @MockBean + RelayProcessor processor; + + @Test + void schedulingTest() { + await() + .atMost(Duration.ofSeconds(1L)) + .untilAsserted(() -> verify(sut, atLeast(1)).execute()); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/TopMessageFinderTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/TopMessageFinderTest.java new file mode 100644 index 0000000..de78565 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/TopMessageFinderTest.java @@ -0,0 +1,36 @@ +package org.masil.seoulyeok.events.relay.application; + +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.relay.port.out.QueryRelayMessagePort; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.BDDMockito.given; + + +@ExtendWith(MockitoExtension.class) +class TopMessageFinderTest { + + @InjectMocks + TopMessageFinder sut; + + @Mock + QueryRelayMessagePort port; + + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(1L); + private static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, Address.of("XX"), DestinationType.SIMPLE_QUEUE); + + @Test + void name() { + given(port.existsByDestinationId(ANY_DESTINATION_ID)).willReturn(true); + + + sut.find(ANY_DESTINATION_ID); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainersTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainersTest.java new file mode 100644 index 0000000..ef9b648 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/PublisherContainersTest.java @@ -0,0 +1,90 @@ +package org.masil.seoulyeok.events.relay.application.publisher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.publisher.DefaultPublishResult; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.publisher.PublisherContainer; +import org.masil.seoulyeok.events.relaymessage.MessagePayload; +import org.masil.seoulyeok.events.relaymessage.PayloadVersion; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.NoSuchElementException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.masil.seoulyeok.events.destination.DestinationType.SIMPLE_QUEUE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PublisherContainersTest { + + private static final RelayMessageId ANY_RELAY_MESSAGE_ID = RelayMessageId.of(1L); + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(2L); + private static final LocalDateTime PUBLISHED_AT = LocalDateTime.now(); + private static final TopRelayMessage ANY_TOP_RELAY_MESSAGE = new TopRelayMessage(RelayMessageId.of(1L),DestinationId.of(1L), MessagePayload.of("xx"), LocalDateTime.now(), null, PayloadVersion.ZERO); + + private static final DefaultPublishResult SUCCESS_PUBLISH_RESULT = DefaultPublishResult.success( + ANY_RELAY_MESSAGE_ID, + ANY_DESTINATION_ID, + PUBLISHED_AT); + public static final Address ANY_ADDRESS = Address.of("ANY_ADDRESS"); + public static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, ANY_ADDRESS, SIMPLE_QUEUE); + + @InjectMocks + PublisherContainers sut; + + @Mock + PublisherContainer container; + + @Test + void 최초의_컨테이너_크기는_0이다() { + assertThat(sut.getContainerSize()).isZero(); + + sut.add(container); + + assertThat(sut.getContainerSize()).isEqualTo(1); + } + + @Test + void SIMPLE_QUEUE_로_publish_하면_호출된다() { + given(container.isSupportedType(SIMPLE_QUEUE)).willReturn(true); + given(container.publish(ANY_TOP_RELAY_MESSAGE, ANY_DESTINATION)).willReturn(SUCCESS_PUBLISH_RESULT); + sut.add(container); + sut.publish(ANY_TOP_RELAY_MESSAGE, ANY_DESTINATION); + + verify(container).publish(any(TopRelayMessage.class), any(DestinationTarget.class)); + } + + @Test + void SIMPLE_QUEUE_로_publish_하면_결과를_반환한다() { + given(container.isSupportedType(SIMPLE_QUEUE)).willReturn(true); + given(container.publish(ANY_TOP_RELAY_MESSAGE, ANY_DESTINATION)).willReturn(SUCCESS_PUBLISH_RESULT); + sut.add(container); + + PublishResult actual = sut.publish(ANY_TOP_RELAY_MESSAGE, ANY_DESTINATION); + + assertThat(actual.getRelayMessageId()).isEqualTo(ANY_RELAY_MESSAGE_ID); + assertThat(actual.isSuccess()).isTrue(); + } + + @Test + void factory_가_관리하는_container_에_찾고자_하는_containerType_이_없다면_예외를_반환한다() { + given(container.isSupportedType(SIMPLE_QUEUE)).willReturn(false); + sut.add(container); + + assertThatThrownBy(() -> sut.publish(ANY_TOP_RELAY_MESSAGE, ANY_DESTINATION)).isInstanceOf( + NoSuchElementException.class); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainerTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainerTest.java new file mode 100644 index 0000000..5a6db92 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/SimpleQueuePublisherContainerTest.java @@ -0,0 +1,53 @@ +package org.masil.seoulyeok.events.relay.application.publisher.simplequeue; + +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relay.application.publisher.simplequeue.aws.AmazonStandardSQSDestinationPublisher; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.masil.seoulyeok.events.destination.DestinationType.SIMPLE_QUEUE; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SimpleQueuePublisherContainerTest { + + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(2L); + private static final TopRelayMessage TOP_RELAY_MESSAGE = null; + private static final Address ANY_ADDRESS = Address.of("ANY_ADDRESS"); + private static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, ANY_ADDRESS, SIMPLE_QUEUE); + + SimpleQueuePublisherContainer sut; + + @Mock + AmazonStandardSQSDestinationPublisher amazonStandardSQSDestinationPublisher; + + @BeforeEach + void setUp() { + sut = new SimpleQueuePublisherContainer(); + } + + @Test + void relay_를_publish_하면_적절한_publisher_가_동작한다() { + sut.add(amazonStandardSQSDestinationPublisher); + + sut.publish(TOP_RELAY_MESSAGE, ANY_DESTINATION); + + verify(amazonStandardSQSDestinationPublisher).execute(TOP_RELAY_MESSAGE, ANY_DESTINATION); + } + + @Test + void publisher_를_추가할_수_있다() { + assertThat(sut.getContainerSize()).isZero(); + + sut.add(amazonStandardSQSDestinationPublisher); + + assertThat(sut.getContainerSize()).isEqualTo(1); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisherTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisherTest.java new file mode 100644 index 0000000..616b71f --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/simplequeue/aws/AmazonStandardSQSDestinationPublisherTest.java @@ -0,0 +1,58 @@ +package org.masil.seoulyeok.events.relay.application.publisher.simplequeue.aws; + +import io.awspring.cloud.messaging.core.QueueMessagingTemplate; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.publisher.PublishResult; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.MessagingException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.masil.seoulyeok.events.destination.DestinationType.SIMPLE_QUEUE; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; + +@ExtendWith(MockitoExtension.class) +class AmazonStandardSQSDestinationPublisherTest { + + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(2L); + private static final Address ANY_ADDRESS = Address.of("ANY_ADDRESS"); + + private static final TopRelayMessage TOP_RELAY_MESSAGE = null; + public static final Destination ANY_DESTINATION = Destination.of(ANY_DESTINATION_ID, ANY_ADDRESS, SIMPLE_QUEUE); + + @InjectMocks + AmazonStandardSQSDestinationPublisher sut; + + @Mock + QueueMessagingTemplate queueMessagingTemplate; + + @Test + @Disabled(value = "sqs 실제 연결하지 않았기 때문에 임시 주석 처리") + void Destination_으로_메시지_발행에_성공하면_결과는_true() { + doNothing().when(queueMessagingTemplate) + .convertAndSend(ANY_DESTINATION.getAddress().getValue(), TOP_RELAY_MESSAGE); + + PublishResult actual = sut.execute(TOP_RELAY_MESSAGE, ANY_DESTINATION); + + assertThat(actual.isSuccess()).isTrue(); + } + + @Test + @Disabled + void Destination_으로_메시지_발행에_실패하면_결과는_false() { + doThrow(new MessagingException("")).when(queueMessagingTemplate) + .convertAndSend(ANY_DESTINATION.getAddress().getValue(), TOP_RELAY_MESSAGE); + + PublishResult actual = sut.execute(TOP_RELAY_MESSAGE, ANY_DESTINATION); + + assertThat(actual.isSuccess()).isFalse(); + } +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainerTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainerTest.java new file mode 100644 index 0000000..5402ef6 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/application/publisher/subscribequeue/SubscribeQueuePublisherContainerTest.java @@ -0,0 +1,4 @@ +package org.masil.seoulyeok.events.relay.application.publisher.subscribequeue; + +class SubscribeQueuePublisherContainerTest { +} diff --git a/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/web/DashBoardControllerTest.java b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/web/DashBoardControllerTest.java new file mode 100644 index 0000000..4e019c3 --- /dev/null +++ b/seoulyeok-message-relay/application/src/test/java/org/masil/seoulyeok/events/relay/web/DashBoardControllerTest.java @@ -0,0 +1,61 @@ +package org.masil.seoulyeok.events.relay.web; + +import org.masil.seoulyeok.events.destination.Address; +import org.masil.seoulyeok.events.destination.Destination; +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relay.port.out.LoadViewDestinationPort; +import org.masil.seoulyeok.events.relay.port.out.LoadViewRelayMessagePort; +import org.masil.seoulyeok.events.relay.web.model.DestinationView; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.masil.seoulyeok.events.destination.DestinationStatus.ACTIVE; +import static org.masil.seoulyeok.events.destination.DestinationType.SIMPLE_QUEUE; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@WebMvcTest(controllers = DashBoardController.class) +class DashBoardControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + LoadViewDestinationPort destinationPort; + + @MockBean + LoadViewRelayMessagePort loadViewRelayMessagePort; + private static final DestinationId ANY_DESTINATION_ID = DestinationId.of(1L); + private static final LocalDateTime ANY_LOCALDATETIME = LocalDateTime.of(2022, 1, 1, 1, 1); + private static final Address ANY_ADDRESS = Address.of("address"); + + @Test + void destinationView() throws Exception { + List destinations = new ArrayList<>(); + destinations.add(Destination.of(ANY_DESTINATION_ID, ANY_ADDRESS, SIMPLE_QUEUE)); + + List destinationViews = new ArrayList<>(); + + destinationViews.add(DestinationView.of(1L, "address", SIMPLE_QUEUE, ACTIVE, 10L, ANY_LOCALDATETIME)); + + given(destinationPort.findAllDestination()).willReturn(destinations); + given(loadViewRelayMessagePort.getLatestReliedMessageBy(ANY_DESTINATION_ID)).willReturn(ANY_LOCALDATETIME); + given(loadViewRelayMessagePort.getLegByDestinationId(ANY_DESTINATION_ID)).willReturn(10L); + + mockMvc.perform(get("/dashboard")) + .andExpect(view().name("dashboard")) + .andExpect(model().attribute("destinationViews", destinationViews)) + .andReturn(); + } + + +} diff --git a/seoulyeok-message-relay/build.gradle b/seoulyeok-message-relay/build.gradle new file mode 100644 index 0000000..403ba65 --- /dev/null +++ b/seoulyeok-message-relay/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'org.springframework.boot' version '2.6.7' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id "io.freefair.lombok" version "6.4.3" +} + +group = 'org.masil.seoulyeok.message-relay' + +allprojects { + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } +} diff --git a/seoulyeok-message-relay/domain/build.gradle b/seoulyeok-message-relay/domain/build.gradle new file mode 100644 index 0000000..bc140c5 --- /dev/null +++ b/seoulyeok-message-relay/domain/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java-library' + id "io.freefair.lombok" +} + +group 'org.masil.seoulyeok.events.relay' +version '0.1.0' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':refs:destination') + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation(platform('org.junit:junit-bom:5.7.1')) + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation 'org.assertj:assertj-core:3.20.2' + testImplementation 'org.mockito:mockito-junit-jupiter:4.0.0' + testImplementation 'org.mockito:mockito-inline:4.6.1' + + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + testImplementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' +} + +test { + useJUnitPlatform() +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DefaultPublishResult.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DefaultPublishResult.java new file mode 100644 index 0000000..a7de945 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DefaultPublishResult.java @@ -0,0 +1,46 @@ +package org.masil.seoulyeok.events.publisher; + +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; +import lombok.AllArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@ToString +@AllArgsConstructor +public class DefaultPublishResult implements PublishResult { + + public static DefaultPublishResult success(RelayMessageId relayMessageId, DestinationId destinationId, LocalDateTime publishedAt) { + return new DefaultPublishResult(relayMessageId, destinationId, publishedAt, true); + } + + public static DefaultPublishResult fail(RelayMessageId relayMessageId, DestinationId destinationId, LocalDateTime publishedAt) { + return new DefaultPublishResult(relayMessageId, destinationId, publishedAt, false); + } + + private final RelayMessageId relayMessageId; + private final DestinationId destinationId; + private final LocalDateTime publishedAt; + private final boolean success; + + @Override + public RelayMessageId getRelayMessageId() { + return relayMessageId; + } + + @Override + public boolean isSuccess() { + return success; + } + + @Override + public DestinationId getDestinationId() { + return destinationId; + } + + @Override + public LocalDateTime getPublishedAt() { + return publishedAt; + } +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DestinationPublisher.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DestinationPublisher.java new file mode 100644 index 0000000..6b289d6 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/DestinationPublisher.java @@ -0,0 +1,8 @@ +package org.masil.seoulyeok.events.publisher; + +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +public interface DestinationPublisher { + PublishResult execute(TopRelayMessage message, DestinationTarget destination); +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublishResult.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublishResult.java new file mode 100644 index 0000000..d35afc0 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublishResult.java @@ -0,0 +1,15 @@ +package org.masil.seoulyeok.events.publisher; + +import org.masil.seoulyeok.events.destination.DestinationId; +import org.masil.seoulyeok.events.relaymessage.RelayMessageId; + +import java.time.LocalDateTime; + +public interface PublishResult { + + RelayMessageId getRelayMessageId(); + boolean isSuccess(); + DestinationId getDestinationId(); + LocalDateTime getPublishedAt(); + +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublisherContainer.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublisherContainer.java new file mode 100644 index 0000000..0827935 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/publisher/PublisherContainer.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.publisher; + +import org.masil.seoulyeok.events.destination.DestinationTarget; +import org.masil.seoulyeok.events.destination.DestinationType; +import org.masil.seoulyeok.events.relaymessage.TopRelayMessage; + +public interface PublisherContainer { + PublishResult publish(TopRelayMessage message, DestinationTarget destination); + + void add(DestinationPublisher publisher); + + boolean isSupportedType(DestinationType type); +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relay/ReceivedEventMessage.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relay/ReceivedEventMessage.java new file mode 100644 index 0000000..a6d7b51 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relay/ReceivedEventMessage.java @@ -0,0 +1,10 @@ +package org.masil.seoulyeok.events.relay; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ReceivedEventMessage { + Long destinationId; + String payload; + String payloadVersion; +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/MessagePayload.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/MessagePayload.java new file mode 100644 index 0000000..91a1602 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/MessagePayload.java @@ -0,0 +1,8 @@ +package org.masil.seoulyeok.events.relaymessage; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class MessagePayload { + String value; +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/PayloadVersion.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/PayloadVersion.java new file mode 100644 index 0000000..f75f05a --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/PayloadVersion.java @@ -0,0 +1,27 @@ +package org.masil.seoulyeok.events.relaymessage; + +import lombok.Value; + +@Value +public class PayloadVersion { + + public static PayloadVersion ZERO = new PayloadVersion("0.0.0"); + + private static final String SEMVER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)" + + "(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; + + public static PayloadVersion of(String value) { + if (!value.matches(SEMVER_REGEX)) { + throw new IllegalArgumentException("payload version is not semantic version"); + } + return new PayloadVersion(value); + } + + String value; + + private PayloadVersion(String value) { + this.value = value; + } +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessage.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessage.java new file mode 100644 index 0000000..0c65d75 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessage.java @@ -0,0 +1,52 @@ +package org.masil.seoulyeok.events.relaymessage; + +import com.masil.commons.clocks.Clocks; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.masil.seoulyeok.events.destination.DestinationId; + +import java.time.LocalDateTime; + +@EqualsAndHashCode +@Getter +@AllArgsConstructor +public class RelayMessage { + + private RelayMessageId id; + private final DestinationId destinationId; + private final MessagePayload messagePayload; + + private final LocalDateTime createAt; + private LocalDateTime reliedAt; + private final PayloadVersion payloadVersion; + + public RelayMessage(DestinationId destinationId) { + Long longId = 1L; + this.id = RelayMessageId.of(longId); + this.destinationId = destinationId; + this.messagePayload = MessagePayload.of(""); + this.createAt = Clocks.now(); + this.payloadVersion = PayloadVersion.ZERO; + } + + private RelayMessage(DestinationId destinationId, MessagePayload messagePayload, PayloadVersion payloadVersion) { + this.id = RelayMessageId.of(1L); + this.destinationId = destinationId; + this.messagePayload = messagePayload; + this.createAt = Clocks.now(); + this.payloadVersion = payloadVersion; + } + + public RelayMessage(RelayMessageId id, DestinationId destinationId, MessagePayload messagePayload, PayloadVersion payloadVersion) { + this.id = id; + this.destinationId = destinationId; + this.messagePayload = messagePayload; + this.createAt = Clocks.now(); + this.payloadVersion = payloadVersion; + } + + public static RelayMessage create(MessagePayload messagePayload, DestinationId destinationId, PayloadVersion payloadVersion) { + return new RelayMessage(destinationId, messagePayload, payloadVersion); + } +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageId.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageId.java new file mode 100644 index 0000000..f4aa5f5 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageId.java @@ -0,0 +1,14 @@ +package org.masil.seoulyeok.events.relaymessage; + +import com.likelen.identifier.core.LongId; +import lombok.Value; + +@Value(staticConstructor = "of") +public class RelayMessageId implements LongId { + Long value; + + @Override + public Long get() { + return value; + } +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageModel.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageModel.java new file mode 100644 index 0000000..5521966 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/RelayMessageModel.java @@ -0,0 +1,11 @@ +package org.masil.seoulyeok.events.relaymessage; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class RelayMessageModel { + Long messageId; + String payload; + String createdAt; + String payloadVersion; +} diff --git a/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/TopRelayMessage.java b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/TopRelayMessage.java new file mode 100644 index 0000000..38daa89 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/main/java/org/masil/seoulyeok/events/relaymessage/TopRelayMessage.java @@ -0,0 +1,37 @@ +package org.masil.seoulyeok.events.relaymessage; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.time.LocalDateTime; + +import lombok.ToString; +import org.masil.seoulyeok.events.destination.DestinationId; + +@AllArgsConstructor +@Getter +@ToString +@EqualsAndHashCode +public class TopRelayMessage { + + public static final TopRelayMessage EMPTY = new TopRelayMessage(null, null, null, null, null, null); + + private RelayMessageId id; + private DestinationId destinationId; + private MessagePayload messagePayload; + private LocalDateTime createAt; + private LocalDateTime reliedAt; + private PayloadVersion payloadVersion; + + public static TopRelayMessage createByMessage(RelayMessage message) { + RelayMessageId id = message.getId(); + MessagePayload messagePayload = message.getMessagePayload(); + DestinationId destinationId = message.getDestinationId(); + LocalDateTime reliedAt = message.getReliedAt(); + LocalDateTime createAt = message.getCreateAt(); + PayloadVersion payloadVersion = message.getPayloadVersion(); + + return new TopRelayMessage(id, destinationId, messagePayload, createAt, reliedAt, payloadVersion); + } +} diff --git a/seoulyeok-message-relay/domain/src/test/java/org/masil/seoulyeok/events/relaymessage/PayloadVersionTest.java b/seoulyeok-message-relay/domain/src/test/java/org/masil/seoulyeok/events/relaymessage/PayloadVersionTest.java new file mode 100644 index 0000000..3435663 --- /dev/null +++ b/seoulyeok-message-relay/domain/src/test/java/org/masil/seoulyeok/events/relaymessage/PayloadVersionTest.java @@ -0,0 +1,19 @@ +package org.masil.seoulyeok.events.relaymessage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class PayloadVersionTest { + + @Test + void SemanticVersioning_형식이_아니라면_생성되지_않는다() { + PayloadVersion actual = PayloadVersion.of("0.1.1"); + assertThat(actual).isNotNull(); + + assertThatThrownBy(() -> PayloadVersion.of("123")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PayloadVersion.of("1.1.1.1")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> PayloadVersion.of("1.1.")).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/seoulyeok-message-relay/messages/build.gradle b/seoulyeok-message-relay/messages/build.gradle new file mode 100644 index 0000000..20f8f64 --- /dev/null +++ b/seoulyeok-message-relay/messages/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-library' + id "io.freefair.lombok" + + +} + +dependencies { + implementation 'org.glassfish:javax.el:3.0.0' + implementation 'org.slf4j:slf4j-api:1.7.28' + + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'com.google.guava:guava:30.1.1-jre' + implementation 'org.valid4j:valid4j:0.5.0' + implementation 'org.hibernate:hibernate-validator:6.2.0.Final' + + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation 'org.assertj:assertj-core:3.20.2' +} + +test { + useJUnitPlatform() +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidateException.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidateException.java new file mode 100644 index 0000000..5dc4078 --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidateException.java @@ -0,0 +1,16 @@ +package com.trevari.messages; + +import lombok.Getter; + +@Getter +public class EventValidateException extends IllegalArgumentException { + + private final String eventType; + private final String violationMessages; + + public EventValidateException(String eventType, String violationMessages) { + this.eventType = eventType; + this.violationMessages = violationMessages; + + } +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidator.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidator.java new file mode 100644 index 0000000..f0d5597 --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/EventValidator.java @@ -0,0 +1,20 @@ +package com.trevari.messages; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; +import java.util.stream.Collectors; + +public class EventValidator { + + void validateAndThrow(T eventMessage) { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> eventMessageViolations = validator.validate(eventMessage); + + if (!eventMessageViolations.isEmpty()) { + throw new EventValidateException(eventMessage.getClass().getName(), + eventMessageViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(", "))); + } + } +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelop.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelop.java new file mode 100644 index 0000000..dd6975e --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelop.java @@ -0,0 +1,17 @@ +package com.trevari.messages; + +import com.trevari.identity.generator.LongIdGeneratorHolder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class GeneralMessageEnvelop implements MessageEnvelop { + Long messageId; + T payload; + + public GeneralMessageEnvelop(T payload) { + this.messageId = LongIdGeneratorHolder.get().gen(MessageId.class).get(); + this.payload = payload; + } +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopPayloadDeserializeException.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopPayloadDeserializeException.java new file mode 100644 index 0000000..57a5f16 --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopPayloadDeserializeException.java @@ -0,0 +1,13 @@ +package com.trevari.messages; + +import lombok.Getter; + +@Getter +public class GeneralMessageEnvelopPayloadDeserializeException extends RuntimeException { + private final GeneralMessageEnvelop messageEnvelop; + + public GeneralMessageEnvelopPayloadDeserializeException(GeneralMessageEnvelop messageEnvelop, Exception e) { + super(e); + this.messageEnvelop = messageEnvelop; + } +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessReturn.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessReturn.java new file mode 100644 index 0000000..d61fb9b --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessReturn.java @@ -0,0 +1,6 @@ +package com.trevari.messages; + +public enum GeneralMessageEnvelopProcessReturn { + + IGNORE, SUCCESS, RETRY +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplate.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplate.java new file mode 100644 index 0000000..6280ea0 --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplate.java @@ -0,0 +1,59 @@ +package com.trevari.messages; + +import com.trevari.domain.core.Serializer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Consumer; + +import static com.trevari.messages.GeneralMessageEnvelopProcessReturn.*; + +@Slf4j +@RequiredArgsConstructor +public class GeneralMessageEnvelopProcessTemplate { + + private final EventValidator eventValidator; + + public GeneralMessageEnvelopProcessTemplate() { + this.eventValidator = new EventValidator(); + } + + public GeneralMessageEnvelopProcessReturn doProcess(GeneralMessageEnvelop messageEnvelop, Class aEventClass, Consumer processor) { + try { + if (messageEnvelop == null || messageEnvelop.getPayload() == null) { + + log.error("GeneralMessageEnvelop or GeneralMessageEnvelop.payload is null"); + return IGNORE; + } + + if (aEventClass == null) { + log.error(String.format("aEventClass is null. MessageId is %s, messageEnvelop Payload is %s", messageEnvelop.getMessageId(), messageEnvelop.getPayload())); + return RETRY; + } + + if (processor == null) { + log.error(String.format("processor is null. MessageId is %s, messageEnvelop Payload is %s", messageEnvelop.getMessageId(), messageEnvelop.getPayload())); + return RETRY; + } + + final String payload = messageEnvelop.getPayload(); + + T deserialize = Serializer.getInstance().deserialize(payload, aEventClass); + + eventValidator.validateAndThrow(deserialize); + + processor.accept(deserialize); + return SUCCESS; + } catch (GeneralMessageEnvelopPayloadDeserializeException e) { + log.error(String.format("Failed to deserialize MessageEnvelop. payload in MessageEnvelop is %s, MessageId is %s", + e.getMessageEnvelop().getPayload(), e.getMessageEnvelop().getMessageId()), e); + return IGNORE; + } catch (EventValidateException e) { + log.error(String.format("%s has violationMessages. Messages: '%s'", e.getEventType(), e.getViolationMessages()), e); + return IGNORE; + } catch (Exception e) { + log.error(String.format("Failed to process of GeneralMessageEnvelop. MessageId is %s, Exception Message : %s", messageEnvelop.getMessageId(), e.getMessage())); + return RETRY; + } + } +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageEnvelop.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageEnvelop.java new file mode 100644 index 0000000..5ff0fef --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageEnvelop.java @@ -0,0 +1,11 @@ +package com.trevari.messages; + +import javax.validation.constraints.NotNull; + +public interface MessageEnvelop { + + @NotNull + Long getMessageId(); + @NotNull + T getPayload(); +} diff --git a/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageId.java b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageId.java new file mode 100644 index 0000000..2f3a5ae --- /dev/null +++ b/seoulyeok-message-relay/messages/src/main/java/com/trevari/messages/MessageId.java @@ -0,0 +1,14 @@ +package com.trevari.messages; + +import com.trevari.identity.core.LongId; +import lombok.Value; + +@Value +public class MessageId implements LongId { + Long value; + + @Override + public Long get() { + return value; + } +} diff --git a/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/EventValidatorTest.java b/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/EventValidatorTest.java new file mode 100644 index 0000000..69d65f1 --- /dev/null +++ b/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/EventValidatorTest.java @@ -0,0 +1,59 @@ +package com.trevari.messages; + +import com.trevari.identity.generator.IdGeneratorFactory; +import com.trevari.identity.generator.LongIdGeneratorHolder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EventValidatorTest { + + EventValidator eventValidator; + + @BeforeEach + void setUp() { + IdGeneratorFactory idGeneratorFactory = new IdGeneratorFactory(() -> 123); + LongIdGeneratorHolder.set(idGeneratorFactory.create()); + } + + @Test + void success() { + FooMessages payload = new FooMessages(); + payload.setFoo("ABC"); + + eventValidator = new EventValidator(); + eventValidator.validateAndThrow(payload); + } + + @Test + void blank_fail() { + FooMessages payload = new FooMessages(); + payload.setFoo(""); + + eventValidator = new EventValidator(); + assertThatThrownBy(() -> eventValidator.validateAndThrow(payload)).isInstanceOf(EventValidateException.class); + } + + @Test + void null_fail() { + FooMessages payload = new FooMessages(); + payload.setFoo(null); + + eventValidator = new EventValidator(); + assertThatThrownBy(() -> eventValidator.validateAndThrow(payload)).isInstanceOf(EventValidateException.class); + } + + @Data + @NoArgsConstructor + static class FooMessages { + @NotBlank + @NotNull + private String foo; + } +} diff --git a/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplateTest.java b/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplateTest.java new file mode 100644 index 0000000..2f3bfad --- /dev/null +++ b/seoulyeok-message-relay/messages/src/test/java/com/trevari/messages/GeneralMessageEnvelopProcessTemplateTest.java @@ -0,0 +1,107 @@ +package com.trevari.messages; + +import com.trevari.domain.core.DomainEvent; +import com.trevari.domain.core.DomainEventId; +import com.trevari.domain.core.Serializer; +import com.trevari.identity.generator.IdGeneratorFactory; +import com.trevari.identity.generator.LongIdGeneratorHolder; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +class GeneralMessageEnvelopProcessTemplateTest { + + + GeneralMessageEnvelopProcessTemplate sut = new GeneralMessageEnvelopProcessTemplate(); + + @BeforeEach + void setUp() { + IdGeneratorFactory idGeneratorFactory = new IdGeneratorFactory(() -> 123); + LongIdGeneratorHolder.set(idGeneratorFactory.create()); + } + + @Data + @ToString + static class S { + DomainEventId eventId; + DestinationId destinationId; + } + + @Data + @ToString + static class DestinationId { + long value; + } + + @Test + void IGNORE_when_messageEnvelop_is_null() { + Consumer processor = fooMessages -> System.out.println(fooMessages.getFoo()); + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(null, FooMessages.class, processor); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.IGNORE); + } + + @Test + void IGNORE_when_eventClass_is_null() { + GeneralMessageEnvelop envelop = new GeneralMessageEnvelop<>(DomainEvent.of(new FooMessages("XXX")).serialize()); + Consumer processor = fooMessages -> System.out.println(fooMessages.getFoo()); + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(envelop, null, processor); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.RETRY); + } + + @Test + void RETRY_when_process_is_null_return() { + GeneralMessageEnvelop envelop = new GeneralMessageEnvelop<>(DomainEvent.of(new FooMessages("XXX")).serialize()); + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(envelop, FooMessages.class, null); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.RETRY); + } + + + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class FooMessages { + @NotBlank + @NotNull + private String foo; + } + + @Test + void EventValidateException_when_create_with_empty_string() { + DomainEvent event = DomainEvent.of(new FooMessages("")); + GeneralMessageEnvelop messageEnvelope = new GeneralMessageEnvelop<>(event.serialize()); + Consumer processor = fooMessages -> System.out.println(fooMessages.getFoo()); + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(messageEnvelope, FooMessages.class, processor); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.IGNORE); + } + + @Test + void EventValidateException_when_create_with_null_string() { + DomainEvent event = DomainEvent.of(new FooMessages(null)); + GeneralMessageEnvelop messageEnvelope = new GeneralMessageEnvelop<>(event.serialize()); + Consumer processor = fooMessages -> System.out.println(fooMessages.getFoo()); + + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(messageEnvelope, FooMessages.class, processor); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.IGNORE); + } + + + @Test + void DomainEventPayloadDeserializeException_when_DomainEvent_is_not_valid() { + DomainEvent event = DomainEvent.of("xxxx"); + GeneralMessageEnvelop messageEnvelope = new GeneralMessageEnvelop<>(event.serialize()); + Consumer processor = fooMessages -> System.out.println(fooMessages.getFoo()); + + GeneralMessageEnvelopProcessReturn processReturn = sut.doProcess(messageEnvelope, FooMessages.class, processor); + assertThat(processReturn).isEqualTo(GeneralMessageEnvelopProcessReturn.IGNORE); + } + +} diff --git a/seoulyeok-message-relay/persistence/build.gradle b/seoulyeok-message-relay/persistence/build.gradle new file mode 100644 index 0000000..bcc8b8a --- /dev/null +++ b/seoulyeok-message-relay/persistence/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java-library' + id "io.freefair.lombok" + id "org.springframework.boot" + id "io.spring.dependency-management" +} + +group 'org.masil.seoulyeok.events.relay' +version '0.1.0' + +dependencies { + implementation project(':seoulyeok-message-relay:domain') + implementation project(':refs:destination') + + implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36' + implementation 'com.github.moimp:domain-core:0.0.2' + implementation 'com.github.moimp.clocks:clocks:1.0.0' + implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0' + + // jdbc + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'org.postgresql:postgresql' + implementation 'com.google.code.gson:gson:2.8.8' + + testImplementation 'io.zonky.test:embedded-database-spring-test:2.1.0' + testImplementation 'org.mockito:mockito-inline:4.6.1' + testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // testing + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.assertj:assertj-core:3.23.1' +} + +test { + useJUnitPlatform() +} + +bootJar { + enabled = false +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/config/DataJdbcConfig.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/config/DataJdbcConfig.java new file mode 100644 index 0000000..18aa509 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/config/DataJdbcConfig.java @@ -0,0 +1,47 @@ +package org.masil.seoulyeok.events.relay.config; + +import org.masil.seoulyeok.events.relay.converter.destination.AddressToStringConverter; +import org.masil.seoulyeok.events.relay.converter.destination.DestinationIdToLongConverter; +import org.masil.seoulyeok.events.relay.converter.destination.LongToDestinationIdConverter; +import org.masil.seoulyeok.events.relay.converter.destination.StringToAddressConverter; +import com.trevari.events.relay.converter.partitionqueue.*; + +import java.util.ArrayList; +import java.util.List; + +import org.masil.seoulyeok.events.relay.converter.partitionqueue.LongToRelayMessageIdConverter; +import org.masil.seoulyeok.events.relay.converter.partitionqueue.MessagePayloadReadingConverter; +import org.masil.seoulyeok.events.relay.converter.partitionqueue.MessagePayloadWritingConverter; +import org.masil.seoulyeok.events.relay.converter.partitionqueue.RelayMessageIdToLongConverter; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; +import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing; + +@Configuration +@EnableJdbcAuditing +public class DataJdbcConfig extends AbstractJdbcConfiguration { + + @Override + public JdbcCustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(this.getCustomConverters()); + } + + private List> getCustomConverters() { + List> converters = new ArrayList<>(); + + converters.add(new DestinationIdToLongConverter()); + converters.add(new LongToDestinationIdConverter()); + + converters.add(new AddressToStringConverter()); + converters.add(new StringToAddressConverter()); + + converters.add(new RelayMessageIdToLongConverter()); + converters.add(new LongToRelayMessageIdConverter()); + + converters.add(MessagePayloadReadingConverter.INSTANCE); + converters.add(MessagePayloadWritingConverter.INSTANCE); + return converters; + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/AddressToStringConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/AddressToStringConverter.java new file mode 100644 index 0000000..8b7b1c7 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/AddressToStringConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.destination; + +import com.trevari.events.destination.Address; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; + +@WritingConverter +public class AddressToStringConverter implements Converter { + @Override + public String convert(Address source) { + return source.getValue(); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/DestinationIdToLongConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/DestinationIdToLongConverter.java new file mode 100644 index 0000000..bed0cbb --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/DestinationIdToLongConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.destination; + +import com.trevari.events.destination.DestinationId; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; + +@WritingConverter +public class DestinationIdToLongConverter implements Converter { + @Override + public Long convert(DestinationId source) { + return source.getValue(); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/LongToDestinationIdConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/LongToDestinationIdConverter.java new file mode 100644 index 0000000..273b5c2 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/LongToDestinationIdConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.destination; + +import com.trevari.events.destination.DestinationId; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +@ReadingConverter +public class LongToDestinationIdConverter implements Converter { + @Override + public DestinationId convert(Long source) { + return DestinationId.of(source); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/StringToAddressConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/StringToAddressConverter.java new file mode 100644 index 0000000..15b344f --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/destination/StringToAddressConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.destination; + +import com.trevari.events.destination.Address; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +@ReadingConverter +public class StringToAddressConverter implements Converter { + @Override + public Address convert(String source) { + return Address.of(source); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/LongToRelayMessageIdConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/LongToRelayMessageIdConverter.java new file mode 100644 index 0000000..1667b23 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/LongToRelayMessageIdConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.partitionqueue; + +import com.trevari.events.relaymessage.RelayMessageId; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +@ReadingConverter +public class LongToRelayMessageIdConverter implements Converter { + @Override + public RelayMessageId convert(Long source) { + return RelayMessageId.of(source); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadReadingConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadReadingConverter.java new file mode 100644 index 0000000..4dec896 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadReadingConverter.java @@ -0,0 +1,18 @@ +package org.masil.seoulyeok.events.relay.converter.partitionqueue; + +import org.masil.seoulyeok.events.relay.util.Serializer; +import com.trevari.events.relaymessage.MessagePayload; +import org.postgresql.util.PGobject; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +@ReadingConverter +public enum MessagePayloadReadingConverter implements Converter { + INSTANCE; + + @Override + public MessagePayload convert(PGobject pgObject) { + String source = pgObject.getValue(); + return Serializer.getInstance().deserialize(source, MessagePayload.class); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadWritingConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadWritingConverter.java new file mode 100644 index 0000000..9601012 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/MessagePayloadWritingConverter.java @@ -0,0 +1,28 @@ +package org.masil.seoulyeok.events.relay.converter.partitionqueue; + +import org.masil.seoulyeok.events.relay.util.Serializer; +import com.trevari.events.relaymessage.MessagePayload; +import org.postgresql.util.PGobject; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; + +import java.sql.SQLException; + +@WritingConverter +public enum MessagePayloadWritingConverter implements Converter { + INSTANCE; + + @Override + public PGobject convert(MessagePayload source) { + String json = Serializer.getInstance().serialize(source); + + PGobject jsonObject = new PGobject(); + jsonObject.setType("json"); + try { + jsonObject.setValue(json); + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return jsonObject; + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/RelayMessageIdToLongConverter.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/RelayMessageIdToLongConverter.java new file mode 100644 index 0000000..7ce56df --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/converter/partitionqueue/RelayMessageIdToLongConverter.java @@ -0,0 +1,13 @@ +package org.masil.seoulyeok.events.relay.converter.partitionqueue; + +import com.trevari.events.relaymessage.RelayMessageId; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; + +@WritingConverter +public class RelayMessageIdToLongConverter implements Converter { + @Override + public Long convert(RelayMessageId source) { + return source.getValue(); + } +} diff --git a/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/util/Serializer.java b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/util/Serializer.java new file mode 100644 index 0000000..a5d29ad --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/main/java/org/masil/seoulyeok/events/relay/util/Serializer.java @@ -0,0 +1,56 @@ +package org.masil.seoulyeok.events.relay.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.lang.reflect.Type; + +public class Serializer { + + private static final String EMPTY = ""; + private static Serializer serializer; + private Gson gson; + + private Serializer() { + this.build(); + } + + public static synchronized Serializer getInstance() { + if (Serializer.serializer == null) { + Serializer.serializer = new Serializer(); + } + return Serializer.serializer; + } + + public String serialize(Object object) { + if (object == null) { + return EMPTY; + } + if (EMPTY.equals(object)) { + return EMPTY; + } + + if (object instanceof String) { + return (String) object; + } + + return this.gson().toJson(object); + } + + public T deserialize(String aSerialization, final Class aType) { + return this.gson().fromJson(aSerialization, aType); + } + + public T deserialize(String json, Type typeOfT) { + return this.gson().fromJson(json, typeOfT); + } + + protected Gson gson() { + return this.gson; + } + + private void build() { + this.gson = new GsonBuilder() + .serializeNulls() + .create(); + } +} diff --git a/seoulyeok-message-relay/persistence/src/test/java/org/masil/seoulyeok/events/relay/util/SerializerTest.java b/seoulyeok-message-relay/persistence/src/test/java/org/masil/seoulyeok/events/relay/util/SerializerTest.java new file mode 100644 index 0000000..efb2813 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/test/java/org/masil/seoulyeok/events/relay/util/SerializerTest.java @@ -0,0 +1,33 @@ +package org.masil.seoulyeok.events.relay.util; + +import org.junit.jupiter.api.Test; + +class SerializerTest { + + @Test + void AssignedEventMessage_에_대한_Serialize_Deserialize() { +// RelayMessage message = RelayMessage.of(Seq.of(1L), DomainEventId.of(2L), DestinationId.of(3L)); +// +// String serialized = Serializer.getInstance().serialize(message); +// assertThat(serialized).isEqualTo("{\"seq\":1,\"eventId\":2,\"destinationId\":3}"); +// +// RelayMessage deserialized = Serializer.getInstance() +// .deserialize(serialized, RelayMessage.class); +// assertThat(message).isEqualTo(deserialized); + } + + @Test + void PublishedEventMessage_에_대한_serialize_deserialize() { +// PublishedEventMessage message = PublishedEventMessage.of(PublishedEventMessageId.of(1L), Seq.of(1L), DomainEventId.of(2L), +// DestinationId.of(3L), +// LocalDateTime.of(2022, 2, 2, 10, 10, 10), true); +// +// String serialized = Serializer.getInstance().serialize(message); +// assertThat(serialized).isEqualTo("{\"id\":1,\"seq\":1,\"eventId\":2,\"destinationId\":3,\"publishedAt\":\"2022-02-02T10:10:10\",\"success\":\"true\"}"); +// +// PublishedEventMessage deserialized = Serializer.getInstance() +// .deserialize(serialized, PublishedEventMessage.class); +// +// assertThat(message).isEqualTo(deserialized); + } +} diff --git a/seoulyeok-message-relay/persistence/src/test/resources/test-init.sql b/seoulyeok-message-relay/persistence/src/test/resources/test-init.sql new file mode 100644 index 0000000..20c2990 --- /dev/null +++ b/seoulyeok-message-relay/persistence/src/test/resources/test-init.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS destination +( + id BIGINT PRIMARY KEY, + address VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + status VARCHAR(50), + version BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS relay_message +( + id BIGINT PRIMARY KEY, + destination_id BIGINT NOT NULL, + message_payload json, + created_at TIMESTAMPTZ, + relied_at TIMESTAMPTZ, + payload_version VARCHAR(50), + version INT NOT NULL +); diff --git a/settings.gradle b/settings.gradle index be5d6f4..34f0f47 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,7 @@ rootProject.name = 'seoulyeok' +include("codegen") + include("seoulyeok-eventstore") include('seoulyeok-eventstore:domain', 'seoulyeok-eventstore:application', @@ -13,11 +15,17 @@ include('seoulyeok-message-pulling:domain', ) include("seoulyeok-message-relay") -include('seoulyeok-message-relay:domain', - 'seoulyeok-message-relay:application', - 'seoulyeok-message-relay:adapter' -) +include("seoulyeok-message-relay:api") +include("seoulyeok-message-relay:messages") +include("seoulyeok-message-relay:persistence") +include("seoulyeok-message-relay:domain") +include("seoulyeok-message-relay:application") + +include("seoulyeok-message-assign") include("seoulyeok-message-dispatcher") +include('refs:destination') +include('refs:event') + //TODO message-relay, assign