Browse Source

Add ability to use custom basePath (#511)

* add ability to use relative pathes

* Add basename prop (#513)

* add static controller

* add docker-compose

* Refactoring

* Refactoring

* fixed comparison bugs

* dirty

* clenaup

* Update React app

Co-authored-by: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com>
Co-authored-by: German Osin <german.osin@gmail.com>
Co-authored-by: Oleg Shuralev <workshur@gmail.com>
Rustam Gimadiev 4 năm trước cách đây
mục cha
commit
fcc703ddd6

+ 1 - 5
README.md

@@ -153,6 +153,7 @@ For example, if you want to use an environment variable to set the `name` parame
 
 |Name               	|Description
 |-----------------------|-------------------------------
+|`SERVER_SERVLET_CONTEXT_PATH` | URI basePath
 |`KAFKA_CLUSTERS_0_NAME` | Cluster name
 |`KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS` 	|Address where to connect 
 |`KAFKA_CLUSTERS_0_ZOOKEEPER` 	| Zookeper service address 
@@ -163,8 +164,3 @@ For example, if you want to use an environment variable to set the `name` parame
 |`KAFKA_CLUSTERS_0_READONLY`        	|Enable read only mode. Default: false
 |`LOGGING_LEVEL_ROOT`        	| Setting log level (all, debug, info, warn, error, fatal, off). Default: debug
 |`LOGGING_LEVEL_COM_PROVECTUS`        	|Setting log level (all, debug, info, warn, error, fatal, off). Default: debug
-
- 
-
- 
-

+ 19 - 0
docker/kafka-ui-reverse-proxy.yaml

@@ -0,0 +1,19 @@
+---
+version: '2'
+services:
+  nginx:
+    image: nginx:latest
+    volumes:
+      - ./proxy.conf:/etc/nginx/conf.d/default.conf
+    ports:
+      - 8080:80
+
+  kafka-ui:
+    container_name: kafka-ui
+    image: provectuslabs/kafka-ui:latest
+    ports:
+      - 8082:8080
+    environment:
+      KAFKA_CLUSTERS_0_NAME: local
+      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
+      SERVER_SERVLET_CONTEXT_PATH: /kafka-ui

+ 9 - 0
docker/proxy.conf

@@ -0,0 +1,9 @@
+server {
+    listen       80;
+    server_name  localhost;
+
+    location /kafka-ui {
+#        rewrite /kafka-ui/(.*) /$1  break;
+        proxy_pass   http://kafka-ui:8080;
+    }
+}

+ 2 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java

@@ -22,7 +22,7 @@ public class Config {
   }
 
   private GenericKeyedObjectPoolConfig poolConfig() {
-    GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig();
+    final var poolConfig = new GenericKeyedObjectPoolConfig();
     poolConfig.setMaxIdlePerKey(3);
     poolConfig.setMaxTotalPerKey(3);
     return poolConfig;
@@ -30,7 +30,7 @@ public class Config {
 
   @Bean
   public MBeanExporter exporter() {
-    final MBeanExporter exporter = new MBeanExporter();
+    final var exporter = new MBeanExporter();
     exporter.setAutodetect(true);
     exporter.setExcludedBeans("pool");
     return exporter;

+ 22 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java

@@ -1,5 +1,6 @@
 package com.provectus.kafka.ui.config;
 
+import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
@@ -7,15 +8,32 @@ import org.springframework.web.server.WebFilterChain;
 import reactor.core.publisher.Mono;
 
 @Component
+
 public class CustomWebFilter implements WebFilter {
+
+  private final ServerProperties serverProperties;
+
+  public CustomWebFilter(ServerProperties serverProperties) {
+    this.serverProperties = serverProperties;
+  }
+
   @Override
   public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
-    if (exchange.getRequest().getURI().getPath().equals("/")
-        || exchange.getRequest().getURI().getPath().startsWith("/ui")) {
+    String contextPath = serverProperties.getServlet().getContextPath() != null 
+        ? serverProperties.getServlet().getContextPath() : "";
+
+    if (exchange.getRequest().getURI().getPath().equals(contextPath + "/")
+        || exchange.getRequest().getURI().getPath().startsWith(contextPath + "/ui")) {
       return chain.filter(
           exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build())
-              .build());
-    }
+              .build()
+      );
+    } else if (exchange.getRequest().getURI().getPath().startsWith(contextPath)) {
+      return chain.filter(
+          exchange.mutate().request(exchange.getRequest().mutate().contextPath(contextPath).build())
+              .build()
+      );
+    }    
 
     return chain.filter(exchange);
   }

+ 55 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java

@@ -0,0 +1,55 @@
+package com.provectus.kafka.ui.controller;
+
+import com.provectus.kafka.ui.util.ResourceUtil;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequiredArgsConstructor
+@Log4j2
+public class StaticController {
+  private final ServerProperties serverProperties;
+
+  @Value("classpath:static/index.html")
+  private Resource indexFile;
+  private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();
+
+  @GetMapping(value = "/index.html", produces = { "text/html" })
+  public Mono<ResponseEntity<String>> getIndex() {
+    return Mono.just(ResponseEntity.ok(getRenderedIndexFile()));
+  }
+
+  public String getRenderedIndexFile() {
+    String rendered = renderedIndexFile.get();
+    if (rendered == null) {
+      rendered = buildIndexFile();
+      if (renderedIndexFile.compareAndSet(null, rendered)) {
+        return rendered;
+      } else {
+        return renderedIndexFile.get();
+      }
+    } else {
+      return rendered;
+    }
+  }
+
+  @SneakyThrows
+  private String buildIndexFile() {
+    final String contextPath = serverProperties.getServlet().getContextPath() != null
+        ? serverProperties.getServlet().getContextPath() : "";
+    final String staticPath = contextPath + "/static";
+    return ResourceUtil.readAsString(indexFile)
+        .replace("href=\"./static", "href=\"" + staticPath)
+        .replace("src=\"./static", "src=\"" + staticPath)
+        .replace("window.basePath=\"/\"", "window.basePath=\"" + contextPath + "\"");
+  }
+}

+ 4 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/deserialization/ProtobufFileRecordDeserializer.java

@@ -26,16 +26,16 @@ public class ProtobufFileRecordDeserializer implements RecordDeserializer {
   }
 
   @Override
-  public Object deserialize(ConsumerRecord<Bytes, Bytes> record) {
+  public Object deserialize(ConsumerRecord<Bytes, Bytes> msg) {
     try {
-      final DynamicMessage message = DynamicMessage.parseFrom(
+      final var message = DynamicMessage.parseFrom(
           protobufSchema.toDescriptor(),
-          new ByteArrayInputStream(record.value().get())
+          new ByteArrayInputStream(msg.value().get())
       );
       byte[] bytes = ProtobufSchemaUtils.toJson(message);
       return parseJson(bytes);
     } catch (Throwable e) {
-      throw new RuntimeException("Failed to parse record from topic " + record.topic(), e);
+      throw new RuntimeException("Failed to parse record from topic " + msg.topic(), e);
     }
   }
 

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/deserialization/RecordDeserializer.java

@@ -5,5 +5,5 @@ import org.apache.kafka.common.utils.Bytes;
 
 public interface RecordDeserializer {
 
-  Object deserialize(ConsumerRecord<Bytes, Bytes> record);
+  Object deserialize(ConsumerRecord<Bytes, Bytes> msg);
 }

+ 64 - 34
kafka-ui-api/src/main/java/com/provectus/kafka/ui/deserialization/SchemaRegistryRecordDeserializer.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.protobuf.Message;
 import com.provectus.kafka.ui.model.KafkaCluster;
+import io.confluent.kafka.schemaregistry.ParsedSchema;
 import io.confluent.kafka.schemaregistry.SchemaProvider;
 import io.confluent.kafka.schemaregistry.avro.AvroSchemaProvider;
 import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;
@@ -16,14 +17,17 @@ import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaUtils;
 import io.confluent.kafka.serializers.KafkaAvroDeserializer;
 import io.confluent.kafka.serializers.protobuf.KafkaProtobufDeserializer;
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
+import lombok.SneakyThrows;
 import lombok.extern.log4j.Log4j2;
 import org.apache.avro.generic.GenericRecord;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.errors.SerializationException;
 import org.apache.kafka.common.serialization.StringDeserializer;
 import org.apache.kafka.common.utils.Bytes;
 
@@ -99,57 +103,83 @@ public class SchemaRegistryRecordDeserializer implements RecordDeserializer {
     return topicFormatMap.computeIfAbsent(record.topic(), k -> detectFormat(record));
   }
 
-  private MessageFormat detectFormat(ConsumerRecord<Bytes, Bytes> record) {
-    String schemaName = String.format(cluster.getSchemaNameTemplate(), record.topic());
+  private MessageFormat detectFormat(ConsumerRecord<Bytes, Bytes> msg) {
     if (schemaRegistryClient != null) {
       try {
-        final List<Integer> versions = schemaRegistryClient.getAllVersions(schemaName);
-        if (!versions.isEmpty()) {
-          final Integer version = versions.iterator().next();
-          final String subjectName = String.format(cluster.getSchemaNameTemplate(), record.topic());
-          final Schema schema = schemaRegistryClient.getByVersion(subjectName, version, false);
-          if (schema.getSchemaType().equals(MessageFormat.PROTOBUF.name())) {
+        final Optional<String> type = getSchemaFromMessage(msg).or(() -> getSchemaBySubject(msg));
+        if (type.isPresent()) {
+          if (type.get().equals(MessageFormat.PROTOBUF.name())) {
             try {
-              protobufDeserializer.deserialize(record.topic(), record.value().get());
+              protobufDeserializer.deserialize(msg.topic(), msg.value().get());
               return MessageFormat.PROTOBUF;
             } catch (Throwable e) {
-              log.info("Failed to get Protobuf schema for topic {}", record.topic(), e);
+              log.info("Failed to get Protobuf schema for topic {}", msg.topic(), e);
             }
-          } else if (schema.getSchemaType().equals(MessageFormat.AVRO.name())) {
+          } else if (type.get().equals(MessageFormat.AVRO.name())) {
             try {
-              avroDeserializer.deserialize(record.topic(), record.value().get());
+              avroDeserializer.deserialize(msg.topic(), msg.value().get());
               return MessageFormat.AVRO;
             } catch (Throwable e) {
-              log.info("Failed to get Avro schema for topic {}", record.topic(), e);
+              log.info("Failed to get Avro schema for topic {}", msg.topic(), e);
             }
-          } else if (schema.getSchemaType().equals(MessageFormat.JSON.name())) {
+          } else if (type.get().equals(MessageFormat.JSON.name())) {
             try {
-              parseJsonRecord(record);
+              parseJsonRecord(msg);
               return MessageFormat.JSON;
             } catch (IOException e) {
-              log.info("Failed to parse json from topic {}", record.topic());
+              log.info("Failed to parse json from topic {}", msg.topic());
             }
           }
         }
-      } catch (RestClientException | IOException e) {
-        log.warn("Failed to get Schema for topic {}", record.topic(), e);
+      } catch (Exception e) {
+        log.warn("Failed to get Schema for topic {}", msg.topic(), e);
       }
     }
 
     try {
-      parseJsonRecord(record);
+      parseJsonRecord(msg);
       return MessageFormat.JSON;
     } catch (IOException e) {
-      log.info("Failed to parse json from topic {}", record.topic());
+      log.info("Failed to parse json from topic {}", msg.topic());
     }
 
     return MessageFormat.STRING;
   }
 
-  private Object parseAvroRecord(ConsumerRecord<Bytes, Bytes> record) throws IOException {
-    String topic = record.topic();
-    if (record.value() != null && avroDeserializer != null) {
-      byte[] valueBytes = record.value().get();
+  @SneakyThrows
+  private Optional<String> getSchemaFromMessage(ConsumerRecord<Bytes, Bytes> msg) {
+    Optional<String> result = Optional.empty();
+    final Bytes value = msg.value();
+    if (value != null) {
+      ByteBuffer buffer = ByteBuffer.wrap(value.get());
+      if (buffer.get() == 0) {
+        int id = buffer.getInt();
+        result = Optional.ofNullable(
+            schemaRegistryClient.getSchemaById(id)
+        ).map(ParsedSchema::schemaType);
+      }
+    }
+    return result;
+  }
+
+  @SneakyThrows
+  private Optional<String> getSchemaBySubject(ConsumerRecord<Bytes, Bytes> msg) {
+    String schemaName = String.format(cluster.getSchemaNameTemplate(), msg.topic());
+    final List<Integer> versions = schemaRegistryClient.getAllVersions(schemaName);
+    if (!versions.isEmpty()) {
+      final Integer version = versions.iterator().next();
+      final String subjectName = String.format(cluster.getSchemaNameTemplate(), msg.topic());
+      final Schema schema = schemaRegistryClient.getByVersion(subjectName, version, false);
+      return Optional.ofNullable(schema).map(Schema::getSchemaType);
+    } else {
+      return Optional.empty();
+    }
+  }
+
+  private Object parseAvroRecord(ConsumerRecord<Bytes, Bytes> msg) throws IOException {
+    String topic = msg.topic();
+    if (msg.value() != null && avroDeserializer != null) {
+      byte[] valueBytes = msg.value().get();
       GenericRecord avroRecord = (GenericRecord) avroDeserializer.deserialize(topic, valueBytes);
       byte[] bytes = AvroSchemaUtils.toJson(avroRecord);
       return parseJson(bytes);
@@ -158,10 +188,10 @@ public class SchemaRegistryRecordDeserializer implements RecordDeserializer {
     }
   }
 
-  private Object parseProtobufRecord(ConsumerRecord<Bytes, Bytes> record) throws IOException {
-    String topic = record.topic();
-    if (record.value() != null && protobufDeserializer != null) {
-      byte[] valueBytes = record.value().get();
+  private Object parseProtobufRecord(ConsumerRecord<Bytes, Bytes> msg) throws IOException {
+    String topic = msg.topic();
+    if (msg.value() != null && protobufDeserializer != null) {
+      byte[] valueBytes = msg.value().get();
       final Message message = protobufDeserializer.deserialize(topic, valueBytes);
       byte[] bytes = ProtobufSchemaUtils.toJson(message);
       return parseJson(bytes);
@@ -170,8 +200,8 @@ public class SchemaRegistryRecordDeserializer implements RecordDeserializer {
     }
   }
 
-  private Object parseJsonRecord(ConsumerRecord<Bytes, Bytes> record) throws IOException {
-    var value = record.value();
+  private Object parseJsonRecord(ConsumerRecord<Bytes, Bytes> msg) throws IOException {
+    var value = msg.value();
     if (value == null) {
       return Map.of();
     }
@@ -184,12 +214,12 @@ public class SchemaRegistryRecordDeserializer implements RecordDeserializer {
     });
   }
 
-  private Object parseStringRecord(ConsumerRecord<Bytes, Bytes> record) {
-    String topic = record.topic();
-    if (record.value() == null) {
+  private Object parseStringRecord(ConsumerRecord<Bytes, Bytes> msg) {
+    String topic = msg.topic();
+    if (msg.value() == null) {
       return Map.of();
     }
-    byte[] valueBytes = record.value().get();
+    byte[] valueBytes = msg.value().get();
     return stringDeserializer.deserialize(topic, valueBytes);
   }
 

+ 3 - 3
kafka-ui-api/src/main/java/com/provectus/kafka/ui/deserialization/SimpleRecordDeserializer.java

@@ -9,9 +9,9 @@ public class SimpleRecordDeserializer implements RecordDeserializer {
   private final StringDeserializer stringDeserializer = new StringDeserializer();
 
   @Override
-  public Object deserialize(ConsumerRecord<Bytes, Bytes> record) {
-    if (record.value() != null) {
-      return stringDeserializer.deserialize(record.topic(), record.value().get());
+  public Object deserialize(ConsumerRecord<Bytes, Bytes> msg) {
+    if (msg.value() != null) {
+      return stringDeserializer.deserialize(msg.topic(), msg.value().get());
     } else {
       return "empty";
     }

+ 4 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/NumberUtil.java

@@ -3,6 +3,10 @@ package com.provectus.kafka.ui.util;
 import org.apache.commons.lang3.math.NumberUtils;
 
 public class NumberUtil {
+
+  private NumberUtil() {
+  }
+
   public static boolean isNumeric(Object value) {
     return value != null && NumberUtils.isCreatable(value.toString());
   }

+ 20 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/ResourceUtil.java

@@ -0,0 +1,20 @@
+package com.provectus.kafka.ui.util;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import org.springframework.core.io.Resource;
+import org.springframework.util.FileCopyUtils;
+
+public class ResourceUtil {
+
+  private ResourceUtil() {
+  }
+
+  public static String readAsString(Resource resource) throws IOException {
+    try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
+      return FileCopyUtils.copyToString(reader);
+    }
+  }
+}

+ 1 - 2
kafka-ui-react-app/.env

@@ -1,2 +1 @@
-# Kafka REST API
-REACT_APP_API_URL=
+# 

+ 177 - 249
kafka-ui-react-app/package-lock.json

@@ -88,7 +88,8 @@
         "typescript": "^4.2.3"
       },
       "engines": {
-        "node": ">=14.15.4"
+        "node": ">=14.15.4",
+        "npm": ">=7.15.1"
       }
     },
     "node_modules/@babel/code-frame": {
@@ -1893,7 +1894,6 @@
         "jest-resolve": "^26.6.2",
         "jest-util": "^26.6.2",
         "jest-worker": "^26.6.2",
-        "node-notifier": "^8.0.0",
         "slash": "^3.0.0",
         "source-map": "^0.6.0",
         "string-length": "^4.0.1",
@@ -2977,47 +2977,6 @@
         "debug": "^4.1.1"
       }
     },
-    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
-      "version": "4.24.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.24.0.tgz",
-      "integrity": "sha512-9+WYJGDnuC9VtYLqBhcSuM7du75fyCS/ypC8c5g7Sdw7pGL4NDTbeH38eJPfzIydCHZDoOgjloxSAA3+4l/zsA==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "4.24.0",
-        "@typescript-eslint/visitor-keys": "4.24.0"
-      }
-    },
-    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
-      "version": "4.24.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.24.0.tgz",
-      "integrity": "sha512-tkZUBgDQKdvfs8L47LaqxojKDE+mIUmOzdz7r+u+U54l3GDkTpEbQ1Jp3cNqqAU9vMUCBA1fitsIhm7yN0vx9Q==",
-      "dev": true
-    },
-    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "4.24.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.24.0.tgz",
-      "integrity": "sha512-kBDitL/by/HK7g8CYLT7aKpAwlR8doshfWz8d71j97n5kUa5caHWvY0RvEUEanL/EqBJoANev8Xc/mQ6LLwXGA==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "4.24.0",
-        "@typescript-eslint/visitor-keys": "4.24.0",
-        "debug": "^4.1.1",
-        "globby": "^11.0.1",
-        "is-glob": "^4.0.1",
-        "semver": "^7.3.2",
-        "tsutils": "^3.17.1"
-      }
-    },
-    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "4.24.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.24.0.tgz",
-      "integrity": "sha512-4ox1sjmGHIxjEDBnMCtWFFhErXtKA1Ec0sBpuz0fqf3P+g3JFGyTxxbF06byw0FRsPnnbq44cKivH7Ks1/0s6g==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "4.24.0",
-        "eslint-visitor-keys": "^2.0.0"
-      }
-    },
     "node_modules/@typescript-eslint/parser/node_modules/debug": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -3033,15 +2992,6 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "dev": true
     },
-    "node_modules/@typescript-eslint/parser/node_modules/semver": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
-      "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      }
-    },
     "node_modules/@typescript-eslint/scope-manager": {
       "version": "4.24.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.24.0.tgz",
@@ -4716,16 +4666,26 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.16.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
-      "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
+      "version": "4.16.6",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+      "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
       "dev": true,
       "dependencies": {
-        "caniuse-lite": "^1.0.30001181",
-        "colorette": "^1.2.1",
-        "electron-to-chromium": "^1.3.649",
+        "caniuse-lite": "^1.0.30001219",
+        "colorette": "^1.2.2",
+        "electron-to-chromium": "^1.3.723",
         "escalade": "^3.1.1",
-        "node-releases": "^1.1.70"
+        "node-releases": "^1.1.71"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/browserslist"
       }
     },
     "node_modules/bs-logger": {
@@ -4947,10 +4907,14 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001207",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz",
-      "integrity": "sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw==",
-      "dev": true
+      "version": "1.0.30001233",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001233.tgz",
+      "integrity": "sha512-BmkbxLfStqiPA7IEzQpIk0UFZFf3A4E6fzjPJ6OR+bFC2L8ES9J8zGA/asoi47p8XDVkev+WJo2I2Nc8c/34Yg==",
+      "dev": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/browserslist"
+      }
     },
     "node_modules/capture-exit": {
       "version": "2.0.0",
@@ -5920,21 +5884,24 @@
       "dev": true
     },
     "node_modules/cssnano": {
-      "version": "4.1.10",
-      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
-      "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
+      "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==",
       "dev": true,
       "dependencies": {
         "cosmiconfig": "^5.0.0",
-        "cssnano-preset-default": "^4.0.7",
+        "cssnano-preset-default": "^4.0.8",
         "is-resolvable": "^1.0.0",
         "postcss": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
       }
     },
     "node_modules/cssnano-preset-default": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
-      "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
+      "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
       "dev": true,
       "dependencies": {
         "css-declaration-sorter": "^4.0.1",
@@ -5965,8 +5932,11 @@
         "postcss-ordered-values": "^4.1.2",
         "postcss-reduce-initial": "^4.0.3",
         "postcss-reduce-transforms": "^4.0.2",
-        "postcss-svgo": "^4.0.2",
+        "postcss-svgo": "^4.0.3",
         "postcss-unique-selectors": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
       }
     },
     "node_modules/cssnano-util-get-arguments": {
@@ -6457,9 +6427,9 @@
       "dev": true
     },
     "node_modules/dns-packet": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
-      "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
+      "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
       "dev": true,
       "dependencies": {
         "ip": "^1.1.0",
@@ -6617,9 +6587,6 @@
       "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
       "integrity": "sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M=",
       "dev": true,
-      "dependencies": {
-        "wcwidth": ">=1.0.1"
-      },
       "optionalDependencies": {
         "wcwidth": ">=1.0.1"
       }
@@ -6647,9 +6614,9 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.3.707",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.707.tgz",
-      "integrity": "sha512-BqddgxNPrcWnbDdJw7SzXVzPmp+oiyjVrc7tkQVaznPGSS9SKZatw6qxoP857M+HbOyyqJQwYQtsuFIMSTNSZA==",
+      "version": "1.3.745",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.745.tgz",
+      "integrity": "sha512-ZZCx4CS3kYT3BREYiIXocDqlNPT56KfdTS1Ogo4yvxRriBqiEXCDTLIQZT/zNVtby91xTWMMxW2NBiXh8bpLHw==",
       "dev": true
     },
     "node_modules/elliptic": {
@@ -9025,9 +8992,9 @@
       "dev": true
     },
     "node_modules/hosted-git-info": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
       "dev": true
     },
     "node_modules/hpack.js": {
@@ -9054,12 +9021,6 @@
       "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
       "dev": true
     },
-    "node_modules/html-comment-regex": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
-      "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
-      "dev": true
-    },
     "node_modules/html-element-map": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.0.tgz",
@@ -9936,15 +9897,6 @@
       "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
       "dev": true
     },
-    "node_modules/is-svg": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
-      "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==",
-      "dev": true,
-      "dependencies": {
-        "html-comment-regex": "^1.1.0"
-      }
-    },
     "node_modules/is-symbol": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
@@ -10434,7 +10386,6 @@
         "@types/node": "*",
         "anymatch": "^3.0.3",
         "fb-watchman": "^2.0.0",
-        "fsevents": "^2.1.2",
         "graceful-fs": "^4.2.4",
         "jest-regex-util": "^26.0.0",
         "jest-serializer": "^26.6.2",
@@ -12691,10 +12642,16 @@
       "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
     },
     "node_modules/nanoid": {
-      "version": "3.1.22",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
-      "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==",
-      "dev": true
+      "version": "3.1.23",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
+      "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
+      "dev": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
     },
     "node_modules/nanomatch": {
       "version": "1.2.13",
@@ -14037,12 +13994,11 @@
       }
     },
     "node_modules/postcss-initial": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.2.tgz",
