瀏覽代碼

Make frontend work properly with custom context url (#2363)

* Add an ability to run app from subfolder

* linting

* Fix all the static resources URLs and manifest.json

Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Oleg Shur 2 年之前
父節點
當前提交
c4f97327c0
共有 45 個文件被更改,包括 487 次插入255 次删除
  1. 21 12
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
  2. 4 0
      kafka-ui-react-app/.jest/resolver.js
  3. 16 5
      kafka-ui-react-app/index.html
  4. 1 3
      kafka-ui-react-app/package.json
  5. 4 18
      kafka-ui-react-app/pnpm-lock.yaml
  6. 0 0
      kafka-ui-react-app/public/favicon/favicon.ico
  7. 2 6
      kafka-ui-react-app/public/manifest.json
  8. 6 5
      kafka-ui-react-app/src/components/App.tsx
  9. 16 8
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  10. 0 21
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  11. 3 1
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx
  12. 2 1
      kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx
  13. 1 1
      kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx
  14. 0 5
      kafka-ui-react-app/src/components/Nav/Nav.styled.ts
  15. 2 1
      kafka-ui-react-app/src/components/Schemas/List/List.tsx
  16. 2 1
      kafka-ui-react-app/src/components/Topics/List/List.tsx
  17. 0 11
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts
  18. 3 13
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts
  19. 10 8
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
  20. 7 7
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx
  21. 2 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx
  22. 3 3
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx
  23. 8 6
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx
  24. 16 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.styled.ts
  25. 9 7
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  26. 6 3
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx
  27. 2 1
      kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx
  28. 1 1
      kafka-ui-react-app/src/components/common/Button/Button.styled.ts
  29. 15 0
      kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx
  30. 15 0
      kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx
  31. 20 0
      kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx
  32. 15 0
      kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx
  33. 17 0
      kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx
  34. 10 0
      kafka-ui-react-app/src/components/common/Icons/SearchIcon.tsx
  35. 85 0
      kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx
  36. 12 18
      kafka-ui-react-app/src/components/common/Input/Input.styled.ts
  37. 6 20
      kafka-ui-react-app/src/components/common/Input/Input.tsx
  38. 2 11
      kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx
  39. 1 1
      kafka-ui-react-app/src/components/common/Search/Search.tsx
  40. 1 0
      kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts
  41. 115 0
      kafka-ui-react-app/src/components/global.css.ts
  42. 1 1
      kafka-ui-react-app/src/index.tsx
  43. 1 37
      kafka-ui-react-app/src/theme/index.scss
  44. 17 1
      kafka-ui-react-app/src/theme/theme.ts
  45. 7 17
      kafka-ui-react-app/vite.config.ts

+ 21 - 12
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java

@@ -20,21 +20,30 @@ public class StaticController {
 
   @Value("classpath:static/index.html")
   private Resource indexFile;
+  @Value("classpath:static/manifest.json")
+  private Resource manifestFile;
+
   private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();
+  private final AtomicReference<String> renderedManifestFile = new AtomicReference<>();
 
   @GetMapping(value = "/index.html", produces = {"text/html"})
   public Mono<ResponseEntity<String>> getIndex(ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(getRenderedIndexFile(exchange)));
+    return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedIndexFile, indexFile)));
+  }
+
+  @GetMapping(value = "/manifest.json", produces = {"application/json"})
+  public Mono<ResponseEntity<String>> getManifest(ServerWebExchange exchange) {
+    return Mono.just(ResponseEntity.ok(getRenderedFile(exchange, renderedManifestFile, manifestFile)));
   }
 
-  public String getRenderedIndexFile(ServerWebExchange exchange) {
-    String rendered = renderedIndexFile.get();
+  public String getRenderedFile(ServerWebExchange exchange, AtomicReference<String> renderedFile, Resource file) {
+    String rendered = renderedFile.get();
     if (rendered == null) {
-      rendered = buildIndexFile(exchange.getRequest().getPath().contextPath().value());
-      if (renderedIndexFile.compareAndSet(null, rendered)) {
+      rendered = buildFile(file, exchange.getRequest().getPath().contextPath().value());
+      if (renderedFile.compareAndSet(null, rendered)) {
         return rendered;
       } else {
-        return renderedIndexFile.get();
+        return renderedFile.get();
       }
     } else {
       return rendered;
@@ -42,11 +51,11 @@ public class StaticController {
   }
 
   @SneakyThrows
-  private String buildIndexFile(String contextPath) {
-    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 + "\"");
+  private String buildFile(Resource file, String contextPath) {
+    return ResourceUtil.readAsString(file)
+        .replace("\"/assets/", "\"" + contextPath + "/assets/")
+        .replace("\"/favicon/", "\"" + contextPath + "/favicon/")
+        .replace("/manifest.json", contextPath + "/manifest.json")
+        .replace("window.basePath = ''", "window.basePath=\"" + contextPath + "\"");
   }
 }

+ 4 - 0
kafka-ui-react-app/.jest/resolver.js

@@ -16,6 +16,10 @@ module.exports = (path, options) => {
         delete pkg['exports'];
         delete pkg['module'];
       }
+      if (pkg.name === 'jsonpath-plus') {
+        delete pkg['exports'];
+        delete pkg['module'];
+      }
       return pkg;
     },
   });

+ 16 - 5
kafka-ui-react-app/index.html

@@ -2,17 +2,28 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <link rel="icon" href="/favicon.ico" sizes="any"><!-- 32×32 -->
-    <link rel="icon" href="/favicon/icon.svg" type="image/svg+xml">
-    <link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png"><!-- 180×180 -->
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+    <!-- Google fonts -->
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Roboto+Mono:wght@400;500&display=swap"
+      rel="stylesheet"
+    />
+
+    <!-- Favicons -->
+    <link rel="icon" href="/favicon/favicon.ico" sizes="any" />
+    <link rel="icon" href="/favicon/icon.svg" type="image/svg+xml" />
+    <link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png" />
     <link rel="manifest" href="/manifest.json" />
 
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
     <title>UI for Apache Kafka</title>
     <script type="text/javascript">
