mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-09 05:56:17 +00:00
* Fix #18110: Allow serving UI under a subpath * Update ui package to pick up BASE_PATH * apply java check style * update * update ui part * update UI paths * fix unit tests * fix build * fix tests --------- Co-authored-by: Chira Madlani <chirag@getcollate.io> Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com>
This commit is contained in:
parent
a999236026
commit
2f4355bd4e
@ -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}
|
||||
|
||||
@ -262,7 +262,7 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
|
||||
registerSamlServlets(catalogConfig, environment);
|
||||
|
||||
// Asset Servlet Registration
|
||||
registerAssetServlet(catalogConfig.getWebConfiguration(), environment);
|
||||
registerAssetServlet(catalogConfig, catalogConfig.getWebConfiguration(), environment);
|
||||
|
||||
// Handle Services Jobs
|
||||
registerHealthCheckJobs(catalogConfig);
|
||||
@ -358,10 +358,15 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
|
||||
EnumSet.allOf(DispatcherType.class), true, eventMonitorConfiguration.getPathPattern());
|
||||
}
|
||||
|
||||
private void registerAssetServlet(OMWebConfiguration webConfiguration, Environment environment) {
|
||||
private void registerAssetServlet(
|
||||
OpenMetadataApplicationConfig config,
|
||||
OMWebConfiguration webConfiguration,
|
||||
Environment environment) {
|
||||
|
||||
// Handle Asset Using Servlet
|
||||
OpenMetadataAssetServlet assetServlet =
|
||||
new OpenMetadataAssetServlet("/assets", "/", "index.html", webConfiguration);
|
||||
new OpenMetadataAssetServlet(
|
||||
config.getBasePath(), "/assets", "/", "index.html", webConfiguration);
|
||||
String pathPattern = "/" + '*';
|
||||
environment.servlets().addServlet("static", assetServlet).addMapping(pathPattern);
|
||||
}
|
||||
|
||||
@ -44,6 +44,13 @@ import org.openmetadata.service.util.JsonUtils;
|
||||
@Getter
|
||||
@Setter
|
||||
public class OpenMetadataApplicationConfig extends Configuration {
|
||||
|
||||
@Getter @JsonProperty private String basePath;
|
||||
|
||||
@Getter
|
||||
@JsonProperty("assets")
|
||||
private Map<String, String> assets;
|
||||
|
||||
@JsonProperty("database")
|
||||
@NotNull
|
||||
@Valid
|
||||
|
||||
@ -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<String, String> getConfig() {
|
||||
Map<String, String> config = new HashMap<>();
|
||||
config.put("basePath", openMetadataApplicationConfig.getBasePath());
|
||||
return config;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(("/auth"))
|
||||
@Operation(
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -15,88 +15,95 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta
|
||||
name="msapplication-TileImage"
|
||||
content="/favicons/ms-icon-144x144.png"
|
||||
content="${basePath}favicons/ms-icon-144x144.png"
|
||||
/>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="description" content="OpenMetadata Application" />
|
||||
<meta property="og:title" content="OpenMetadata" />
|
||||
<meta property="og:description" content="OpenMetadata Application" />
|
||||
<meta property="og:image" content="/favicons/android-icon-192x192.png" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="${basePath}favicons/android-icon-192x192.png"
|
||||
/>
|
||||
<script>
|
||||
window.BASE_PATH = '${basePath}';
|
||||
</script>
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.png" type="image/png" />
|
||||
<link rel="shortcut icon" href="${basePath}favicon.png" type="image/png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="/favicons/apple-icon-57x57.png"
|
||||
href="${basePath}favicons/apple-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="/favicons/apple-icon-60x60.png"
|
||||
href="${basePath}favicons/apple-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="/favicons/apple-icon-72x72.png"
|
||||
href="${basePath}favicons/apple-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/favicons/apple-icon-76x76.png"
|
||||
href="${basePath}favicons/apple-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="/favicons/apple-icon-114x114.png"
|
||||
href="${basePath}favicons/apple-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="/favicons/apple-icon-120x120.png"
|
||||
href="${basePath}favicons/apple-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="/favicons/apple-icon-144x144.png"
|
||||
href="${basePath}favicons/apple-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/favicons/apple-icon-152x152.png"
|
||||
href="${basePath}favicons/apple-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicons/apple-icon-180x180.png"
|
||||
href="${basePath}favicons/apple-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="192x192"
|
||||
href="/favicons/android-icon-192x192.png"
|
||||
href="${basePath}favicons/android-icon-192x192.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicons/favicon-32x32.png"
|
||||
href="${basePath}favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="96x96"
|
||||
href="/favicons/favicon-96x96.png"
|
||||
href="${basePath}favicons/favicon-96x96.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicons/favicon-16x16.png"
|
||||
href="${basePath}favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="manifest" href="${basePath}manifest.json" />
|
||||
|
||||
<title>OpenMetadata</title>
|
||||
</head>
|
||||
|
||||
16
openmetadata-ui/src/main/resources/ui/src/@types/global.d.ts
vendored
Normal file
16
openmetadata-ui/src/main/resources/ui/src/@types/global.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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<typeof useLocation> => {
|
||||
const location = useLocation();
|
||||
const modifiedPathname = location.pathname.replace(
|
||||
process.env.APP_SUB_PATH ?? '',
|
||||
''
|
||||
);
|
||||
const modifiedPathname = location.pathname.replace(getBasePath(), '');
|
||||
|
||||
return {
|
||||
...location,
|
||||
|
||||
@ -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' }),
|
||||
});
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
: {}
|
||||
);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user