-      "integrity": "sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz",
+      "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==",
       "dev": true,
       "dependencies": {
-        "lodash.template": "^4.5.0",
         "postcss": "^7.0.2"
       }
     },
@@ -14649,14 +14605,21 @@
       }
     },
     "node_modules/postcss-safe-parser/node_modules/postcss": {
-      "version": "8.2.9",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.9.tgz",
-      "integrity": "sha512-b+TmuIL4jGtCHtoLi+G/PisuIl9avxs8IZMSmlABRwNz5RLUUACrC+ws81dcomz1nRezm5YPdXiMEzBEKgYn+Q==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
+      "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==",
       "dev": true,
       "dependencies": {
         "colorette": "^1.2.2",
-        "nanoid": "^3.1.22",
-        "source-map": "^0.6.1"
+        "nanoid": "^3.1.23",
+        "source-map-js": "^0.6.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
       }
     },
     "node_modules/postcss-selector-matches": {
@@ -14692,15 +14655,17 @@
       }
     },
     "node_modules/postcss-svgo": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz",
-      "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==",
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
+      "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
       "dev": true,
       "dependencies": {
-        "is-svg": "^3.0.0",
         "postcss": "^7.0.0",
         "postcss-value-parser": "^3.0.0",
         "svgo": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
       }
     },
     "node_modules/postcss-svgo/node_modules/postcss-value-parser": {
@@ -15481,7 +15446,6 @@
         "eslint-webpack-plugin": "^2.5.2",
         "file-loader": "6.1.1",
         "fs-extra": "^9.0.1",
-        "fsevents": "^2.1.3",
         "html-webpack-plugin": "4.5.0",
         "identity-obj-proxy": "3.0.0",
         "jest": "26.6.0",
@@ -16186,9 +16150,9 @@
       "dev": true
     },
     "node_modules/resolve-url-loader": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz",
-      "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.3.tgz",
+      "integrity": "sha512-WbDSNFiKPPLem1ln+EVTE+bFUBdTTytfQZWbmghroaFNFaAVmGq0Saqw6F/306CwgPXsGwXVxbODE+3xAo/YbA==",
       "dev": true,
       "dependencies": {
         "adjust-sourcemap-loader": "3.0.0",
@@ -16201,6 +16165,9 @@
         "rework": "1.0.1",
         "rework-visit": "1.0.0",
         "source-map": "0.6.1"
+      },
+      "engines": {
+        "node": ">=6.0.0"
       }
     },
     "node_modules/resolve-url-loader/node_modules/ansi-styles": {
@@ -17185,6 +17152,15 @@
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true
     },
+    "node_modules/source-map-js": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
+      "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/source-map-resolve": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
@@ -18874,10 +18850,8 @@
       "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==",
       "dev": true,
       "dependencies": {
-        "chokidar": "^3.4.1",
         "graceful-fs": "^4.1.2",
-        "neo-async": "^2.5.0",
-        "watchpack-chokidar2": "^2.0.1"
+        "neo-async": "^2.5.0"
       },
       "optionalDependencies": {
         "chokidar": "^3.4.1",
@@ -19261,7 +19235,6 @@
         "anymatch": "^2.0.0",
         "async-each": "^1.0.1",
         "braces": "^2.3.2",
-        "fsevents": "^1.2.7",
         "glob-parent": "^3.1.0",
         "inherits": "^2.0.3",
         "is-binary-path": "^1.0.0",
@@ -19523,9 +19496,9 @@
       }
     },
     "node_modules/webpack-dev-server/node_modules/ws": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
