[Stateful sidenav] Set solution default route (#187763)

This commit is contained in:
Sébastien Loix 2024-07-17 17:39:05 +01:00 committed by GitHub
parent 08f91b74e7
commit fd2a94d27f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 449 additions and 154 deletions

View file

@ -113,7 +113,10 @@ export class CoreAppsService {
{ path: '/', validate: false, options: { access: 'public' } },
async (context, req, res) => {
const { uiSettings } = await context.core;
const defaultRoute = await uiSettings.client.get<string>('defaultRoute');
let defaultRoute = await uiSettings.client.get<string>('defaultRoute', { request: req });
if (!defaultRoute) {
defaultRoute = '/app/home';
}
const basePath = httpSetup.basePath.get(req);
const url = `${basePath}${defaultRoute}`;

View file

@ -431,7 +431,7 @@ export interface SolutionNavigationDefinition<LinkId extends AppDeepLinkId = App
icon?: IconType;
/** React component to render in the side nav for the navigation */
sideNavComponent?: SideNavComponent;
/** The page to navigate to when switching to this solution navigation. */
/** The page to navigate to when clicking on the Kibana (or custom) logo. */
homePage?: LinkId;
}

View file

@ -22,6 +22,7 @@ import {
type UserProvidedValues,
type DarkModeValue,
parseDarkModeValue,
type UiSettingsParams,
} from '@kbn/core-ui-settings-common';
import { Template } from './views';
import {
@ -147,8 +148,13 @@ export class RenderingService {
globalSettingsUserValues = userValues[1];
}
const defaultSettings = await withAsyncDefaultValues(
request,
uiSettings.client?.getRegistered()
);
const settings = {
defaults: uiSettings.client?.getRegistered() ?? {},
defaults: defaultSettings,
user: settingsUserValues,
};
const globalSettings = {
@ -300,3 +306,29 @@ const isAuthenticated = (auth: HttpAuth, request: KibanaRequest) => {
// status is 'unknown' when auth is disabled. we just need to not be `unauthenticated` here.
return authStatus !== 'unauthenticated';
};
/**
* Load async values from the definitions that have a `getValue()` function
*
* @param defaultSettings The default settings to add async values to
* @param request The current KibanaRequest
* @returns The default settings with values updated with async values
*/
const withAsyncDefaultValues = async (
request: KibanaRequest,
defaultSettings: Readonly<Record<string, Omit<UiSettingsParams, 'schema'>>> = {}
): Promise<Readonly<Record<string, Omit<UiSettingsParams, 'schema'>>>> => {
const updatedSettings = { ...defaultSettings };
await Promise.all(
Object.entries(defaultSettings)
.filter(([_, definition]) => typeof definition.getValue === 'function')
.map(([key, definition]) => {
return definition.getValue!({ request }).then((value) => {
updatedSettings[key] = { ...definition, value };
});
})
);
return updatedSettings;
};

View file

@ -13,6 +13,7 @@ export type {
UiSettingsParams,
UserProvidedValues,
UiSettingsScope,
GetUiSettingsContext,
} from './src/ui_settings';
export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode';

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { KibanaRequest } from '@kbn/core-http-server';
import type { Type } from '@kbn/config-schema';
import type { UiCounterMetricType } from '@kbn/analytics';
@ -44,6 +45,10 @@ export interface DeprecationSettings {
docLinksKey: string;
}
export interface GetUiSettingsContext {
request?: KibanaRequest;
}
/**
* UiSettings parameters defined by the plugins.
* @public
@ -53,6 +58,8 @@ export interface UiSettingsParams<T = unknown> {
name?: string;
/** default value to fall back to if a user doesn't provide any */
value?: T;
/** handler to return the default value asynchronously. Supersedes the `value` prop */
getValue?: (context?: GetUiSettingsContext) => Promise<T>;
/** description provided to a user in UI */
description?: string;
/** used to group the configured setting in the UI */

View file

@ -12,7 +12,8 @@
],
"kbn_references": [
"@kbn/config-schema",
"@kbn/analytics"
"@kbn/analytics",
"@kbn/core-http-server"
],
"exclude": [
"target/**/*",

View file

@ -8,7 +8,11 @@
import { omit } from 'lodash';
import type { Logger } from '@kbn/logging';
import type { UiSettingsParams, UserProvidedValues } from '@kbn/core-ui-settings-common';
import type {
GetUiSettingsContext,
UiSettingsParams,
UserProvidedValues,
} from '@kbn/core-ui-settings-common';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
import { ValidationBadValueError, ValidationSettingNotFoundError } from '../ui_settings_errors';
@ -23,7 +27,6 @@ export interface BaseUiSettingsDefaultsClientOptions {
*/
export abstract class BaseUiSettingsClient implements IUiSettingsClient {
private readonly defaults: Record<string, UiSettingsParams>;
private readonly defaultValues: Record<string, unknown>;
protected readonly overrides: Record<string, any>;
protected readonly log: Logger;
@ -33,9 +36,6 @@ export abstract class BaseUiSettingsClient implements IUiSettingsClient {
this.overrides = overrides;
this.defaults = defaults;
this.defaultValues = Object.fromEntries(
Object.entries(this.defaults).map(([key, { value }]) => [key, value])
);
}
getRegistered() {
@ -46,13 +46,14 @@ export abstract class BaseUiSettingsClient implements IUiSettingsClient {
return copiedDefaults;
}
async get<T = any>(key: string): Promise<T> {
const all = await this.getAll();
async get<T = any>(key: string, context?: GetUiSettingsContext): Promise<T> {
const all = await this.getAll(context);
return all[key] as T;
}
async getAll<T = any>() {
const result = { ...this.defaultValues };
async getAll<T = any>(context?: GetUiSettingsContext) {
const defaultValues = await this.getDefaultValues(context);
const result = { ...defaultValues };
const userProvided = await this.getUserProvided();
Object.keys(userProvided).forEach((key) => {
@ -99,6 +100,35 @@ export abstract class BaseUiSettingsClient implements IUiSettingsClient {
}
}
private async getDefaultValues(context?: GetUiSettingsContext) {
const values: { [key: string]: unknown } = {};
const promises: Array<[string, Promise<unknown>]> = [];
for (const [key, definition] of Object.entries(this.defaults)) {
if (definition.getValue) {
promises.push([key, definition.getValue(context)]);
} else {
values[key] = definition.value;
}
}
await Promise.all(
promises.map(([key, promise]) =>
promise
.then((value) => {
values[key] = value;
})
.catch((error) => {
this.log.error(`[UiSettingsClient] Failed to get value for key "${key}": ${error}`);
// Fallback to `value` prop if `getValue()` fails
values[key] = this.defaults[key].value;
})
)
);
return values;
}
abstract getUserProvided<T = any>(): Promise<Record<string, UserProvidedValues<T>>>;
abstract setMany(changes: Record<string, any>): Promise<void>;

View file

@ -62,6 +62,43 @@ describe('ui settings defaults', () => {
result.foo = 'bar';
}).toThrow();
});
it('returns default values from async getValue() handler', async () => {
const defaults = {
foo: {
schema: schema.string(),
getValue: async () => {
// simulate async operation
await new Promise((resolve) => setTimeout(resolve, 200));
return 'default foo';
},
},
baz: { schema: schema.string(), value: 'default baz' },
};
const uiSettings = new UiSettingsDefaultsClient({ defaults, log: logger });
await expect(uiSettings.getAll()).resolves.toStrictEqual({
foo: 'default foo',
baz: 'default baz',
});
});
it('pass down the context object to the getValue() handler', async () => {
const getValue = jest.fn().mockResolvedValue('default foo');
const defaults = {
foo: {
schema: schema.string(),
getValue,
},
};
const uiSettings = new UiSettingsDefaultsClient({ defaults, log: logger });
const context = { foo: 'bar' };
await uiSettings.getAll(context as any);
expect(getValue).toHaveBeenCalledWith(context);
});
});
describe('#get()', () => {
@ -94,6 +131,38 @@ describe('ui settings defaults', () => {
await expect(uiSettings.get('foo')).resolves.toBe('default foo');
});
it('returns default values from async getValue() handler', async () => {
const defaults = {
foo: {
schema: schema.string(),
getValue: async () => {
// simulate async operation
await new Promise((resolve) => setTimeout(resolve, 200));
return 'default foo';
},
},
};
const uiSettings = new UiSettingsDefaultsClient({ defaults, log: logger });
await expect(uiSettings.get('foo')).resolves.toBe('default foo');
});
it('pass down the context object to the getValue() handler', async () => {
const getValue = jest.fn().mockResolvedValue('default foo');
const defaults = {
foo: {
schema: schema.string(),
getValue,
},
};
const uiSettings = new UiSettingsDefaultsClient({ defaults, log: logger });
const context = { foo: 'bar' };
await uiSettings.get('foo', context as any);
expect(getValue).toHaveBeenCalledWith(context);
});
});
describe('#setMany()', () => {

View file

@ -9,7 +9,6 @@
import { getAccessibilitySettings } from './accessibility';
import { getDateFormatSettings } from './date_formats';
import { getMiscUiSettings } from './misc';
import { getNavigationSettings } from './navigation';
import { getNotificationsSettings } from './notifications';
import { getThemeSettings } from './theme';
import { getCoreSettings } from '.';
@ -24,7 +23,6 @@ describe('getCoreSettings', () => {
getAnnouncementsSettings(),
getDateFormatSettings(),
getMiscUiSettings(),
getNavigationSettings(),
getNotificationsSettings(),
getThemeSettings(),
getStateSettings(),

View file

@ -10,7 +10,6 @@ import type { UiSettingsParams } from '@kbn/core-ui-settings-common';
import { getAccessibilitySettings } from './accessibility';
import { getDateFormatSettings } from './date_formats';
import { getMiscUiSettings } from './misc';
import { getNavigationSettings } from './navigation';
import { getNotificationsSettings } from './notifications';
import { getThemeSettings } from './theme';
import { getStateSettings } from './state';
@ -28,7 +27,6 @@ export const getCoreSettings = (
...getAnnouncementsSettings(),
...getDateFormatSettings(),
...getMiscUiSettings(),
...getNavigationSettings(),
...getNotificationsSettings(),
...getThemeSettings(options),
...getStateSettings(),

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { UiSettingsParams } from '@kbn/core-ui-settings-common';
import { getNavigationSettings } from './navigation';
describe('navigation settings', () => {
const navigationSettings = getNavigationSettings();
const getValidationFn = (setting: UiSettingsParams) => (value: any) =>
setting.schema.validate(value);
describe('defaultRoute', () => {
const validate = getValidationFn(navigationSettings.defaultRoute);
it('should only accept relative urls', () => {
expect(() => validate('/some-url')).not.toThrow();
expect(() => validate('http://some-url')).toThrowErrorMatchingInlineSnapshot(
`"Must be a relative URL."`
);
expect(() => validate(125)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [string] but got [number]"`
);
});
});
});

View file

@ -1,41 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { isRelativeUrl } from '@kbn/std';
import type { UiSettingsParams } from '@kbn/core-ui-settings-common';
export const getNavigationSettings = (): Record<string, UiSettingsParams> => {
return {
defaultRoute: {
name: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteTitle', {
defaultMessage: 'Default route',
}),
value: '/app/home',
schema: schema.string({
validate(value) {
if (!value.startsWith('/') || !isRelativeUrl(value)) {
return i18n.translate(
'core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage',
{
defaultMessage: 'Must be a relative URL.',
}
);
}
},
}),
description: i18n.translate('core.ui_settings.params.defaultRoute.defaultRouteText', {
defaultMessage:
'This setting specifies the default route when opening Kibana. ' +
'You can use this setting to modify the landing page when opening Kibana. ' +
'The route must be a relative URL.',
}),
},
};
};

View file

@ -218,13 +218,17 @@ export class UiSettingsService
if (!definition.schema) {
throw new Error(`Validation schema is not provided for [${key}] UI Setting`);
}
definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`);
if (definition.value) {
definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`);
}
}
for (const [key, definition] of this.uiSettingsGlobalDefaults) {
if (!definition.schema) {
throw new Error(`Validation schema is not provided for [${key}] Global UI Setting`);
}
definition.schema.validate(definition.value, {});
if (definition.value) {
definition.schema.validate(definition.value, {});
}
}
}

View file

@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
import type { UserProvidedValues, UiSettingsParams } from '@kbn/core-ui-settings-common';
import type {
UserProvidedValues,
UiSettingsParams,
GetUiSettingsContext,
} from '@kbn/core-ui-settings-common';
interface ValueValidation {
valid: boolean;
@ -29,11 +33,11 @@ export interface IUiSettingsClient {
/**
* Retrieves uiSettings values set by the user with fallbacks to default values if not specified.
*/
get: <T = any>(key: string) => Promise<T>;
get: <T = any>(key: string, context?: GetUiSettingsContext) => Promise<T>;
/**
* Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified.
*/
getAll: <T = any>() => Promise<Record<string, T>>;
getAll: <T = any>(context?: GetUiSettingsContext) => Promise<Record<string, T>>;
/**
* Retrieves a set of all uiSettings values set by the user.
*/

View file

@ -9,7 +9,7 @@
jest.mock('../../..');
import { IUiSettingsClient } from '@kbn/core/public';
import { getUiSettingFn } from '../ui_setting';
import { getUiSettingFnBrowser as getUiSettingFn } from '../ui_setting';
import { functionWrapper } from './utils';
describe('uiSetting', () => {

View file

@ -11,16 +11,39 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../..';
import { UiSetting } from '../../expression_types/specs/ui_setting';
interface UiSettingsClient {
/**
* Note: The UiSettings client interface is different between the browser and server.
* For that reason we can't expose a common getUiSettingFn for both environments.
* To maintain the consistency with the current file structure, we expose 2 separate functions
* from this file inside the "common" folder.
*/
interface UiSettingsClientBrowser {
get<T>(key: string, defaultValue?: T): T | Promise<T>;
}
interface UiSettingStartDependencies {
uiSettings: UiSettingsClient;
interface UiSettingStartDependenciesBrowser {
uiSettings: UiSettingsClientBrowser;
}
interface UiSettingFnArguments {
getStartDependencies(getKibanaRequest: () => KibanaRequest): Promise<UiSettingStartDependencies>;
interface UiSettingFnArgumentsBrowser {
getStartDependencies(
getKibanaRequest: () => KibanaRequest
): Promise<UiSettingStartDependenciesBrowser>;
}
interface UiSettingsClientServer {
get<T>(key: string): T | Promise<T>;
}
interface UiSettingStartDependenciesServer {
uiSettings: UiSettingsClientServer;
}
interface UiSettingFnArgumentsServer {
getStartDependencies(
getKibanaRequest: () => KibanaRequest
): Promise<UiSettingStartDependenciesServer>;
}
export interface UiSettingArguments {
@ -35,40 +58,54 @@ export type ExpressionFunctionUiSetting = ExpressionFunctionDefinition<
Promise<UiSetting>
>;
export function getUiSettingFn({
getStartDependencies,
}: UiSettingFnArguments): ExpressionFunctionUiSetting {
return {
name: 'uiSetting',
help: i18n.translate('expressions.functions.uiSetting.help', {
defaultMessage: 'Returns a UI settings parameter value.',
}),
args: {
default: {
help: i18n.translate('expressions.functions.uiSetting.args.default', {
defaultMessage: 'A default value in case of the parameter is not set.',
}),
},
parameter: {
aliases: ['_'],
help: i18n.translate('expressions.functions.uiSetting.args.parameter', {
defaultMessage: 'The parameter name.',
}),
required: true,
types: ['string'],
},
const commonExpressionFnSettings: Omit<ExpressionFunctionUiSetting, 'fn'> = {
name: 'uiSetting',
help: i18n.translate('expressions.functions.uiSetting.help', {
defaultMessage: 'Returns a UI settings parameter value.',
}),
args: {
default: {
help: i18n.translate('expressions.functions.uiSetting.args.default', {
defaultMessage: 'A default value in case of the parameter is not set.',
}),
},
parameter: {
aliases: ['_'],
help: i18n.translate('expressions.functions.uiSetting.args.parameter', {
defaultMessage: 'The parameter name.',
}),
required: true,
types: ['string'],
},
},
};
const kibanaRequestError = new Error(
i18n.translate('expressions.functions.uiSetting.error.kibanaRequest', {
defaultMessage:
'A KibanaRequest is required to get UI settings on the server. ' +
'Please provide a request object to the expression execution params.',
})
);
const getInvalidParameterError = (parameter: string) =>
new Error(
i18n.translate('expressions.functions.uiSetting.error.parameter', {
defaultMessage: 'Invalid parameter "{parameter}".',
values: { parameter },
})
);
export function getUiSettingFnBrowser({
getStartDependencies,
}: UiSettingFnArgumentsBrowser): ExpressionFunctionUiSetting {
return {
...commonExpressionFnSettings,
async fn(input, { default: defaultValue, parameter }, { getKibanaRequest }) {
const { uiSettings } = await getStartDependencies(() => {
const request = getKibanaRequest?.();
if (!request) {
throw new Error(
i18n.translate('expressions.functions.uiSetting.error.kibanaRequest', {
defaultMessage:
'A KibanaRequest is required to get UI settings on the server. ' +
'Please provide a request object to the expression execution params.',
})
);
throw kibanaRequestError;
}
return request;
@ -81,12 +118,35 @@ export function getUiSettingFn({
value: await uiSettings.get(parameter, defaultValue),
};
} catch {
throw new Error(
i18n.translate('expressions.functions.uiSetting.error.parameter', {
defaultMessage: 'Invalid parameter "{parameter}".',
values: { parameter },
})
);
throw getInvalidParameterError(parameter);
}
},
};
}
export function getUiSettingFnServer({
getStartDependencies,
}: UiSettingFnArgumentsServer): ExpressionFunctionUiSetting {
return {
...commonExpressionFnSettings,
async fn(input, { parameter }, { getKibanaRequest }) {
const { uiSettings } = await getStartDependencies(() => {
const request = getKibanaRequest?.();
if (!request) {
throw kibanaRequestError;
}
return request;
});
try {
return {
type: 'ui_setting',
key: parameter,
value: await uiSettings.get(parameter),
};
} catch {
throw getInvalidParameterError(parameter);
}
},
};

View file

@ -135,7 +135,8 @@ export {
theme,
cumulativeSum,
overallMetric,
getUiSettingFn,
getUiSettingFnBrowser,
getUiSettingFnServer,
buildResultColumns,
getBucketIdentifier,
ExpressionFunction,

View file

@ -7,10 +7,10 @@
*/
import { CoreSetup } from '@kbn/core/public';
import { getUiSettingFn as getCommonUiSettingFn } from '../../common';
import { getUiSettingFnBrowser } from '../../common';
export function getUiSettingFn({ getStartServices }: Pick<CoreSetup, 'getStartServices'>) {
return getCommonUiSettingFn({
return getUiSettingFnBrowser({
async getStartDependencies() {
const [{ uiSettings }] = await getStartServices();

View file

@ -7,10 +7,10 @@
*/
import { CoreSetup } from '@kbn/core/server';
import { getUiSettingFn as getCommonUiSettingFn } from '../../common';
import { getUiSettingFnServer } from '../../common';
export function getUiSettingFn({ getStartServices }: Pick<CoreSetup, 'getStartServices'>) {
return getCommonUiSettingFn({
return getUiSettingFnServer({
async getStartDependencies(getKibanaRequest) {
const [{ savedObjects, uiSettings }] = await getStartServices();
const savedObjectsClient = savedObjects.getScopedClient(getKibanaRequest());

View file

@ -7,3 +7,12 @@
*/
export const SOLUTION_NAV_FEATURE_FLAG_NAME = 'solutionNavEnabled';
export const DEFAULT_ROUTE_UI_SETTING_ID = 'defaultRoute';
export const DEFAULT_ROUTES = {
classic: '/app/home',
es: '/app/enterprise_search/overview',
oblt: '/app/observabilityOnboarding',
security: '/app/security/get_started',
};

View file

@ -5,9 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PluginInitializerContext } from '@kbn/core/server';
import { NavigationServerPlugin } from './plugin';
export async function plugin() {
return new NavigationServerPlugin();
export async function plugin(initContext: PluginInitializerContext) {
return new NavigationServerPlugin(initContext);
}

View file

@ -5,7 +5,13 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import type {
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
Logger,
} from '@kbn/core/server';
import type {
NavigationServerSetup,
@ -13,6 +19,7 @@ import type {
NavigationServerStart,
NavigationServerStartDependencies,
} from './types';
import { getUiSettings } from './ui_settings';
export class NavigationServerPlugin
implements
@ -23,12 +30,18 @@ export class NavigationServerPlugin
NavigationServerStartDependencies
>
{
constructor() {}
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
setup(
core: CoreSetup<NavigationServerStartDependencies>,
plugins: NavigationServerSetupDependencies
) {
core.uiSettings.register(getUiSettings(core, this.logger));
return {};
}

View file

@ -7,6 +7,7 @@
*/
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface NavigationServerSetup {}
@ -16,9 +17,11 @@ export interface NavigationServerStart {}
export interface NavigationServerSetupDependencies {
cloud?: CloudSetup;
spaces?: SpacesPluginSetup;
}
export interface NavigationServerStartDependencies {
cloudExperiments?: CloudExperimentsPluginStart;
cloud?: CloudStart;
spaces?: SpacesPluginStart;
}

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks';
import type { UiSettingsParams } from '@kbn/core-ui-settings-common';
import { spacesMock } from '@kbn/spaces-plugin/server/mocks';
import type { Space } from '@kbn/spaces-plugin/common';
import { getUiSettings } from './ui_settings';
import { DEFAULT_ROUTES } from '../common/constants';
describe('ui settings', () => {
const core = coreMock.createSetup();
const logger = loggingSystemMock.createLogger();
const getValidationFn = (setting: UiSettingsParams) => (value: any) =>
setting.schema.validate(value);
describe('defaultRoute', () => {
it('should only accept relative urls', () => {
const uiSettings = getUiSettings(core, logger);
const validate = getValidationFn(uiSettings.defaultRoute);
expect(() => validate('/some-url')).not.toThrow();
expect(() => validate('http://some-url')).toThrowErrorMatchingInlineSnapshot(
`"Must be a relative URL."`
);
expect(() => validate(125)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [string] but got [number]"`
);
});
describe('getValue()', () => {
it('should return classic when neither "space" nor "request" is provided', async () => {
const { defaultRoute } = getUiSettings(core, logger);
await expect(defaultRoute.getValue!()).resolves.toBe(DEFAULT_ROUTES.classic);
});
it('should return the route based on the active space', async () => {
const spaces = spacesMock.createStart();
for (const solution of ['classic', 'es', 'oblt', 'security'] as const) {
const mockSpace: Pick<Space, 'solution'> = { solution };
spaces.spacesService.getActiveSpace.mockResolvedValue(mockSpace as Space);
core.getStartServices.mockResolvedValue([{} as any, { spaces }, {} as any]);
const { defaultRoute } = getUiSettings(core, logger);
await expect(defaultRoute.getValue!({ request: {} as any })).resolves.toBe(
DEFAULT_ROUTES[solution]
);
}
});
it('should handle error thrown', async () => {
const spaces = spacesMock.createStart();
spaces.spacesService.getActiveSpace.mockRejectedValue(new Error('something went wrong'));
core.getStartServices.mockResolvedValue([{} as any, { spaces }, {} as any]);
const { defaultRoute } = getUiSettings(core, logger);
await expect(defaultRoute.getValue!({ request: {} as any })).resolves.toBe(
DEFAULT_ROUTES.classic
);
});
});
});
});

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { CoreSetup, KibanaRequest, Logger } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { UiSettingsParams } from '@kbn/core/types';
import { i18n } from '@kbn/i18n';
import { isRelativeUrl } from '@kbn/std';
import { DEFAULT_ROUTE_UI_SETTING_ID, DEFAULT_ROUTES } from '../common/constants';
import { NavigationServerStartDependencies } from './types';
/**
* uiSettings definitions for Navigation
*/
export const getUiSettings = (
core: CoreSetup<NavigationServerStartDependencies>,
logger: Logger
): Record<string, UiSettingsParams> => {
return {
[DEFAULT_ROUTE_UI_SETTING_ID]: {
name: i18n.translate('navigation.ui_settings.params.defaultRoute.defaultRouteTitle', {
defaultMessage: 'Default route',
}),
getValue: async ({ request }: { request?: KibanaRequest } = {}) => {
const [_, { spaces }] = await core.getStartServices();
if (!spaces || !request) {
return DEFAULT_ROUTES.classic;
}
try {
const activeSpace = await spaces.spacesService.getActiveSpace(request);
const solution = activeSpace?.solution ?? 'classic';
return DEFAULT_ROUTES[solution] ?? DEFAULT_ROUTES.classic;
} catch (e) {
logger.error(`Failed to retrieve active space: ${e.message}`);
return DEFAULT_ROUTES.classic;
}
},
schema: schema.string({
validate(value) {
if (!value.startsWith('/') || !isRelativeUrl(value)) {
return i18n.translate(
'navigation.uiSettings.defaultRoute.defaultRouteIsRelativeValidationMessage',
{
defaultMessage: 'Must be a relative URL.',
}
);
}
},
}),
description: i18n.translate('navigation.uiSettings.defaultRoute.defaultRouteText', {
defaultMessage:
'This setting specifies the default route when opening Kibana. ' +
'You can use this setting to modify the landing page when opening Kibana. ' +
'The route must be a relative URL.',
}),
},
};
};

View file

@ -24,6 +24,10 @@
"@kbn/config",
"@kbn/cloud-experiments-plugin",
"@kbn/spaces-plugin",
"@kbn/core-ui-settings-common",
"@kbn/config-schema",
"@kbn/i18n",
"@kbn/std",
],
"exclude": [
"target/**/*",

View file

@ -36,7 +36,7 @@ export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
async (context, request, response) => {
try {
const { uiSettings } = await context.core;
const defaultRoute = await uiSettings.client.get<string>('defaultRoute');
const defaultRoute = await uiSettings.client.get<string>('defaultRoute', { request });
const basePath = deps.basePath.get(request);
const nextCandidateRoute = parseNextURL(request.url.href);

View file

@ -1016,9 +1016,6 @@
"core.ui_settings.params.dateNanosFormatTitle": "Date au format nanosecondes",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "Jour de la semaine non valide : {dayOfWeek}",
"core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "Doit être une URL relative.",
"core.ui_settings.params.defaultRoute.defaultRouteText": "Ce paramètre spécifie le chemin par défaut lors de l'ouverture de Kibana. Vous pouvez utiliser ce paramètre pour modifier la page de destination à l'ouverture de Kibana. Le chemin doit être une URL relative.",
"core.ui_settings.params.defaultRoute.defaultRouteTitle": "Chemin par défaut",
"core.ui_settings.params.disableAnimationsText": "Désactivez toutes les animations non nécessaires dans l'interface utilisateur de Kibana. Actualisez la page pour appliquer les modifications.",
"core.ui_settings.params.disableAnimationsTitle": "Désactiver les animations",
"core.ui_settings.params.hideAnnouncements": "Masquer les annonces",

View file

@ -1016,9 +1016,6 @@
"core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "無効な曜日:{dayOfWeek}",
"core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対URLでなければなりません。",
"core.ui_settings.params.defaultRoute.defaultRouteText": "この設定は、Kibana起動時のデフォルトのルートを設定します。この設定で、Kibana起動時のランディングページを変更できます。ルートは相対URLでなければなりません。",
"core.ui_settings.params.defaultRoute.defaultRouteTitle": "デフォルトのルート",
"core.ui_settings.params.disableAnimationsText": "Kibana UIの不要なアニメーションをオフにします。変更を適用するにはページを更新してください。",
"core.ui_settings.params.disableAnimationsTitle": "アニメーションを無効にする",
"core.ui_settings.params.hideAnnouncements": "お知らせを非表示",

View file

@ -1018,9 +1018,6 @@
"core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期",
"core.ui_settings.params.dateNanosLinkTitle": "date_nanos",
"core.ui_settings.params.dayOfWeekText.invalidValidationMessage": "周内日无效:{dayOfWeek}",
"core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。",
"core.ui_settings.params.defaultRoute.defaultRouteText": "此设置用于指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。",
"core.ui_settings.params.defaultRoute.defaultRouteTitle": "默认路由",
"core.ui_settings.params.disableAnimationsText": "在 Kibana UI 中关闭所有不必要的动画。刷新页面可应用所做的更改。",
"core.ui_settings.params.disableAnimationsTitle": "禁用动画",
"core.ui_settings.params.hideAnnouncements": "隐藏公告",