-      window.basePath = "";
+      window.basePath = '';
     </script>
   </head>
+
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>

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

@@ -7,7 +7,6 @@
     "@babel/core": "^7.16.0",
     "@babel/plugin-syntax-flow": "^7.18.6",
     "@babel/plugin-transform-react-jsx": "^7.18.6",
-    "@fortawesome/fontawesome-free": "^6.1.1",
     "@hookform/error-message": "^2.0.0",
     "@hookform/resolvers": "^2.7.1",
     "@reduxjs/toolkit": "^1.8.3",
@@ -20,13 +19,12 @@
     "ace-builds": "^1.7.1",
     "ajv": "^8.6.3",
     "babel-jest": "^28.1.1",
-    "bulma": "^0.9.3",
     "classnames": "^2.2.6",
     "dayjs": "^1.11.2",
     "fetch-mock": "^9.11.0",
     "jest": "^28.1.1",
     "jest-watch-typeahead": "^2.0.0",
-    "json-schema-faker": "^0.5.0-rcv.39",
+    "json-schema-faker": "^0.5.0-rcv.44",
     "lodash": "^4.17.21",
     "pretty-ms": "7.0.1",
     "react": "^18.1.0",

+ 4 - 18
kafka-ui-react-app/pnpm-lock.yaml

@@ -7,7 +7,6 @@ specifiers:
   '@babel/preset-env': ^7.18.2
   '@babel/preset-react': ^7.17.12
   '@babel/preset-typescript': ^7.17.12
-  '@fortawesome/fontawesome-free': ^6.1.1
   '@hookform/error-message': ^2.0.0
   '@hookform/resolvers': ^2.7.1
   '@jest/types': ^28.1.1
@@ -36,7 +35,6 @@ specifiers:
   ace-builds: ^1.7.1
   ajv: ^8.6.3
   babel-jest: ^28.1.1
-  bulma: ^0.9.3
   classnames: ^2.2.6
   dayjs: ^1.11.2
   dotenv: ^16.0.1
@@ -60,7 +58,7 @@ specifiers:
   jest-sonar-reporter: ^2.0.0
   jest-styled-components: ^7.0.8
   jest-watch-typeahead: ^2.0.0
-  json-schema-faker: ^0.5.0-rcv.39
+  json-schema-faker: ^0.5.0-rcv.44
   lint-staged: ^13.0.2
   lodash: ^4.17.21
   prettier: ^2.3.1
@@ -93,7 +91,6 @@ dependencies:
   '@babel/core': 7.18.2
   '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.18.2
   '@babel/plugin-transform-react-jsx': 7.18.6_@babel+core@7.18.2
-  '@fortawesome/fontawesome-free': 6.1.1
   '@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm
   '@hookform/resolvers': 2.8.9_react-hook-form@7.6.9
   '@reduxjs/toolkit': 1.8.3_ctm756ikdwcjcvyfxxwskzbr6q
@@ -106,13 +103,12 @@ dependencies:
   ace-builds: 1.7.1
   ajv: 8.8.2
   babel-jest: 28.1.1_@babel+core@7.18.2
-  bulma: 0.9.3
   classnames: 2.3.1
   dayjs: 1.11.3
   fetch-mock: 9.11.0
   jest: 28.1.1_yqiaopbgmqcuvx27p5xxvum6wm
   jest-watch-typeahead: 2.0.0_jest@28.1.1
-  json-schema-faker: 0.5.0-rcv.40
+  json-schema-faker: 0.5.0-rcv.44
   lodash: 4.17.21
   pretty-ms: 7.0.1
   react: 18.1.0
@@ -1894,12 +1890,6 @@ packages:
       - supports-color
     dev: true
 
-  /@fortawesome/fontawesome-free/6.1.1:
-    resolution: {integrity: sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==}
-    engines: {node: '>=6'}
-    requiresBuild: true
-    dev: false
-
   /@hookform/error-message/2.0.0_l2dcsysovzdujulgxvsen7vbsm:
     resolution: {integrity: sha512-Y90nHzjgL2MP7GFy75kscdvxrCTjtyxGmOLLxX14nd08OXRIh9lMH/y9Kpdo0p1IPowJBiZMHyueg7p+yrqynQ==}
     peerDependencies:
@@ -3376,10 +3366,6 @@ packages:
       ieee754: 1.2.1
     dev: true
 
-  /bulma/0.9.3:
-    resolution: {integrity: sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==}
-    dev: false
-
   /call-bind/1.0.2:
     resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
     dependencies:
@@ -5860,8 +5846,8 @@ packages:
   /json-parse-even-better-errors/2.3.1:
     resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
 
-  /json-schema-faker/0.5.0-rcv.40:
-    resolution: {integrity: sha512-BczZvu03jKrGh3ovCWrHusiX6MwiaKK2WZeyomKBNA8Nm/n7aBYz0mub1CnONB6cgxOZTNxx4afNmLblbUmZbA==}
+  /json-schema-faker/0.5.0-rcv.44:
+    resolution: {integrity: sha512-MbDxYFsPXTVMawW1Y6zEU7QhfwsT+ZJ2d+LI8n57Y8+Xw1Cdx1hITgsFTLNOJ1lDMHZqWeXGGgMbc1hW0BGisg==}
     hasBin: true
     dependencies:
       json-schema-ref-parser: 6.1.0

+ 0 - 0
kafka-ui-react-app/public/favicon.ico → kafka-ui-react-app/public/favicon/favicon.ico


+ 2 - 6
kafka-ui-react-app/public/manifest.json

@@ -11,9 +11,5 @@
       "type": "image/png",
       "sizes": "512x512"
     }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}
+  ]
+}

+ 6 - 5
kafka-ui-react-app/src/components/App.tsx

@@ -12,11 +12,11 @@ import theme from 'theme/theme';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { showServerError } from 'lib/errorHandling';
 import { Toaster } from 'react-hot-toast';