-      "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
+      "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
       "dev": true,
       "dependencies": {
         "async-limiter": "~1.0.0"
@@ -20218,10 +20191,25 @@
       }
     },
     "node_modules/ws": {
-      "version": "7.4.4",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
-      "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
-      "dev": true
+      "version": "7.4.6",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+      "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
     },
     "node_modules/xml": {
       "version": "1.0.1",
@@ -23254,47 +23242,6 @@
         "debug": "^4.1.1"
       },
       "dependencies": {
-        "@typescript-eslint/scope-manager": {
-          "version": "4.24.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.24.0.tgz",
-          "integrity": "sha512-9+WYJGDnuC9VtYLqBhcSuM7du75fyCS/ypC8c5g7Sdw7pGL4NDTbeH38eJPfzIydCHZDoOgjloxSAA3+4l/zsA==",
-          "dev": true,
-          "requires": {
-            "@typescript-eslint/types": "4.24.0",
-            "@typescript-eslint/visitor-keys": "4.24.0"
-          }
-        },
-        "@typescript-eslint/types": {
-          "version": "4.24.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.24.0.tgz",
-          "integrity": "sha512-tkZUBgDQKdvfs8L47LaqxojKDE+mIUmOzdz7r+u+U54l3GDkTpEbQ1Jp3cNqqAU9vMUCBA1fitsIhm7yN0vx9Q==",
-          "dev": true
-        },
-        "@typescript-eslint/typescript-estree": {
-          "version": "4.24.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.24.0.tgz",
-          "integrity": "sha512-kBDitL/by/HK7g8CYLT7aKpAwlR8doshfWz8d71j97n5kUa5caHWvY0RvEUEanL/EqBJoANev8Xc/mQ6LLwXGA==",
-          "dev": true,
-          "requires": {
-            "@typescript-eslint/types": "4.24.0",
-            "@typescript-eslint/visitor-keys": "4.24.0",
-            "debug": "^4.1.1",
-            "globby": "^11.0.1",
-            "is-glob": "^4.0.1",
-            "semver": "^7.3.2",
-            "tsutils": "^3.17.1"
-          }
-        },
-        "@typescript-eslint/visitor-keys": {
-          "version": "4.24.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.24.0.tgz",
-          "integrity": "sha512-4ox1sjmGHIxjEDBnMCtWFFhErXtKA1Ec0sBpuz0fqf3P+g3JFGyTxxbF06byw0FRsPnnbq44cKivH7Ks1/0s6g==",
-          "dev": true,
-          "requires": {
-            "@typescript-eslint/types": "4.24.0",
-            "eslint-visitor-keys": "^2.0.0"
-          }
-        },
         "debug": {
           "version": "4.3.1",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -23309,15 +23256,6 @@
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
           "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
           "dev": true
-        },
-        "semver": {
-          "version": "7.3.5",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
-          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
-          "dev": true,
-          "requires": {
-            "lru-cache": "^6.0.0"
-          }
         }
       }
     },
