Repository: provectus/kafka-ui
Issue: Implement a full support for custom context path
Labels: type/enhancement
scope/frontend
status/accepted

https://webpack.js.org/guides/public-path/

Issue comments:
https://vitejs.dev/guide/build.html#public-base-path

when can configure this during build, by this parameter , if we can actually manage to pass the parameter during build.

but this is not on the fly url change solution , this won't cut it.

https://github.com/vitejs/vite/issues/3522

Pull request review comments:
👍 

Patch that resolved the issue:
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
index 696f47073b7..72138c8010c 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
@@ -53,9 +53,7 @@ public String getRenderedFile(ServerWebExchange exchange, AtomicReference<String
   @SneakyThrows
   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 + "\"");
+        .replace("\"assets/", "\"" + contextPath + "/assets/")
+        .replace("PUBLIC-PATH-VARIABLE",  contextPath);
   }
 }
diff --git a/kafka-ui-react-app/index.html b/kafka-ui-react-app/index.html
index 7b4ed2772aa..aa3f89f8f90 100644
--- a/kafka-ui-react-app/index.html
+++ b/kafka-ui-react-app/index.html
@@ -3,7 +3,6 @@
   <head>
     <meta charset="utf-8" />
     <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 />
@@ -13,14 +12,18 @@
     />
 
     <!-- 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" />
+    <link rel="icon" href="PUBLIC-PATH-VARIABLE/favicon/favicon.ico" sizes="any" />
+    <link rel="icon" href="PUBLIC-PATH-VARIABLE/favicon/icon.svg" type="image/svg+xml" />
+    <link rel="apple-touch-icon" href="PUBLIC-PATH-VARIABLE/favicon/apple-touch-icon.png" />
+    <link rel="manifest" href="PUBLIC-PATH-VARIABLE/manifest.json" />
 
     <title>UI for Apache Kafka</title>
     <script type="text/javascript">
-      window.basePath = '';
+      window.basePath = 'PUBLIC-PATH-VARIABLE';
+
+      window.__assetsPathBuilder = function (importer) {
+        return window.basePath+ "/" + importer;
+      };
     </script>
   </head>
 
diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
index 11cc975b4ff..89c4931754c 100644
--- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
+++ b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
@@ -16,23 +16,15 @@ import {
 import ClusterContext from 'components/contexts/ClusterContext';
 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';
 
-// 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 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>();
@@ -59,49 +51,51 @@ const Cluster: React.FC = () => {
   return (
     <Suspense fallback={<PageLoader />}>
       <ClusterContext.Provider value={contextValue}>
-        <Routes>
-          <Route
-            path={getNonExactPath(clusterBrokerRelativePath)}
-            element={<Brokers />}
-          />
-          <Route
-            path={getNonExactPath(clusterTopicsRelativePath)}
-            element={<Topics />}
-          />
-          <Route
-            path={getNonExactPath(clusterConsumerGroupsRelativePath)}
-            element={<ConsumerGroups />}
-          />
-          {contextValue.hasSchemaRegistryConfigured && (
+        <Suspense fallback={<PageLoader />}>
+          <Routes>
             <Route
-              path={getNonExactPath(clusterSchemasRelativePath)}
-              element={<Schemas />}
+              path={getNonExactPath(clusterBrokerRelativePath)}
+              element={<Brokers />}
             />
-          )}
-          {contextValue.hasKafkaConnectConfigured && (
             <Route
-              path={getNonExactPath(clusterConnectsRelativePath)}
-              element={<Connect />}
+              path={getNonExactPath(clusterTopicsRelativePath)}
+              element={<Topics />}
             />
-          )}
-          {contextValue.hasKafkaConnectConfigured && (
             <Route
-              path={getNonExactPath(clusterConnectorsRelativePath)}
-              element={<Connect />}
+              path={getNonExactPath(clusterConsumerGroupsRelativePath)}
+              element={<ConsumerGroups />}
             />
-          )}
-          {contextValue.hasKsqlDbConfigured && (
+            {contextValue.hasSchemaRegistryConfigured && (
+              <Route
+                path={getNonExactPath(clusterSchemasRelativePath)}
+                element={<Schemas />}
+              />
+            )}
+            {contextValue.hasKafkaConnectConfigured && (
+              <Route
+                path={getNonExactPath(clusterConnectsRelativePath)}
+                element={<Connect />}
+              />
+            )}
+            {contextValue.hasKafkaConnectConfigured && (
+              <Route
+                path={getNonExactPath(clusterConnectorsRelativePath)}
+                element={<Connect />}
+              />
+            )}
+            {contextValue.hasKsqlDbConfigured && (
+              <Route
+                path={getNonExactPath(clusterKsqlDbRelativePath)}
+                element={<KsqlDb />}
+              />
+            )}
             <Route
-              path={getNonExactPath(clusterKsqlDbRelativePath)}
-              element={<KsqlDb />}
+              path="/"
+              element={<Navigate to={clusterBrokerRelativePath} replace />}
             />
-          )}
-          <Route
-            path="/"
-            element={<Navigate to={clusterBrokerRelativePath} replace />}
-          />
-        </Routes>
-        <Outlet />
+          </Routes>
+          <Outlet />
+        </Suspense>
       </ClusterContext.Provider>
     </Suspense>
   );
diff --git a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx b/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
index 9fcb77a79a9..d7b2c6d2bb2 100644
--- a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
+++ b/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
 import ClusterComponent from 'components/Cluster/Cluster';
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterBrokersPath,
@@ -59,6 +59,9 @@ describe('Cluster', () => {
       </WithRoute>,
       { initialEntries: [pathname] }
     );
+    await waitFor(() => {
+      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
+    });
   };
 
   it('renders Brokers', async () => {
diff --git a/kafka-ui-react-app/vite.config.ts b/kafka-ui-react-app/vite.config.ts
index 7b329779071..3320bb8cd87 100644
--- a/kafka-ui-react-app/vite.config.ts
+++ b/kafka-ui-react-app/vite.config.ts
@@ -18,6 +18,26 @@ export default defineConfig(({ mode }) => {
     build: {
       outDir: 'build',
     },
+    experimental: {
+      renderBuiltUrl(
+        filename: string,
+        {
+          hostType,
+        }: {
+          hostId: string;
+          hostType: 'js' | 'css' | 'html';
+          type: 'asset' | 'public';
+        }
+      ) {
+        if (hostType === 'js') {
+          return {
+            runtime: `window.__assetsPathBuilder(${JSON.stringify(filename)})`,
+          };
+        }
+
+        return filename;
+      },
+    },
     define: {
       'process.env.NODE_ENV': `"${mode}"`,
       'process.env.VITE_TAG': `"${process.env.VITE_TAG}"`,


Test patch: