Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"build": "rm -rf build && DISABLE_ESLINT_PLUGIN=true CI=true react-app-rewired build",
"//build:embedded": "echo 'PUBLIC_URL is a setting for create-react-app. Embedded version is built and hosted as is on ydb servers, with no way of knowing the final URL pattern. PUBLIC_URL=. keeps paths to all static relative, allowing servers to handle them as needed'",
"build:embedded": "GENERATE_SOURCEMAP=false PUBLIC_URL=. REACT_APP_BACKEND=http://localhost:8765 REACT_APP_META_BACKEND=undefined npm run build",
"build:embedded-mc": "GENERATE_SOURCEMAP=false PUBLIC_URL=. REACT_APP_BACKEND= REACT_APP_META_BACKEND= npm run build",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build embedded multi-cluster version - app host is used as meta backend

"build:embedded:archive": "npm run build:embedded && mv build embedded-ui && cp CHANGELOG.md embedded-ui/CHANGELOG.md && zip -r embedded-ui.zip embedded-ui && rm -rf embedded-ui",
"lint": "run-p lint:*",
"lint:js": "eslint --ext .js,.jsx,.ts,.tsx .",
Expand Down
11 changes: 7 additions & 4 deletions src/components/TenantNameWrapper/TenantNameWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ export function TenantNameWrapper({tenant, additionalTenantsProps}: TenantNameWr
status={tenant.Overall}
infoPopoverContent={infoPopoverContent}
hasClipboardButton
path={getTenantPath({
database: tenant.Name,
backend,
})}
path={getTenantPath(
{
database: tenant.Name,
backend,
},
{withBasename: isExternalLink},
)}
/>
);
}
5 changes: 3 additions & 2 deletions src/containers/Cluster/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {CreateHrefOptions} from '../../routes';
import routes, {createHref} from '../../routes';
import type {ValueOf} from '../../types/common';

Expand Down Expand Up @@ -44,6 +45,6 @@ export function isClusterTab(tab: any): tab is ClusterTab {
return Object.values(clusterTabsIds).includes(tab);
}