@@ -25031,16 +24969,16 @@
       }
     },
     "browserslist": {
-      "version": "4.16.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
-      "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
+      "version": "4.16.6",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+      "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "^1.0.30001181",
-        "colorette": "^1.2.1",
-        "electron-to-chromium": "^1.3.649",
+        "caniuse-lite": "^1.0.30001219",
+        "colorette": "^1.2.2",
+        "electron-to-chromium": "^1.3.723",
         "escalade": "^3.1.1",
-        "node-releases": "^1.1.70"
+        "node-releases": "^1.1.71"
       }
     },
     "bs-logger": {
@@ -25272,9 +25210,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001207",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz",
-      "integrity": "sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw==",
+      "version": "1.0.30001233",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001233.tgz",
+      "integrity": "sha512-BmkbxLfStqiPA7IEzQpIk0UFZFf3A4E6fzjPJ6OR+bFC2L8ES9J8zGA/asoi47p8XDVkev+WJo2I2Nc8c/34Yg==",
       "dev": true
     },
     "capture-exit": {
@@ -26269,13 +26207,13 @@
       "dev": true
     },
     "cssnano": {
-      "version": "4.1.10",
-      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
-      "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
+      "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==",
       "dev": true,
       "requires": {
         "cosmiconfig": "^5.0.0",
-        "cssnano-preset-default": "^4.0.7",
+        "cssnano-preset-default": "^4.0.8",
         "is-resolvable": "^1.0.0",
         "postcss": "^7.0.0"
       },
@@ -26311,9 +26249,9 @@
       }
     },
     "cssnano-preset-default": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
