mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Stateful sidenav] Set solution default route (#187763)
This commit is contained in:
parent
08f91b74e7
commit
fd2a94d27f
30 changed files with 449 additions and 154 deletions
|
@ -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}`;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ export type {
|
|||
UiSettingsParams,
|
||||
UserProvidedValues,
|
||||
UiSettingsScope,
|
||||
GetUiSettingsContext,
|
||||
} from './src/ui_settings';
|
||||
export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode';
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/config-schema",
|
||||
"@kbn/analytics"
|
||||
"@kbn/analytics",
|
||||
"@kbn/core-http-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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.',
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -135,7 +135,8 @@ export {
|
|||
theme,
|
||||
cumulativeSum,
|
||||
overallMetric,
|
||||
getUiSettingFn,
|
||||
getUiSettingFnBrowser,
|
||||
getUiSettingFnServer,
|
||||
buildResultColumns,
|
||||
getBucketIdentifier,
|
||||
ExpressionFunction,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
72
src/plugins/navigation/server/ui_settings.test.ts
Normal file
72
src/plugins/navigation/server/ui_settings.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
66
src/plugins/navigation/server/ui_settings.ts
Normal file
66
src/plugins/navigation/server/ui_settings.ts
Normal 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.',
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "お知らせを非表示",
|
||||
|
|
|
@ -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": "隐藏公告",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue