diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index 163d93960ff..d3275a89266 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -14,9 +14,15 @@ clusterName: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} swagger: resourcePackage: org.openmetadata.service.resources +assets: + resourcePath: /assets/ + uriPath: ${BASE_PATH:-/} + +basePath: ${BASE_PATH:-/} server: - rootPath: '/api/*' + applicationContextPath: ${BASE_PATH:-/} + rootPath: ${BASE_PATH:-/}api/* applicationConnectors: - type: http bindHost: ${SERVER_HOST:-0.0.0.0} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index fd39a20358d..3f9e7bbc0bc 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -262,7 +262,7 @@ public class OpenMetadataApplication extends Application assets; + @JsonProperty("database") @NotNull @Valid diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java index 11cfa18a6d6..8ebcb020a11 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java @@ -19,6 +19,8 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.HashMap; +import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -54,6 +56,14 @@ public class ConfigResource { this.openMetadataApplicationConfig = config; } + @GET + @Produces(MediaType.APPLICATION_JSON) + public Map getConfig() { + Map config = new HashMap<>(); + config.put("basePath", openMetadataApplicationConfig.getBasePath()); + return config; + } + @GET @Path(("/auth")) @Operation( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/IndexResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/IndexResource.java new file mode 100644 index 00000000000..0ea178c67fb --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/IndexResource.java @@ -0,0 +1,46 @@ +package org.openmetadata.service.resources.system; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.stream.Collectors; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.openmetadata.service.OpenMetadataApplicationConfig; + +@Path("/") +public class IndexResource { + private String indexHtml; + + public IndexResource() { + InputStream inputStream = getClass().getResourceAsStream("/assets/index.html"); + indexHtml = + new BufferedReader(new InputStreamReader(inputStream)) + .lines() + .collect(Collectors.joining("\n")); + } + + public void initialize(OpenMetadataApplicationConfig config) { + this.indexHtml = this.indexHtml.replace("${basePath}", config.getBasePath()); + } + + public static String getIndexFile(String basePath) { + + InputStream inputStream = IndexResource.class.getResourceAsStream("/assets/index.html"); + String indexHtml = + new BufferedReader(new InputStreamReader(inputStream)) + .lines() + .collect(Collectors.joining("\n")); + + return indexHtml.replace("${basePath}", basePath); + } + + @GET + @Produces(MediaType.TEXT_HTML) + public Response getIndex() { + return Response.ok(indexHtml).build(); + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java index 8a586acdaf0..c0dd021fb23 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java @@ -35,7 +35,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import java.net.URL; import java.security.interfaces.RSAPublicKey; -import java.util.*; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; import java.util.stream.Collectors; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/socket/OpenMetadataAssetServlet.java b/openmetadata-service/src/main/java/org/openmetadata/service/socket/OpenMetadataAssetServlet.java index cd05a3f4ca6..eff91f94529 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/socket/OpenMetadataAssetServlet.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/socket/OpenMetadataAssetServlet.java @@ -23,20 +23,39 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.Nullable; import org.openmetadata.service.config.OMWebConfiguration; +import org.openmetadata.service.resources.system.IndexResource; public class OpenMetadataAssetServlet extends AssetServlet { private final OMWebConfiguration webConfiguration; + private final String basePath; + public OpenMetadataAssetServlet( - String resourcePath, String uriPath, @Nullable String indexFile, OMWebConfiguration webConf) { + String basePath, + String resourcePath, + String uriPath, + @Nullable String indexFile, + OMWebConfiguration webConf) { super(resourcePath, uriPath, indexFile, "text/html", StandardCharsets.UTF_8); this.webConfiguration = webConf; + this.basePath = basePath; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { setSecurityHeader(webConfiguration, resp); + + String requestUri = req.getRequestURI(); + + if (requestUri.endsWith("/")) { + IndexResource index = new IndexResource(); + // Write the dynamic config.js content to the response + resp.setContentType("text/html"); + resp.getWriter().write(IndexResource.getIndexFile(this.basePath)); + return; + } + super.doGet(req, resp); if (!resp.isCommitted() && (resp.getStatus() == 404)) { resp.sendError(404); diff --git a/openmetadata-ui/src/main/resources/ui/public/index.html b/openmetadata-ui/src/main/resources/ui/public/index.html index 172e3f6bb42..7da003ff2d1 100644 --- a/openmetadata-ui/src/main/resources/ui/public/index.html +++ b/openmetadata-ui/src/main/resources/ui/public/index.html @@ -15,88 +15,95 @@ + - + + - + - + OpenMetadata diff --git a/openmetadata-ui/src/main/resources/ui/src/@types/global.d.ts b/openmetadata-ui/src/main/resources/ui/src/@types/global.d.ts new file mode 100644 index 00000000000..3050e32ffce --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/@types/global.d.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface Window { + BASE_PATH?: string; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.test.ts index a5be78ef3c2..2c6569c078b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.test.ts @@ -11,7 +11,6 @@ * limitations under the License. */ import { renderHook } from '@testing-library/react-hooks'; -import process from 'process'; import { useLocation } from 'react-router-dom'; import useCustomLocation from './useCustomLocation'; @@ -22,7 +21,7 @@ jest.mock('react-router-dom', () => ({ describe('useCustomLocation', () => { it('should modify the pathname correctly', () => { // Mock the environment variable - process.env.APP_SUB_PATH = '/app'; + window.BASE_PATH = '/app/'; // Mock the useLocation hook (useLocation as jest.Mock).mockReturnValue({ @@ -65,7 +64,7 @@ describe('useCustomLocation', () => { it('should return the original location object if APP_SUB_PATH is not set', () => { // Mock the environment variable - delete process.env.APP_SUB_PATH; + delete window.BASE_PATH; // Mock the useLocation hook (useLocation as jest.Mock).mockReturnValue({ @@ -91,7 +90,7 @@ describe('useCustomLocation', () => { it('should return the original location object if APP_SUB_PATH is empty', () => { // Mock the environment variable - process.env.APP_SUB_PATH = ''; + window.BASE_PATH = ''; // Mock the useLocation hook (useLocation as jest.Mock).mockReturnValue({ @@ -117,7 +116,7 @@ describe('useCustomLocation', () => { it('should return the original location object if APP_SUB_PATH is not a prefix', () => { // Mock the environment variable - process.env.APP_SUB_PATH = '/test'; + window.BASE_PATH = '/test'; // Mock the useLocation hook (useLocation as jest.Mock).mockReturnValue({ @@ -143,7 +142,7 @@ describe('useCustomLocation', () => { it('should return the updated pathname on second render', () => { // Mock the environment variable - process.env.APP_SUB_PATH = '/app'; + window.BASE_PATH = '/app/'; // Mock the useLocation hook (useLocation as jest.Mock).mockReturnValue({ diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.ts index db9e294434a..7b5f6efc795 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomLocation/useCustomLocation.ts @@ -10,15 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import process from 'process'; import { useLocation } from 'react-router-dom'; +import { getBasePath } from '../../utils/HistoryUtils'; const useCustomLocation = (): ReturnType => { const location = useLocation(); - const modifiedPathname = location.pathname.replace( - process.env.APP_SUB_PATH ?? '', - '' - ); + const modifiedPathname = location.pathname.replace(getBasePath(), ''); return { ...location, diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/index.ts b/openmetadata-ui/src/main/resources/ui/src/rest/index.ts index 3e9721f9247..fd795dc4062 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/index.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/index.ts @@ -13,9 +13,10 @@ import axios from 'axios'; import Qs from 'qs'; +import { getBasePath } from '../utils/HistoryUtils'; const axiosClient = axios.create({ - baseURL: '/api/v1', + baseURL: `${getBasePath()}/api/v1`, paramsSerializer: (params) => Qs.stringify(params, { arrayFormat: 'comma' }), }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts index 665bfbdb78a..d2c591e5015 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts @@ -21,7 +21,6 @@ import { CookieStorage } from 'cookie-storage'; import jwtDecode, { JwtPayload } from 'jwt-decode'; import { first, get, isEmpty, isNil } from 'lodash'; import { WebStorageStateStore } from 'oidc-client'; -import process from 'process'; import { AuthenticationConfigurationWithScope, OidcUser, @@ -35,6 +34,7 @@ import { } from '../generated/configuration/authenticationConfiguration'; import { AuthProvider } from '../generated/settings/settings'; import { isDev } from './EnvironmentUtils'; +import { getBasePath } from './HistoryUtils'; import { setOidcToken } from './LocalStorageUtils'; const cookieStorage = new CookieStorage(); @@ -42,7 +42,7 @@ const cookieStorage = new CookieStorage(); // 1 minutes for client auth approach export const EXPIRY_THRESHOLD_MILLES = 1 * 60 * 1000; -const subPath = process.env.APP_SUB_PATH ?? ''; +const subPath = getBasePath(); export const getRedirectUri = (callbackUrl: string) => { return isDev() diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts index eead40242d3..3214bc54e6a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts @@ -12,14 +12,15 @@ */ import { createBrowserHistory } from 'history'; -import process from 'process'; -const subPath = process.env.APP_SUB_PATH ?? ''; +export const getBasePath = () => { + return window.BASE_PATH?.slice(0, -1) ?? ''; +}; export const history = createBrowserHistory( - subPath + getBasePath() ? { - basename: subPath, + basename: getBasePath(), } : {} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts index 11a719bbf07..f336d3d3b39 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts @@ -13,7 +13,6 @@ import { isUndefined } from 'lodash'; import { ServiceTypes } from 'Models'; -import process from 'process'; import QueryString from 'qs'; import { IN_PAGE_SEARCH_ROUTES, @@ -49,6 +48,7 @@ import { ProfilerDashboardType } from '../enums/table.enum'; import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline'; import { DataQualityPageTabs } from '../pages/DataQuality/DataQualityPage.interface'; import { IncidentManagerTabs } from '../pages/IncidentManager/IncidentManager.interface'; +import { getBasePath } from './HistoryUtils'; import { getPartialNameFromFQN } from './CommonUtils'; import { getServiceRouteFromServiceType } from './ServiceUtils'; import { getEncodedFqn } from './StringsUtils'; @@ -608,9 +608,8 @@ export const getNotificationAlertDetailsPath = (fqn: string, tab?: string) => { return path; }; - export const getPathNameFromWindowLocation = () => { - return window.location.pathname.replace(process.env.APP_SUB_PATH ?? '', ''); + return window.location.pathname.replace(getBasePath() ?? '', ''); }; export const getTagsDetailsPath = (entityFQN: string) => { diff --git a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js index ca3bf866cd4..32f82769807 100644 --- a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js +++ b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js @@ -18,7 +18,6 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); const process = require('process'); const outputPath = path.join(__dirname, 'build'); -const subPath = process.env.APP_SUB_PATH ?? ''; const devServerTarget = process.env.DEV_SERVER_TARGET ?? 'http://localhost:8585/'; @@ -37,7 +36,7 @@ module.exports = { // Clean the output directory before emit. clean: true, // Ensures bundle is served from absolute path as opposed to relative - publicPath: `${subPath}/`, + publicPath: `/`, }, // Loaders @@ -199,11 +198,6 @@ module.exports = { // Route all requests to index.html so that app gets to handle all copy pasted deep links historyApiFallback: { disableDotRule: true, - ...(subPath - ? { - index: `${subPath}/index.html`, - } - : {}), }, // Proxy configuration proxy: [ diff --git a/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js b/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js index 0bd45672928..f89d886d88e 100644 --- a/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js +++ b/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js @@ -20,7 +20,6 @@ const TerserPlugin = require('terser-webpack-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const outputPath = path.join(__dirname, 'dist/assets'); -const subPath = process.env.APP_SUB_PATH ?? ''; module.exports = { cache: { @@ -39,10 +38,12 @@ module.exports = { // Output configuration output: { path: outputPath, - filename: '[name].[contenthash].js', // Use contenthash for unique filenames - chunkFilename: '[name].[contenthash].js', // Ensure unique chunk filenames - clean: true, // Clean the output directory before emit - publicPath: `${subPath ?? ''}/`, + filename: '[name].[fullhash].js', + chunkFilename: '[name].[fullhash].js', + // Clean the output directory before emit. + clean: true, + // Ensures bundle is served from absolute path as opposed to relative + publicPath: '/', }, // Loaders