-      "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
+      "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
       "dev": true,
       "requires": {
         "css-declaration-sorter": "^4.0.1",
@@ -26344,7 +26282,7 @@
         "postcss-ordered-values": "^4.1.2",
         "postcss-reduce-initial": "^4.0.3",
         "postcss-reduce-transforms": "^4.0.2",
-        "postcss-svgo": "^4.0.2",
+        "postcss-svgo": "^4.0.3",
         "postcss-unique-selectors": "^4.0.1"
       }
     },
@@ -26822,9 +26760,9 @@
       "dev": true
     },
     "dns-packet": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
-      "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
+      "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
       "dev": true,
       "requires": {
         "ip": "^1.1.0",
@@ -27015,9 +26953,9 @@
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.3.707",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.707.tgz",
-      "integrity": "sha512-BqddgxNPrcWnbDdJw7SzXVzPmp+oiyjVrc7tkQVaznPGSS9SKZatw6qxoP857M+HbOyyqJQwYQtsuFIMSTNSZA==",
+      "version": "1.3.745",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.745.tgz",
+      "integrity": "sha512-ZZCx4CS3kYT3BREYiIXocDqlNPT56KfdTS1Ogo4yvxRriBqiEXCDTLIQZT/zNVtby91xTWMMxW2NBiXh8bpLHw==",
       "dev": true
     },
     "elliptic": {
@@ -29469,9 +29407,9 @@
       "dev": true
     },
     "hosted-git-info": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
       "dev": true
     },
     "hpack.js": {
@@ -29498,12 +29436,6 @@
       "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
       "dev": true
     },