-
-import * as S from './App.styled';
-import Logo from './common/Logo/Logo';
-import GitIcon from './common/Icons/GitIcon';
-import DiscordIcon from './common/Icons/DiscordIcon';
+import GlobalCSS from 'components/global.css';
+import * as S from 'components/App.styled';
+import Logo from 'components/common/Logo/Logo';
+import GitIcon from 'components/common/Icons/GitIcon';
+import DiscordIcon from 'components/common/Icons/DiscordIcon';
 
 const queryClient = new QueryClient({
   defaultOptions: {
@@ -44,6 +44,7 @@ const App: React.FC = () => {
   return (
     <QueryClientProvider client={queryClient}>
       <ThemeProvider theme={theme}>
+        <GlobalCSS />
         <S.Layout>
           <S.Navbar role="navigation" aria-label="Page Header">
             <S.NavbarBrand>

+ 16 - 8
kafka-ui-react-app/src/components/Cluster/Cluster.tsx

@@ -19,15 +19,23 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { useClusters } from 'lib/hooks/api/clusters';
+import Brokers from 'components/Brokers/Brokers';
+import Topics from 'components/Topics/Topics';
+import Schemas from 'components/Schemas/Schemas';
+import Connect from 'components/Connect/Connect';
+import KsqlDb from 'components/KsqlDb/KsqlDb';
+import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';
 
-const Brokers = React.lazy(() => import('components/Brokers/Brokers'));
-const Topics = React.lazy(() => import('components/Topics/Topics'));
-const Schemas = React.lazy(() => import('components/Schemas/Schemas'));
-const Connect = React.lazy(() => import('components/Connect/Connect'));
-const KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb'));
-const ConsumerGroups = React.lazy(
-  () => import('components/ConsumerGroups/ConsumerGroups')
-);
+// We can't use Lazy loading till we have a better way to update publicPath in runtime
+// Now java app replaces paths in builded index.html file.
+// const Brokers = React.lazy(() => import('components/Brokers/Brokers'));
+// const Topics = React.lazy(() => import('components/Topics/Topics'));
+// const Schemas = React.lazy(() => import('components/Schemas/Schemas'));
+// const Connect = React.lazy(() => import('components/Connect/Connect'));
+// const KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb'));
+// const ConsumerGroups = React.lazy(
+//   () => import('components/ConsumerGroups/ConsumerGroups')
+// );
 
 const Cluster: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();

+ 0 - 21
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -71,9 +71,6 @@ const Actions: React.FC = () => {
           onClick={pauseConnectorHandler}
           disabled={isMutating}
         >
-          <span>
-            <i className="fas fa-pause" />
-          </span>
           <span>Pause</span>
         </Button>
       )}
@@ -86,9 +83,6 @@ const Actions: React.FC = () => {
           onClick={resumeConnectorHandler}
           disabled={isMutating}
         >
-          <span>
-            <i className="fas fa-play" />
-          </span>
           <span>Resume</span>
         </Button>
       )}
@@ -100,9 +94,6 @@ const Actions: React.FC = () => {
         onClick={restartConnectorHandler}
         disabled={isMutating}
       >
-        <span>
-          <i className="fas fa-sync-alt" />
-        </span>
         <span>Restart Connector</span>
       </Button>
       <Button
@@ -112,9 +103,6 @@ const Actions: React.FC = () => {
         onClick={restartAllTasksHandler}
         disabled={isMutating}
       >
-        <span>
-          <i className="fas fa-sync-alt" />
-        </span>
         <span>Restart All Tasks</span>
       </Button>
       <Button
@@ -124,9 +112,6 @@ const Actions: React.FC = () => {
         onClick={restartFailedTasksHandler}
         disabled={isMutating}
       >
-        <span>
-          <i className="fas fa-sync-alt" />
-        </span>
         <span>Restart Failed Tasks</span>
       </Button>
       <Button
@@ -140,9 +125,6 @@ const Actions: React.FC = () => {
           routerProps.connectorName
         )}
       >
-        <span>
-          <i className="fas fa-pencil-alt" />
-        </span>
         <span>Edit Config</span>
       </Button>
 
@@ -153,9 +135,6 @@ const Actions: React.FC = () => {
         onClick={setDeleteConnectorConfirmationOpen}
         disabled={isMutating}
       >
-        <span>
-          <i className="far fa-trash-alt" />
-        </span>
         <span>Delete</span>
       </Button>
       <ConfirmationModal

+ 3 - 1
kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx

@@ -18,6 +18,8 @@ jest.mock('lib/hooks/api/kafkaConnect', () => ({
   useConnectors: jest.fn(),
 }));
 
+jest.mock('components/common/Icons/SpinnerIcon', () => () => 'progressbar');
+
 const clusterName = 'local';
 
 describe('Connectors List Page', () => {
@@ -82,7 +84,7 @@ describe('Connectors List Page', () => {
       await renderComponent();
       const metrics = screen.getByRole('group');
       expect(metrics).toBeInTheDocument();
-      expect(within(metrics).getAllByRole('progressbar').length).toEqual(3);
+      expect(within(metrics).getAllByText('progressbar').length).toEqual(3);
     });
 
     it('renders indicators for empty list of connectors', async () => {

+ 2 - 1
kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx

@@ -7,6 +7,7 @@ import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import CloseIcon from 'components/common/Icons/CloseIcon';
 import { yupResolver } from '@hookform/resolvers/yup';
 import yup from 'lib/yupExtended';
+import PlusIcon from 'components/common/Icons/PlusIcon';
 
 import * as S from './QueryForm.styled';
 
@@ -165,7 +166,7 @@ const QueryForm: React.FC<Props> = ({
               buttonType="secondary"
               onClick={() => append({ key: '', value: '' })}
             >
-              <i className="fas fa-plus" />
+              <PlusIcon />
               Add Stream Property
             </Button>
           </S.StreamPropertiesContainer>

+ 1 - 1
kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx

@@ -27,7 +27,7 @@ const ClusterMenu: React.FC<Props> = ({
   const [isOpen, setIsOpen] = React.useState(!!singleMode);
   return (
     <S.List>
-      <S.Divider />
+      <hr />
       <ClusterTab
         title={name}
         status={status}

+ 0 - 5
kafka-ui-react-app/src/components/Nav/Nav.styled.ts

@@ -9,11 +9,6 @@ export const List = styled.ul.attrs({ role: 'menu' })`
   }
 `;
 
-export const Divider = styled.hr`
-  margin: 0;
-  height: 1px;
-`;
-
 export const Link = styled(NavLink)(
   ({ theme }) => css`
     width: 100%;

+ 2 - 1
kafka-ui-react-app/src/components/Schemas/List/List.tsx

@@ -20,6 +20,7 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
 import Search from 'components/common/Search/Search';
 import useSearch from 'lib/hooks/useSearch';
+import PlusIcon from 'components/common/Icons/PlusIcon';
 
 import ListItem from './ListItem';
 import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
@@ -54,7 +55,7 @@ const List: React.FC = () => {
               buttonType="primary"
               to={clusterSchemaNewRelativePath}
             >
-              <i className="fas fa-plus" /> Create Schema
+              <PlusIcon /> Create Schema
             </Button>
           </>
         )}

+ 2 - 1
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -29,6 +29,7 @@ import Switch from 'components/common/Switch/Switch';
 import { SmartTable } from 'components/common/SmartTable/SmartTable';
 import { TableColumn } from 'components/common/SmartTable/TableColumn';
 import { useTableState } from 'lib/hooks/useTableState';
+import PlusIcon from 'components/common/Icons/PlusIcon';
 
 import {
   MessagesCell,
@@ -192,7 +193,7 @@ const List: React.FC<TopicsListProps> = ({
               buttonSize="M"
               to={clusterTopicNewRelativePath}
             >
-              <i className="fas fa-plus" /> Add a Topic
+              <PlusIcon /> Add a Topic
             </Button>
           )}
         </PageHeading>

+ 0 - 11
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts

@@ -1,11 +0,0 @@
-import styled from 'styled-components';
-
-export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
-  leader?: boolean;
-}>`
-  ${this} ~ ${this}::before {
-    color: black;
-    content: ', ';
-  }
-  color: ${(props) => (props.leader ? 'orange' : null)};
-`;

+ 3 - 13
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts

@@ -114,7 +114,7 @@ export const ButtonContainer = styled.div`
 export const ListItem = styled.li`
   font-size: 12px;
   font-weight: 400;
-  margin-left: 20px;
+  margin: 4px 0;
   line-height: 1.5;
   color: ${({ theme }) => theme.table.td.color.normal};
 `;
@@ -127,16 +127,6 @@ export const InfoParagraph = styled.div`
   color: ${({ theme }) => theme.table.td.color.normal};
 `;
 
-export const InfoCodeSample = styled.pre`
-  background: #f5f5f5;
-  padding: 5px;
-  border: 1px solid #e1e1e1;
-  border-radius: 5px;
-  width: fit-content;
-  margin: 5px 20px;
-  color: #cc0f35;
-`;
-
 export const MessageFilterModal = styled.div`
   height: auto;
   width: 560px;
@@ -240,8 +230,9 @@ export const ActiveSmartFilterWrapper = styled.div`
 `;
 
 export const DeleteSavedFilter = styled.div.attrs({ role: 'deleteIcon' })`
-  color: ${({ theme }) => theme.breadcrumb};
+  margin-top: 2px;
   cursor: pointer;
+  color: ${({ theme }) => theme.icons.deleteIcon};
 `;
 
 export const FilterEdit = styled.div`
@@ -299,7 +290,6 @@ export const ActiveSmartFilter = styled.div`
 
 export const DeleteSavedFilterIcon = styled.div`
   color: ${({ theme }) => theme.icons.closeIcon};
-  border-left: 1px solid ${({ theme }) => theme.savedFilterDivider.color};
   display: flex;
   align-items: center;
   padding-left: 6px;

+ 10 - 8
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx

@@ -30,6 +30,11 @@ import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors';
 import { useAppSelector } from 'lib/hooks/redux';
 import { RouteParamsClusterTopic } from 'lib/paths';
 import useAppParams from 'lib/hooks/useAppParams';
+import PlusIcon from 'components/common/Icons/PlusIcon';
+import CloseIcon from 'components/common/Icons/CloseIcon';
+import ClockIcon from 'components/common/Icons/ClockIcon';
+import ArrowDownIcon from 'components/common/Icons/ArrowDownIcon';
+import FileIcon from 'components/common/Icons/FileIcon';
 
 import * as S from './Filters.styled';
 import {
@@ -474,17 +479,14 @@ const Filters: React.FC<FiltersProps> = ({
       </div>
       <S.ActiveSmartFilterWrapper>
         <Button buttonType="primary" buttonSize="M" onClick={toggle}>
-          <i className="fas fa-plus fa-sm" />
+          <PlusIcon />
           Add Filters
         </Button>
         {activeFilter.name && (
           <S.ActiveSmartFilter data-testid="activeSmartFilter">
             {activeFilter.name}
             <S.DeleteSavedFilterIcon onClick={deleteActiveFilter}>
-              <i
-                className="fas fa-times"
-                data-testid="activeSmartFilterCloseIcon"
-              />
+              <CloseIcon />
             </S.DeleteSavedFilterIcon>
           </S.ActiveSmartFilter>
         )}
@@ -519,19 +521,19 @@ const Filters: React.FC<FiltersProps> = ({
         </S.MessageLoading>
         <S.Metric title="Elapsed Time">
           <S.MetricsIcon>
-            <i className="far fa-clock" />
+            <ClockIcon />
           </S.MetricsIcon>
           <span>{Math.max(elapsedMs || 0, 0)} ms</span>
         </S.Metric>
         <S.Metric title="Bytes Consumed">
           <S.MetricsIcon>
-            <i className="fas fa-arrow-down" />
+            <ArrowDownIcon />
           </S.MetricsIcon>
           <BytesFormatted value={bytesConsumed} />
         </S.Metric>
         <S.Metric title="Messages Consumed">
           <S.MetricsIcon>
-            <i className="far fa-file-alt" />
+            <FileIcon />
           </S.MetricsIcon>
           <span>{messagesConsumed} messages consumed</span>
         </S.Metric>

+ 7 - 7
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/InfoModal.tsx

@@ -45,19 +45,19 @@ const InfoModal: React.FC<InfoModalProps> = ({ toggleIsOpen }) => {
             headers[&quot;sentAt&quot;] == &quot;2020-01-01&quot;
           </code>
         </S.ListItem>
-        <S.ListItem>multiline filters are also allowed:</S.ListItem>
-        <S.InfoParagraph>
-          <S.InfoCodeSample>
-            <code>
+        <S.ListItem>
+          multiline filters are also allowed:
+          <S.InfoParagraph>
+            <pre>
               def name = value.name
               <br />
               def age = value.age
               <br />
               name == &quot;iliax&quot; && age == 30
               <br />
-            </code>
-          </S.InfoCodeSample>
-        </S.InfoParagraph>
+            </pre>
+          </S.InfoParagraph>
+        </S.ListItem>
       </ol>
       <S.ButtonContainer>
         <Button

+ 2 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/SavedFilters.tsx

@@ -2,6 +2,7 @@ import React, { FC } from 'react';
 import { Button } from 'components/common/Button/Button';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import useModal from 'lib/hooks/useModal';
+import DeleteIcon from 'components/common/Icons/DeleteIcon';
 
 import * as S from './Filters.styled';
 import { MessageFilters } from './Filters';
@@ -73,7 +74,7 @@ const SavedFilters: FC<Props> = ({
                 Edit
               </S.FilterEdit>
               <S.DeleteSavedFilter onClick={() => deleteFilterHandler(index)}>
-                <i className="fas fa-times" />
+                <DeleteIcon />
               </S.DeleteSavedFilter>
             </S.FilterOptions>
           </S.SavedFilter>

+ 3 - 3
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx

@@ -19,6 +19,8 @@ const defaultContextValue: ContextProps = {
   changeSeekDirection: jest.fn(),
 };
 
+jest.mock('components/common/Icons/CloseIcon', () => () => 'mock-CloseIcon');
+
 const renderComponent = (
   props: Partial<FiltersProps> = {},
   ctx: ContextProps = defaultContextValue
@@ -195,9 +197,7 @@ describe('Filters component', () => {
 
     it('delete the active smart Filter', async () => {
       const smartFilterElement = screen.getByTestId('activeSmartFilter');
-      const deleteIcon = within(smartFilterElement).getByTestId(
-        'activeSmartFilterCloseIcon'
-      );
+      const deleteIcon = within(smartFilterElement).getByText('mock-CloseIcon');
       await act(() => userEvent.click(deleteIcon));
 
       const anotherSmartFilterElement =

+ 8 - 6
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/SavedFilters.spec.tsx

@@ -7,6 +7,8 @@ import { screen, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
+jest.mock('components/common/Icons/DeleteIcon', () => () => 'mock-DeleteIcon');
+
 describe('SavedFilter Component', () => {
   const mockFilters: MessageFilters[] = [
     { name: 'name', code: 'code' },
@@ -115,9 +117,9 @@ describe('SavedFilter Component', () => {
     });
 
     it('Open Confirmation for the deletion modal', () => {
-      const { container } = setUpComponent({ deleteFilter: deleteMock });
+      setUpComponent({ deleteFilter: deleteMock });
       const savedFilters = getSavedFilters();
-      const deleteIcons = container.getElementsByTagName('i');
+      const deleteIcons = screen.getAllByText('mock-DeleteIcon');
       userEvent.hover(savedFilters[0]);
       userEvent.click(deleteIcons[0]);
       const modelDialog = screen.getByRole('dialog');
@@ -128,9 +130,9 @@ describe('SavedFilter Component', () => {
     });
 
     it('Close Confirmations deletion modal with button', () => {
-      const { container } = setUpComponent({ deleteFilter: deleteMock });
+      setUpComponent({ deleteFilter: deleteMock });
       const savedFilters = getSavedFilters();
-      const deleteIcons = container.getElementsByTagName('i');
+      const deleteIcons = screen.getAllByText('mock-DeleteIcon');
 
       userEvent.hover(savedFilters[0]);
       userEvent.click(deleteIcons[0]);
@@ -145,9 +147,9 @@ describe('SavedFilter Component', () => {
     });
 
     it('Delete the saved filter', () => {
-      const { container } = setUpComponent({ deleteFilter: deleteMock });
+      setUpComponent({ deleteFilter: deleteMock });
       const savedFilters = getSavedFilters();
-      const deleteIcons = container.getElementsByTagName('i');
+      const deleteIcons = screen.getAllByText('mock-DeleteIcon');
 
       userEvent.hover(savedFilters[0]);
       userEvent.click(deleteIcons[0]);

+ 16 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.styled.ts

@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const Replica = styled.span.attrs({ 'aria-label': 'replica-info' })<{
+  leader?: boolean;
+}>`
+  color: ${({ leader, theme }) =>
+    leader ? theme.topicMetaData.liderReplica.color : null};
+
+  &:after {
+    content: ', ';
+  }
+
+  &:last-child::after {
+    content: '';
+  }
+`;

+ 9 - 7
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -9,11 +9,12 @@ import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { useAppSelector } from 'lib/hooks/redux';
 import { getTopicByName } from 'redux/reducers/topics/selectors';
-import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
 import { RouteParamsClusterTopic } from 'lib/paths';
 import useAppParams from 'lib/hooks/useAppParams';
 import { Dropdown, DropdownItem } from 'components/common/Dropdown';
 
+import * as S from './Overview.styled';
+
 export interface Props {
   clearTopicMessages(params: {
     clusterName: ClusterName;
@@ -119,13 +120,14 @@ const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
               <tr key={`partition-list-item-key-${partition.partition}`}>
                 <td>{partition.partition}</td>
                 <td>
-                  {partition.replicas?.map((replica: Replica) => (
-                    <ReplicaCell
-                      leader={replica.leader}
-                      key={`replica-list-item-key-${replica.broker}`}
+                  {partition.replicas?.map(({ broker, leader }: Replica) => (
+                    <S.Replica
+                      leader={leader}
+                      key={broker}
+                      title={leader ? 'Leader' : ''}
                     >
-                      {replica.broker}
-                    </ReplicaCell>
+                      {broker}
+                    </S.Replica>
                   ))}
                 </td>
                 <td>{partition.offsetMin}</td>

+ 6 - 3
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx

@@ -10,7 +10,7 @@ import ClusterContext from 'components/contexts/ClusterContext';
 import userEvent from '@testing-library/user-event';
 import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
 import { clusterTopicPath } from 'lib/paths';
-import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
+import { Replica } from 'components/Topics/Topic/Details/Overview/Overview.styled';
 
 describe('Overview', () => {
   const mockClusterName = 'local';
@@ -73,10 +73,13 @@ describe('Overview', () => {
   });
 
   it('renders replica cell with props', () => {
-    render(<ReplicaCell leader />);
+    render(<Replica leader />);
     const element = screen.getByLabelText('replica-info');
     expect(element).toBeInTheDocument();
-    expect(element).toHaveStyleRule('color', 'orange');
+    expect(element).toHaveStyleRule(
+      'color',
+      theme.topicMetaData.liderReplica.color
+    );
   });
 
   describe('when it has internal flag', () => {

+ 2 - 1
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

@@ -3,6 +3,7 @@ import { TopicFormData } from 'redux/interfaces';
 import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
 import { Button } from 'components/common/Button/Button';
 import { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants';
+import PlusIcon from 'components/common/Icons/PlusIcon';
 
 import CustomParamField from './CustomParamField';
 import * as S from './CustomParams.styled';
@@ -58,7 +59,7 @@ const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
           buttonType="secondary"
           onClick={() => append({ name: '', value: '' })}
         >
-          <i className="fas fa-plus" />
+          <PlusIcon />
           Add Custom Parameter
         </Button>
       </div>

+ 1 - 1
kafka-ui-react-app/src/components/common/Button/Button.styled.ts

@@ -58,7 +58,7 @@ const StyledButton = styled.button<ButtonProps>`
     color: ${(props) => props.theme.button.primary.color};
   }
 
-  & i {
+  & svg {
     margin-right: 7px;
   }
 `;

+ 15 - 0
kafka-ui-react-app/src/components/common/Icons/ArrowDownIcon.tsx

@@ -0,0 +1,15 @@
+import React from 'react';
+
+const ArrowDownIcon: React.FC = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 384 512"
+    width="10"
+    height="10"
+  >
+    {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+    <path d="M374.6 310.6l-160 160C208.4 476.9 200.2 480 192 480s-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 370.8V64c0-17.69 14.33-31.1 31.1-31.1S224 46.31 224 64v306.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0S387.1 298.1 374.6 310.6z" />
+  </svg>
+);
+
+export default ArrowDownIcon;

+ 15 - 0
kafka-ui-react-app/src/components/common/Icons/ClockIcon.tsx

@@ -0,0 +1,15 @@
+import React from 'react';
+
+const ClockIcon: React.FC = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 512 512"
+    width={10}
+    height={10}
+  >
+    {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+    <path d="M232 120C232 106.7 242.7 96 256 96C269.3 96 280 106.7 280 120V243.2L365.3 300C376.3 307.4 379.3 322.3 371.1 333.3C364.6 344.3 349.7 347.3 338.7 339.1L242.7 275.1C236 271.5 232 264 232 255.1L232 120zM256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0zM48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48C141.1 48 48 141.1 48 256z" />
+  </svg>
+);
+
+export default ClockIcon;

+ 20 - 0
kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { useTheme } from 'styled-components';
+
+const DeleteIcon: React.FC = () => {
+  const theme = useTheme();
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 448 512"
+      fill={theme.icons.deleteIcon}
+      width="14"
+      height="14"
+    >
+      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+      <path d="M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM31.1 128H416V448C416 483.3 387.3 512 352 512H95.1C60.65 512 31.1 483.3 31.1 448V128zM111.1 208V432C111.1 440.8 119.2 448 127.1 448C136.8 448 143.1 440.8 143.1 432V208C143.1 199.2 136.8 192 127.1 192C119.2 192 111.1 199.2 111.1 208zM207.1 208V432C207.1 440.8 215.2 448 223.1 448C232.8 448 240 440.8 240 432V208C240 199.2 232.8 192 223.1 192C215.2 192 207.1 199.2 207.1 208zM304 208V432C304 440.8 311.2 448 320 448C328.8 448 336 440.8 336 432V208C336 199.2 328.8 192 320 192C311.2 192 304 199.2 304 208z" />
+    </svg>
+  );
+};
+
+export default DeleteIcon;

+ 15 - 0
kafka-ui-react-app/src/components/common/Icons/FileIcon.tsx

@@ -0,0 +1,15 @@
+import React from 'react';
+
+const FileIcon: React.FC = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 384 512"
+    width="10"
+    height="10"
+  >
+    {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+    <path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM96 280C96 293.3 106.8 304 120 304h144C277.3 304 288 293.3 288 280S277.3 256 264 256h-144C106.8 256 96 266.8 96 280zM264 352h-144C106.8 352 96 362.8 96 376s10.75 24 24 24h144c13.25 0 24-10.75 24-24S277.3 352 264 352z" />
+  </svg>
+);
+
+export default FileIcon;

+ 17 - 0
kafka-ui-react-app/src/components/common/Icons/PlusIcon.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+
+const PlusIcon: React.FC = () => {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 448 512"
+      width="14"
+      height="14"
+    >
+      {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+      <path d="M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z" />
+    </svg>
+  );
+};
+
+export default PlusIcon;

+ 10 - 0
kafka-ui-react-app/src/components/common/Icons/SearchIcon.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+
+const SearchIcon: React.FC = () => (
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+    {/* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
+    <path d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z" />
+  </svg>
+);
+
+export default SearchIcon;

+ 85 - 0
kafka-ui-react-app/src/components/common/Icons/SpinnerIcon.tsx

@@ -0,0 +1,85 @@
+import React from 'react';
+import { useTheme } from 'styled-components';
+
+const SpinnerIcon: React.FC = () => {
+  const theme = useTheme();
+  return (
+    <svg
+      width="30"
+      height="30"
+      viewBox="0 0 120 30"
+      xmlns="http://www.w3.org/2000/svg"
+      fill={theme.pageLoader.borderColor}
+    >
+      {/* By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL */}
+      <circle cx="15" cy="15" r="15">
+        <animate
+          attributeName="r"
+          from="15"
+          to="15"
+          begin="0s"
+          dur="0.8s"
+          values="15;9;15"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+        <animate
+          attributeName="fill-opacity"
+          from="1"
+          to="1"
+          begin="0s"
+          dur="0.8s"
+          values="1;.5;1"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+      </circle>
+      <circle cx="60" cy="15" r="9" fillOpacity="0.3">
+        <animate
+          attributeName="r"
+          from="9"
+          to="9"
+          begin="0s"
+          dur="0.8s"
+          values="9;15;9"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+        <animate
+          attributeName="fill-opacity"
+          from="0.5"
+          to="0.5"
+          begin="0s"
+          dur="0.8s"
+          values=".5;1;.5"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+      </circle>
+      <circle cx="105" cy="15" r="15">
+        <animate
+          attributeName="r"
+          from="15"
+          to="15"
+          begin="0s"
+          dur="0.8s"
+          values="15;9;15"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+        <animate
+          attributeName="fill-opacity"
+          from="1"
+          to="1"
+          begin="0s"
+          dur="0.8s"
+          values="1;.5;1"
+          calcMode="linear"
+          repeatCount="indefinite"
+        />
+      </circle>
+    </svg>
+  );
+};
+
+export default SpinnerIcon;

+ 12 - 18
kafka-ui-react-app/src/components/common/Input/Input.styled.ts

@@ -7,6 +7,18 @@ export interface InputProps {
 
 export const Wrapper = styled.div`
   position: relative;
+
+  svg {
+    position: absolute;
+    top: 8px;
+    line-height: 0;
+    z-index: 1;
+    left: 12px;
+    right: unset;
+    height: 16px;
+    width: 16px;
+    fill: ${({ theme }) => theme.input.icon.color};
+  }
 `;
 
 export const Input = styled.input<InputProps>(
@@ -55,21 +67,3 @@ export const FormError = styled.p`
   color: ${({ theme }) => theme.input.error};
   font-size: 12px;
 `;
-
-interface InputIconProps {
-  className: string;
-  position: 'left' | 'right';
-  inputSize: 'M' | 'L';
-}
-
-export const InputIcon = styled.i<InputIconProps>`
-  position: absolute;
-  top: 50%;
-  line-height: 0;
-  z-index: 1;
-  left: ${({ position }) => (position === 'left' ? '12px' : 'unset')};
-  right: ${({ position }) => (position === 'right' ? '15px' : 'unset')};
-  height: 11px;
-  width: 11px;
-  color: ${({ theme }) => theme.input.icon.color};
-`;

+ 6 - 20
kafka-ui-react-app/src/components/common/Input/Input.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import { RegisterOptions, useFormContext } from 'react-hook-form';
+import SearchIcon from 'components/common/Icons/SearchIcon';
 
 import * as S from './Input.styled';
 
@@ -8,44 +9,29 @@ export interface InputProps
     Omit<S.InputProps, 'hasLeftIcon'> {
   name?: string;
   hookFormOptions?: RegisterOptions;
-  leftIcon?: string;
-  rightIcon?: string;
+  search?: boolean;
 }
 
 const Input: React.FC<InputProps> = ({
   name,
   hookFormOptions,
-  leftIcon,
-  rightIcon,
+  search,
   inputSize = 'L',
   ...rest
 }) => {
   const methods = useFormContext();
   return (
     <S.Wrapper>
-      {leftIcon && (
-        <S.InputIcon
-          className={leftIcon}
-          position="left"
-          inputSize={inputSize}
-        />
-      )}
+      {search && <SearchIcon />}
       {name ? (
         <S.Input
           inputSize={inputSize}
           {...methods.register(name, { ...hookFormOptions })}
-          hasLeftIcon={!!leftIcon}
+          hasLeftIcon={!!search}
           {...rest}
         />
       ) : (
-        <S.Input inputSize={inputSize} hasLeftIcon={!!leftIcon} {...rest} />
-      )}
-      {rightIcon && (
-        <S.InputIcon
-          className={rightIcon}
-          position="right"
-          inputSize={inputSize}
-        />
+        <S.Input inputSize={inputSize} hasLeftIcon={!!search} {...rest} />
       )}
     </S.Wrapper>
   );

+ 2 - 11
kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx

@@ -1,4 +1,5 @@
 import React, { PropsWithChildren } from 'react';
+import SpinnerIcon from 'components/common/Icons/SpinnerIcon';
 
 import * as S from './Metrics.styled';
 
@@ -28,17 +29,7 @@ const Indicator: React.FC<PropsWithChildren<Props>> = ({
           </S.CircularAlertWrapper>
         )}
       </S.IndicatorTitle>
-      <span>
-        {fetching ? (
-          <i
-            className="fas fa-spinner fa-pulse"
-            role="progressbar"
-            aria-label="Loading"
-          />
-        ) : (
-          children
-        )}
-      </span>
+      <span>{fetching ? <SpinnerIcon /> : children}</span>
     </div>
   </S.IndicatorWrapper>
 );

+ 1 - 1
kafka-ui-react-app/src/components/common/Search/Search.tsx

@@ -26,9 +26,9 @@ const Search: React.FC<SearchProps> = ({
       placeholder={placeholder}
       onChange={onChange}
       defaultValue={value}
-      leftIcon="fas fa-search"
       inputSize="M"
       disabled={disabled}
+      search
     />
   );
 };

+ 1 - 0
kafka-ui-react-app/src/components/common/table/TableHeaderCell/TableHeaderCell.styled.ts

@@ -115,4 +115,5 @@ export const TableHeaderCell = styled.th`
   padding: 4px 0 4px 24px;
   border-bottom-width: 1px;
   vertical-align: middle;
+  text-align: left;
 `;

+ 115 - 0
kafka-ui-react-app/src/components/global.css.ts

@@ -0,0 +1,115 @@
+import { createGlobalStyle, css } from 'styled-components';
+
+export default createGlobalStyle(
+  ({ theme }) => css`
+    html {
+      font-family: 'Inter', sans-serif;
+      font-size: 14px;
+      -webkit-font-smoothing: antialiased;
+      -moz-osx-font-smoothing: grayscale;
+      background-color: ${theme.layout.backgroundColor};
+      overflow-x: hidden;
+      overflow-y: scroll;
+      text-rendering: optimizeLegibility;
+      text-size-adjust: 100%;
+      min-width: 300px;
+    }
+
+    #root,
+    body {
+      width: 100%;
+      position: relative;
+      margin: 0;
+      font-family: 'Inter', sans-serif;
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 20px;
+    }
+
+    article,
+    aside,
+    figure,
+    footer,
+    header,
+    hgroup,
+    section {
+      display: block;
+    }
+
+    body,
+    button,
+    input,
+    optgroup,
+    select,
+    textarea {
+      font-family: inherit;
+    }
+
+    code,
+    pre {
+      font-family: 'Roboto Mono', sans-serif;
+      -moz-osx-font-smoothing: auto;
+      -webkit-font-smoothing: auto;
+      background-color: ${theme.code.backgroundColor};
+      color: ${theme.code.color};
+      font-size: 12px;
+      font-weight: 400;
+      padding: 2px 8px;
+      border-radius: 5px;
+      width: fit-content;
+    }
+
+    pre {
+      overflow-x: auto;
+      white-space: pre
+      word-wrap: normal
+
+      code {
+        background-color: transparent;
+        color: currentColor
+        padding: 0
+      }
+    }
+
+    a {
+      color: ${theme.link.color};
+      cursor: pointer;
+      text-decoration: none;
+      &:hover {
+        color: ${theme.link.hoverColor};
+      }
+    }
+
+    img {
+      height: auto;
+      max-width: 100%;
+    }
+
+    input[type='checkbox'],
+    input[type='radio'] {
+      vertical-align: baseline;
+    }
+
+    hr {
+      background-color: ${theme.hr.backgroundColor};
+      border: none;
+      display: block;
+      height: 1px;
+      margin: 0;
+    }
+
+    fieldset {
+      border: none;
+    }
+
+
+    @keyframes fadein {
+      from {
+        opacity: 0;
+      }
+      to {
+        opacity: 1;
+      }
+    }
+  `
+);

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

@@ -4,8 +4,8 @@ import { BrowserRouter } from 'react-router-dom';
 import { Provider } from 'react-redux';
 import App from 'components/App';
 import { store } from 'redux/store';
-import 'theme/index.scss';
 import 'lib/constants';
+import 'theme/index.scss';
 
 const container =
   document.getElementById('root') || document.createElement('div');

+ 1 - 37
kafka-ui-react-app/src/theme/index.scss

@@ -1,37 +1 @@
-@import '@fortawesome/fontawesome-free/css/all.min.css';
-
-// Base
-@import "./minireset";
-@import "bulma/sass/base/generic";
-
-@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&display=swap');
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');
-
-#root,
-body,
-html {
-  width: 100%;
-  position: relative;
-  margin: 0;
-  font-family: 'Inter', sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  background-color: #fff;
-}
-
-input, select, textarea, button {
-  font-family: inherit;
-}
-
-code {
-  font-family: 'Roboto Mono', sans-serif;
-}
-
-@keyframes fadein {
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-}
+@import "./minireset.css";

+ 17 - 1
kafka-ui-react-app/src/theme/theme.ts

@@ -60,10 +60,22 @@ const Colors = {
 };
 
 const theme = {
+  link: {
+    color: Colors.brand[50],
+    hoverColor: Colors.brand[60],
+  },
+  hr: {
+    backgroundColor: Colors.neutral[5],
+  },
+  code: {
+    backgroundColor: Colors.neutral[5],
+    color: Colors.red[55],
+  },
   layout: {
+    backgroundColor: Colors.neutral[0],
     minWidth: '1200px',
     navBarWidth: '201px',
-    navBarHeight: '3.25rem',
+    navBarHeight: '53px',
     stuffColor: Colors.neutral[5],
     stuffBorderColor: Colors.neutral[10],
     overlay: {
@@ -477,6 +489,9 @@ const theme = {
       value: Colors.neutral[80],
       meta: Colors.neutral[30],
     },
+    liderReplica: {
+      color: Colors.green[60],
+    },
   },
   dangerZone: {
     borderColor: Colors.neutral[10],
@@ -498,6 +513,7 @@ const theme = {
   },
   icons: {
     closeIcon: Colors.neutral[30],
+    deleteIcon: Colors.red[20],
     warningIcon: Colors.yellow[20],
     messageToggleIcon: {
       normal: Colors.brand[50],

+ 7 - 17
kafka-ui-react-app/vite.config.ts

@@ -1,4 +1,9 @@
-import { defineConfig, loadEnv, UserConfigExport } from 'vite';
+import {
+  defineConfig,
+  loadEnv,
+  UserConfigExport,
+  splitVendorChunkPlugin,
+} from 'vite';
 import react from '@vitejs/plugin-react';
 import tsconfigPaths from 'vite-tsconfig-paths';
 
@@ -6,27 +11,12 @@ export default defineConfig(({ mode }) => {
   process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
 
   const defaultConfig: UserConfigExport = {
-    plugins: [react(), tsconfigPaths()],
+    plugins: [react(), tsconfigPaths(), splitVendorChunkPlugin()],
     server: {
       port: 3000,
     },
     build: {
       outDir: 'build',
-      rollupOptions: {
-        output: {
-          manualChunks: {
-            vendor: [
-              'react',
-              'react-router-dom',
-              'react-dom',
-              'redux',
-              'react-redux',
-              'styled-components',
-              'react-ace',
-            ],
-          },
-        },
-      },
     },
     define: {
       'process.env.NODE_ENV': `"${mode}"`,