export const getClusterPath = (activeTab?: ClusterTab, query = {}) => {
return createHref(routes.cluster, activeTab ? {activeTab} : undefined, query);
export const getClusterPath = (activeTab?: ClusterTab, query = {}, options?: CreateHrefOptions) => {
return createHref(routes.cluster, activeTab ? {activeTab} : undefined, query, options);
};
8 changes: 6 additions & 2 deletions src/containers/Clusters/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const CLUSTERS_COLUMNS: Column<PreparedCluster>[] = [
const clusterPath =
useEmbeddedUi && backend
? createDeveloperUIMonitoringPageHref(backend)
: getClusterPath(undefined, {backend, clusterName});
: getClusterPath(undefined, {backend, clusterName}, {withBasename: true});

const clusterStatus = row.cluster?.Overall;

Expand Down Expand Up @@ -110,7 +110,11 @@ export const CLUSTERS_COLUMNS: Column<PreparedCluster>[] = [
preparedVersions.length > 0 && (
<ExternalLink
className={b('cluster-versions')}
href={getClusterPath(clusterTabsIds.versions, {backend, clusterName})}
href={getClusterPath(
clusterTabsIds.versions,
{backend, clusterName},
{withBasename: true},
)}
>
<React.Fragment>
{preparedVersions.map((item, index) => (
Expand Down
5 changes: 3 additions & 2 deletions src/containers/Tenant/TenantPages.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {CreateHrefOptions} from '../../routes';
import routes, {createHref} from '../../routes';
import {TENANT_SUMMARY_TABS_IDS} from '../../store/reducers/tenant/constants';
import type {paramSetup} from '../../store/state-url-mapping';
Expand Down Expand Up @@ -40,6 +41,6 @@ export const TENANT_SCHEMA_TAB = [
},
];

export const getTenantPath = (query: TenantQuery) => {
return createHref(routes.tenant, undefined, query);
export const getTenantPath = (query: TenantQuery, options?: CreateHrefOptions) => {
return createHref(routes.tenant, undefined, query, options);
};
17 changes: 15 additions & 2 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import qs from 'qs';
import type {QueryParamConfig} from 'use-query-params';
import {StringParam} from 'use-query-params';

import {backend, clusterName, webVersion} from './store';
import {backend, basename, clusterName, webVersion} from './store';
import {normalizePathSlashes} from './utils';

export const CLUSTERS = 'clusters';
export const CLUSTER = 'cluster';
Expand Down Expand Up @@ -55,10 +56,15 @@ const prepareRoute = (route: string) => {

type Query = AnyRecord;

export interface CreateHrefOptions {
withBasename?: boolean;
}

export function createHref(
route: string,
params?: Record<string, string | number | undefined>,
query: Query = {},
options: CreateHrefOptions = {},
) {
let extendedQuery = query;

Expand All @@ -78,7 +84,14 @@ export function createHref(

const preparedRoute = prepareRoute(route);

return `${compile(preparedRoute)(params)}${search}`;
const compiledRoute = `${compile(preparedRoute)(params)}${search}`;

if (options.withBasename) {
// For SPA links react-router adds basename itself
// It is needed for external links - <a> or uikit <Link>
return normalizePathSlashes(`${basename}/${compiledRoute}`);
}
return compiledRoute;
}

// embedded version could be located in some folder (e.g. host/some_folder/app_router_path)
Expand Down
116 changes: 116 additions & 0 deletions src/store/__test__/getUrlData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {getUrlData} from '../getUrlData';

describe('getUrlData', () => {
const windowSpy = jest.spyOn(window, 'window', 'get');

afterEach(() => {
windowSpy.mockClear();
});
afterAll(() => {
windowSpy.mockRestore();
});

describe('multi-cluster version', () => {
test('should parse pathname with folder', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://ydb-ui/ui/cluster?clusterName=my_cluster&backend=http://my-node:8765',
pathname: '/ui/cluster?clusterName=my_cluster&backend=http://my-node:8765',
},
} as Window & typeof globalThis;
});
const result = getUrlData({singleClusterMode: false, customBackend: undefined});
expect(result).toEqual({
basename: '/ui',
backend: 'http://my-node:8765',
clusterName: 'my_cluster',
});
});
test('should parse pathname with folder and some prefix', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://ydb-ui/monitoring/ui/cluster?clusterName=my_cluster&backend=http://my-node:8765',
pathname:
'/monitoring/ui/cluster?clusterName=my_cluster&backend=http://my-node:8765',
},
} as Window & typeof globalThis;
});
const result = getUrlData({singleClusterMode: false, customBackend: undefined});
expect(result).toEqual({
basename: '/monitoring/ui',
backend: 'http://my-node:8765',
clusterName: 'my_cluster',
});
});
test('should parse pathname with folder and some prefix', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://ydb-ui/cluster?clusterName=my_cluster&backend=http://my-node:8765',
pathname: '/cluster?clusterName=my_cluster&backend=http://my-node:8765',
},
} as Window & typeof globalThis;
});
const result = getUrlData({singleClusterMode: false, customBackend: undefined});
expect(result).toEqual({
basename: '',
backend: 'http://my-node:8765',
clusterName: 'my_cluster',
});
});
});
describe('single-cluster version with custom backend', () => {
test('should parse correclty parse pathname', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://localhost:3000/cluster',
pathname: '/cluster',
},
} as Window & typeof globalThis;
});
const result = getUrlData({
singleClusterMode: true,
customBackend: 'http://my-node:8765',
});
expect(result).toEqual({
basename: '',
backend: 'http://my-node:8765',
});
});
});
describe('single-cluster embedded version', () => {
test('should parse pathname with folder', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://my-node:8765/monitoring/cluster',
pathname: '/monitoring/cluster',
},
} as Window & typeof globalThis;
});
const result = getUrlData({singleClusterMode: true, customBackend: undefined});
expect(result).toEqual({
basename: '/monitoring',
backend: '',
});
});
test('should parse pathname with folder and some prefix', () => {
windowSpy.mockImplementation(() => {
return {
location: {
href: 'http://my-node:8765/node/12/monitoring/cluster',
pathname: '/node/12/monitoring/cluster',
},
} as Window & typeof globalThis;
});
const result = getUrlData({singleClusterMode: true, customBackend: undefined});
expect(result).toEqual({
basename: '/node/12/monitoring',
backend: '/node/12',
});
});
});
});
1 change: 0 additions & 1 deletion src/store/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function configureStore({
api = new YdbEmbeddedAPI({webVersion, withCredentials: !customBackend}),
} = {}) {
({backend, basename, clusterName} = getUrlData({
href: window.location.href,
singleClusterMode,
customBackend,
}));
Expand Down
42 changes: 29 additions & 13 deletions src/store/getUrlData.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import {normalizePathSlashes} from '../utils';

export const getUrlData = ({
href,
singleClusterMode,
customBackend,
}: {
href: string;
singleClusterMode: boolean;
customBackend?: string;
}) => {
// UI could be located in "monitoring" or "ui" folders
const parsedPrefix = window.location.pathname.match(/.*(?=\/(monitoring|ui)\/)/) || [];
const basenamePrefix = parsedPrefix.length > 0 ? parsedPrefix[0] : '';
const folder = parsedPrefix.length > 1 ? parsedPrefix[1] : '';

let basename = '';

if (folder && !basenamePrefix) {
basename = normalizePathSlashes(`/${folder}`);
} else if (folder && basenamePrefix) {
basename = normalizePathSlashes(`${basenamePrefix}/${folder}`);
}

const urlSearchParams = new URL(window.location.href).searchParams;
const backend = urlSearchParams.get('backend') ?? undefined;
const clusterName = urlSearchParams.get('clusterName') ?? undefined;

if (!singleClusterMode) {
const urlSearchParams = new URL(href).searchParams;
const backend = urlSearchParams.get('backend') ?? undefined;
const clusterName = urlSearchParams.get('clusterName') ?? undefined;
// Multi-cluster version
// Cluster and backend are determined by url params
return {
basename: '/',
basename,
backend,
clusterName,
};
} else if (customBackend) {
const urlSearchParams = new URL(href).searchParams;
const backend = urlSearchParams.get('backend') ?? undefined;
// Single-cluster version
// UI and backend are on different hosts
// There is a backend url param for requests
return {
basename: '/',
basename,
backend: backend ? backend : customBackend,
};
} else {
const parsedPrefix = window.location.pathname.match(/.*(?=\/monitoring)/) || [];
const basenamePrefix = parsedPrefix.length > 0 ? parsedPrefix[0] : '';
const basename = [basenamePrefix, 'monitoring'].filter(Boolean).join('/');

// Single-cluster version
// UI and backend are located on the same host
// We use the the host for backend requests
return {
basename,
backend: basenamePrefix || '',
Expand Down
5 changes: 5 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export async function wait<T = unknown>(time: number, value?: T): Promise<T | un
setTimeout(() => resolve(value), time);
});
}

export function normalizePathSlashes(path: string) {
// Prevent multiple slashes when concatenating path parts
return path.replaceAll(/([^:])(\/\/+)/g, '$1/');
}
6 changes: 3 additions & 3 deletions src/utils/parseBalancer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {normalizePathSlashes} from '.';

const protocolRegex = /^http[s]?:\/\//;
const viewerPathnameRegex = /\/viewer\/json$/;

Expand Down Expand Up @@ -63,9 +65,7 @@ export function prepareBackendFromBalancer(rawBalancer: string) {

// Use meta_backend if it is defined to form backend url
if (window.meta_backend) {
const path = window.meta_backend + '/' + preparedBalancer;
// Prevent multiple slashes in case meta_backend ends with slash or balancer starts with slash
return path.replaceAll(/([^:])(\/\/+)/g, '$1/');
return normalizePathSlashes(`${window.meta_backend}/${preparedBalancer}`);
}

return preparedBalancer;
Expand Down
Loading