-    "html-comment-regex": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
-      "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
-      "dev": true
-    },
     "html-element-map": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.0.tgz",
@@ -30406,15 +30338,6 @@
       "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
       "dev": true
     },
-    "is-svg": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
-      "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==",
-      "dev": true,
-      "requires": {
-        "html-comment-regex": "^1.1.0"
-      }
-    },
     "is-symbol": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
@@ -33220,9 +33143,9 @@
       "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
     },
     "nanoid": {
-      "version": "3.1.22",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
-      "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==",
+      "version": "3.1.23",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
+      "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
       "dev": true
     },
     "nanomatch": {
@@ -34648,12 +34571,11 @@
       }
     },
     "postcss-initial": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.2.tgz",
-      "integrity": "sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz",
+      "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==",
       "dev": true,
       "requires": {
-        "lodash.template": "^4.5.0",
         "postcss": "^7.0.2"
       }
     },
@@ -35298,14 +35220,14 @@
       },
       "dependencies": {
         "postcss": {
-          "version": "8.2.9",
-          "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.9.tgz",
-          "integrity": "sha512-b+TmuIL4jGtCHtoLi+G/PisuIl9avxs8IZMSmlABRwNz5RLUUACrC+ws81dcomz1nRezm5YPdXiMEzBEKgYn+Q==",
+          "version": "8.3.0",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
+          "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==",
           "dev": true,
           "requires": {
             "colorette": "^1.2.2",
-            "nanoid": "^3.1.22",
-            "source-map": "^0.6.1"
+            "nanoid": "^3.1.23",
+            "source-map-js": "^0.6.2"
           }
         }
       }
@@ -35343,12 +35265,11 @@
       }
     },
     "postcss-svgo": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz",
-      "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==",
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
+      "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
       "dev": true,
       "requires": {
-        "is-svg": "^3.0.0",
         "postcss": "^7.0.0",
         "postcss-value-parser": "^3.0.0",
         "svgo": "^1.0.0"
@@ -36820,9 +36741,9 @@
       "dev": true
     },
     "resolve-url-loader": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz",
-      "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.3.tgz",
+      "integrity": "sha512-WbDSNFiKPPLem1ln+EVTE+bFUBdTTytfQZWbmghroaFNFaAVmGq0Saqw6F/306CwgPXsGwXVxbODE+3xAo/YbA==",
       "dev": true,
       "requires": {
         "adjust-sourcemap-loader": "3.0.0",
@@ -37859,6 +37780,12 @@
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true
     },
+    "source-map-js": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
+      "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
+      "dev": true
+    },
     "source-map-resolve": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
@@ -40485,9 +40412,9 @@
           }
         },
         "ws": {
-          "version": "6.2.1",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
-          "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+          "version": "6.2.2",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
+          "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
           "dev": true,
           "requires": {
             "async-limiter": "~1.0.0"
@@ -40963,10 +40890,11 @@
       }
     },
     "ws": {
-      "version": "7.4.4",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
-      "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
-      "dev": true
+      "version": "7.4.6",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+      "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+      "dev": true,
+      "requires": {}
     },
     "xml": {
       "version": "1.0.1",

+ 3 - 1
kafka-ui-react-app/package.json

@@ -1,6 +1,7 @@
 {
   "name": "kafka-ui",
   "version": "0.1.0",
+  "homepage": "./",
   "private": true,
   "dependencies": {
     "@fortawesome/fontawesome-free": "^5.15.3",
@@ -117,7 +118,8 @@
     "typescript": "^4.2.3"
   },
   "engines": {
-    "node": ">=14.15.4"
+    "node": ">=14.15.4",
+    "npm": ">=7.15.1"
   },
   "proxy": "http://localhost:8080",
   "jest": {

+ 3 - 0
kafka-ui-react-app/public/index.html

@@ -7,6 +7,9 @@
     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
     <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
     <title>Kafka UI</title>
+    <script type="text/javascript">
+      window.basePath = "/";
+    </script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 2 - 1
kafka-ui-react-app/src/index.tsx

@@ -6,12 +6,13 @@ import * as serviceWorker from 'serviceWorker';
 import configureStore from 'redux/store/configureStore';
 import AppContainer from 'components/AppContainer';
 import 'theme/index.scss';
+import 'lib/constants';
 
 const store = configureStore();
 
 ReactDOM.render(
   <Provider store={store}>
-    <BrowserRouter>
+    <BrowserRouter basename={window.basePath || '/'}>
       <AppContainer />
     </BrowserRouter>
   </Provider>,

+ 7 - 1
kafka-ui-react-app/src/lib/constants.ts

@@ -1,7 +1,13 @@
 import { ConfigurationParameters } from 'generated-sources';
 
+declare global {
+  interface Window {
+    basePath: string;
+  }
+}
+
 export const BASE_PARAMS: ConfigurationParameters = {
-  basePath: process.env.REACT_APP_API_URL || '',
+  basePath: window.basePath || '',
   credentials: 'include',
   headers: {
     'Content-Type': 'application/json',