From 80bfebf8504024efb99056f703b45efd18027de9 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 11 Feb 2021 12:10:35 +0100 Subject: [PATCH] Implement custom global header banner (#87438) (#91089) * first draft * update plugin list * fix tsproject * update bundle limits file * remove unused start dep * adapt imports * POC of footer banner * update styles, mostly * plug banner to uiSettings * adding some unit tests * add tests on sort_fields * cleanup sums in sass mixins * some self review stuff * update generated doc * add tests for color field * update chrome header test snapshots * retrieve license info from the server * switch from uiSettings to plugin config * update plugin list description * update default colors * NIT * add markdown support * fix banner overlap in fullscreen mode * change banner height to 32px * change banner's font size to 14 * delete unused uiSettings --- .stylelintrc | 1 + docs/developer/plugin-list.asciidoc | 4 + .../kibana-plugin-core-public.chromestart.md | 1 + ...core-public.chromestart.setheaderbanner.md | 28 ++ ...in-core-public.chromeuserbanner.content.md | 11 + ...ana-plugin-core-public.chromeuserbanner.md | 19 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../core/public/kibana-plugin-core-public.md | 1 + ...ana-plugin-core-public.uisettingsparams.md | 1 + ...ugin-core-public.uisettingsparams.order.md | 15 + ...ibana-plugin-core-public.uisettingstype.md | 2 +- ...ana-plugin-core-server.uisettingsparams.md | 1 + ...ugin-core-server.uisettingsparams.order.md | 15 + ...ibana-plugin-core-server.uisettingstype.md | 2 +- packages/kbn-optimizer/limits.yml | 1 + src/core/public/_mixins.scss | 43 ++ src/core/public/_variables.scss | 5 + src/core/public/chrome/chrome_service.mock.ts | 3 + src/core/public/chrome/chrome_service.tsx | 242 ++-------- src/core/public/chrome/index.ts | 20 +- src/core/public/chrome/types.ts | 241 ++++++++++ .../header/__snapshots__/header.test.tsx.snap | 107 ++++- src/core/public/chrome/ui/header/_banner.scss | 22 + src/core/public/chrome/ui/header/_index.scss | 2 +- .../public/chrome/ui/header/header.test.tsx | 4 +- src/core/public/chrome/ui/header/header.tsx | 21 +- .../public/chrome/ui/header/header_badge.tsx | 2 +- .../chrome/ui/header/header_breadcrumbs.tsx | 2 +- .../chrome/ui/header/header_help_menu.tsx | 2 +- .../chrome/ui/header/header_top_banner.tsx | 34 ++ src/core/public/core_app/styles/_mixins.scss | 8 + src/core/public/core_system.ts | 1 - src/core/public/index.scss | 1 + src/core/public/index.ts | 2 + src/core/public/public.api.md | 10 +- src/core/public/rendering/_base.scss | 32 +- .../public/rendering/rendering_service.tsx | 23 +- src/core/server/server.api.md | 3 +- src/core/types/ui_settings.ts | 10 +- .../management_app/advanced_settings.tsx | 20 +- .../field/__snapshots__/field.test.tsx.snap | 413 ++++++++++++++++++ .../components/field/field.test.tsx | 46 ++ .../management_app/components/field/field.tsx | 12 + .../public/management_app/lib/index.ts | 1 + .../management_app/lib/sort_fields.test.ts | 56 +++ .../public/management_app/lib/sort_fields.ts | 31 ++ .../management_app/lib/to_editable_config.ts | 6 +- .../public/management_app/types.ts | 1 + .../application/components/discover.scss | 4 +- .../public/application/components/_home.scss | 4 +- .../public/application/components/home.js | 3 + .../public/components/_overview.scss | 4 +- x-pack/.i18nrc.json | 3 +- x-pack/plugins/banners/README.md | 38 ++ x-pack/plugins/banners/common/index.ts | 8 + x-pack/plugins/banners/common/types.ts | 20 + x-pack/plugins/banners/jest.config.js | 12 + x-pack/plugins/banners/kibana.json | 11 + .../banners/public/components/banner.scss | 7 + .../banners/public/components/banner.tsx | 33 ++ .../banners/public/components/index.ts | 8 + .../banners/public/get_banner_info.test.ts | 35 ++ .../plugins/banners/public/get_banner_info.ts | 13 + x-pack/plugins/banners/public/index.ts | 12 + .../banners/public/plugin.test.mocks.ts | 11 + x-pack/plugins/banners/public/plugin.test.tsx | 86 ++++ x-pack/plugins/banners/public/plugin.tsx | 44 ++ x-pack/plugins/banners/public/types.ts | 12 + x-pack/plugins/banners/server/config.ts | 42 ++ x-pack/plugins/banners/server/index.ts | 12 + x-pack/plugins/banners/server/plugin.ts | 33 ++ x-pack/plugins/banners/server/routes/index.ts | 14 + x-pack/plugins/banners/server/routes/info.ts | 36 ++ x-pack/plugins/banners/server/types.ts | 15 + x-pack/plugins/banners/server/utils.test.ts | 26 ++ x-pack/plugins/banners/server/utils.ts | 12 + x-pack/plugins/banners/tsconfig.json | 22 + x-pack/plugins/maps/public/_main.scss | 10 +- .../painless_lab/public/styles/_index.scss | 7 +- .../public/application/_app.scss | 8 +- x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 1 + 82 files changed, 1855 insertions(+), 282 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.content.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.uisettingsparams.order.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.uisettingsparams.order.md create mode 100644 src/core/public/_mixins.scss create mode 100644 src/core/public/chrome/types.ts create mode 100644 src/core/public/chrome/ui/header/_banner.scss create mode 100644 src/core/public/chrome/ui/header/header_top_banner.tsx create mode 100644 src/plugins/advanced_settings/public/management_app/lib/sort_fields.test.ts create mode 100644 src/plugins/advanced_settings/public/management_app/lib/sort_fields.ts create mode 100644 x-pack/plugins/banners/README.md create mode 100644 x-pack/plugins/banners/common/index.ts create mode 100644 x-pack/plugins/banners/common/types.ts create mode 100644 x-pack/plugins/banners/jest.config.js create mode 100644 x-pack/plugins/banners/kibana.json create mode 100644 x-pack/plugins/banners/public/components/banner.scss create mode 100644 x-pack/plugins/banners/public/components/banner.tsx create mode 100644 x-pack/plugins/banners/public/components/index.ts create mode 100644 x-pack/plugins/banners/public/get_banner_info.test.ts create mode 100644 x-pack/plugins/banners/public/get_banner_info.ts create mode 100644 x-pack/plugins/banners/public/index.ts create mode 100644 x-pack/plugins/banners/public/plugin.test.mocks.ts create mode 100644 x-pack/plugins/banners/public/plugin.test.tsx create mode 100644 x-pack/plugins/banners/public/plugin.tsx create mode 100644 x-pack/plugins/banners/public/types.ts create mode 100644 x-pack/plugins/banners/server/config.ts create mode 100644 x-pack/plugins/banners/server/index.ts create mode 100644 x-pack/plugins/banners/server/plugin.ts create mode 100644 x-pack/plugins/banners/server/routes/index.ts create mode 100644 x-pack/plugins/banners/server/routes/info.ts create mode 100644 x-pack/plugins/banners/server/types.ts create mode 100644 x-pack/plugins/banners/server/utils.test.ts create mode 100644 x-pack/plugins/banners/server/utils.ts create mode 100644 x-pack/plugins/banners/tsconfig.json diff --git a/.stylelintrc b/.stylelintrc index 29c1f4b552b48..26431cfee6f1d 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -32,6 +32,7 @@ rules: - function - return - for + - at-root comment-no-empty: true no-duplicate-at-import-rules: true no-duplicate-selectors: true diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 90d9c42c8aef3..fc565491b4f63 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -297,6 +297,10 @@ which will load the visualization's editor. |To access an elasticsearch instance that has live data you have two options: +|{kib-repo}blob/{branch}/x-pack/plugins/banners/README.md[banners] +|Allow to add a header banner that will be displayed on every page of the Kibana application + + |{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] |Notes: Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 663b326193de5..2d465745c436b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -67,6 +67,7 @@ core.chrome.setHelpExtension(elem => { | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setBreadcrumbsAppendExtension(breadcrumbsAppendExtension)](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) | Mount an element next to the last breadcrumb | | [setCustomNavLink(newCustomNavLink)](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) | Override the current set of custom nav link | +| [setHeaderBanner(headerBanner)](./kibana-plugin-core-public.chromestart.setheaderbanner.md) | Set the banner that will appear on top of the chrome header. | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md new file mode 100644 index 0000000000000..02a2fa65ed478 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setheaderbanner.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setHeaderBanner](./kibana-plugin-core-public.chromestart.setheaderbanner.md) + +## ChromeStart.setHeaderBanner() method + +Set the banner that will appear on top of the chrome header. + +Signature: + +```typescript +setHeaderBanner(headerBanner?: ChromeUserBanner): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| headerBanner | ChromeUserBanner | | + +Returns: + +`void` + +## Remarks + +Using `undefined` when invoking this API will remove the banner. + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.content.md b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.content.md new file mode 100644 index 0000000000000..7a77fdc6223de --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.content.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeUserBanner](./kibana-plugin-core-public.chromeuserbanner.md) > [content](./kibana-plugin-core-public.chromeuserbanner.content.md) + +## ChromeUserBanner.content property + +Signature: + +```typescript +content: MountPoint; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md new file mode 100644 index 0000000000000..8617c5c4d2b12 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromeuserbanner.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeUserBanner](./kibana-plugin-core-public.chromeuserbanner.md) + +## ChromeUserBanner interface + + +Signature: + +```typescript +export interface ChromeUserBanner +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [content](./kibana-plugin-core-public.chromeuserbanner.content.md) | MountPoint<HTMLDivElement> | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index f4bce8b51ebb1..5be8f8ce7e8c7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index e307b5c9971b0..5524cf328fbfe 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -56,6 +56,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeRecentlyAccessed](./kibana-plugin-core-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-core-public.chromerecentlyaccessed.md) for recently accessed history. | | [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md) | | | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. | +| [ChromeUserBanner](./kibana-plugin-core-public.chromeuserbanner.md) | | | [CoreSetup](./kibana-plugin-core-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | | [CoreStart](./kibana-plugin-core-public.corestart.md) | Core services exposed to the Plugin start lifecycle | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index 0b7e6467667cb..6fcfae559dd33 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -23,6 +23,7 @@ export interface UiSettingsParams | [name](./kibana-plugin-core-public.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | +| [order](./kibana-plugin-core-public.uisettingsparams.order.md) | number | index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | | [readonly](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | | [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.order.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.order.md new file mode 100644 index 0000000000000..d93aaeb904616 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.order.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [order](./kibana-plugin-core-public.uisettingsparams.order.md) + +## UiSettingsParams.order property + +index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. + + settings without order defined will be displayed last and ordered by name + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingstype.md b/docs/development/core/public/kibana-plugin-core-public.uisettingstype.md index 5753704ccfe03..65e6264ea1e08 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingstype.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingstype.md @@ -9,5 +9,5 @@ UI element type to represent the settings. Signature: ```typescript -export declare type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; +export declare type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image' | 'color'; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index d35afc4a149d1..4bb7be77c595a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -23,6 +23,7 @@ export interface UiSettingsParams | [name](./kibana-plugin-core-server.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | +| [order](./kibana-plugin-core-server.uisettingsparams.order.md) | number | index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. settings without order defined will be displayed last and ordered by name | | [readonly](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | | [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.order.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.order.md new file mode 100644 index 0000000000000..140bdad5d786b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.order.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [order](./kibana-plugin-core-server.uisettingsparams.order.md) + +## UiSettingsParams.order property + +index of the settings within its category (ascending order, smallest will be displayed first). Used for ordering in the UI. + + settings without order defined will be displayed last and ordered by name + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingstype.md b/docs/development/core/server/kibana-plugin-core-server.uisettingstype.md index 3c439897ea031..7edee442baa24 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingstype.md @@ -9,5 +9,5 @@ UI element type to represent the settings. Signature: ```typescript -export declare type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; +export declare type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image' | 'color'; ``` diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a364aa4c8de29..5c399a052485f 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -105,3 +105,4 @@ pageLoadAssetSize: spacesOss: 18817 osquery: 107090 fileUpload: 25664 + banners: 17946 diff --git a/src/core/public/_mixins.scss b/src/core/public/_mixins.scss new file mode 100644 index 0000000000000..2dbef465e074e --- /dev/null +++ b/src/core/public/_mixins.scss @@ -0,0 +1,43 @@ +@import './variables'; + +/* stylelint-disable-next-line length-zero-no-unit -- need consistent unit to sum them */ +@mixin kibanaFullBodyHeight($additionalOffset: 0px) { + // default - header, no banner + height: calc(100vh - #{$kbnHeaderOffset + $additionalOffset}); + + @at-root { + // no header, no banner + .kbnBody--chromeHidden & { + height: calc(100vh - #{$additionalOffset}); + } + // header, banner + .kbnBody--hasHeaderBanner & { + height: calc(100vh - #{$kbnHeaderOffsetWithBanner + $additionalOffset}); + } + // no header, banner + .kbnBody--chromeHidden.kbnBody--hasHeaderBanner & { + height: calc(100vh - #{$kbnHeaderBannerHeight + $additionalOffset}); + } + } +} + +/* stylelint-disable-next-line length-zero-no-unit -- need consistent unit to sum them */ +@mixin kibanaFullBodyMinHeight($additionalOffset: 0px) { + // default - header, no banner + min-height: calc(100vh - #{$kbnHeaderOffset + $additionalOffset}); + + @at-root { + // no header, no banner + .kbnBody--chromeHidden & { + min-height: calc(100vh - #{$additionalOffset}); + } + // header, banner + .kbnBody--hasHeaderBanner & { + min-height: calc(100vh - #{$kbnHeaderOffsetWithBanner + $additionalOffset}); + } + // no header, banner + .kbnBody--chromeHidden.kbnBody--hasHeaderBanner & { + min-height: calc(100vh - #{$kbnHeaderBannerHeight + $additionalOffset}); + } + } +} diff --git a/src/core/public/_variables.scss b/src/core/public/_variables.scss index 8c054e770bd4b..f6ff5619bfc53 100644 --- a/src/core/public/_variables.scss +++ b/src/core/public/_variables.scss @@ -1,3 +1,8 @@ @import '@elastic/eui/src/global_styling/variables/header'; +// height of the header banner +$kbnHeaderBannerHeight: $euiSizeXL; +// total height of the header (when the banner is *not* present) $kbnHeaderOffset: $euiHeaderHeightCompensation * 2; +// total height of the header when the banner is present +$kbnHeaderOffsetWithBanner: $kbnHeaderOffset + $kbnHeaderBannerHeight; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index cb0876f6bc725..ae9c58af69603 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -61,6 +61,8 @@ const createStartContractMock = () => { getIsNavDrawerLocked$: jest.fn(), getCustomNavLink$: jest.fn(), setCustomNavLink: jest.fn(), + setHeaderBanner: jest.fn(), + getBodyClasses$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); @@ -72,6 +74,7 @@ const createStartContractMock = () => { startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); + startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([])); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index ee8d1c17ccd59..e69bf9025fdb9 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -6,69 +6,37 @@ * Side Public License, v 1. */ -import { EuiBreadcrumb, IconType } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; import { EuiLink } from '@elastic/eui'; -import { MountPoint } from '../types'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; import { HttpStart } from '../http'; import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; -import { IUiSettingsClient } from '../ui_settings'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; -import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; +import { NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; -import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; +import { + ChromeBadge, + ChromeBrand, + ChromeBreadcrumb, + ChromeBreadcrumbsAppendExtension, + ChromeHelpExtension, + InternalChromeStart, + ChromeUserBanner, +} from './types'; const IS_LOCKED_KEY = 'core.chrome.isLocked'; -/** @public */ -export interface ChromeBadge { - text: string; - tooltip: string; - iconType?: IconType; -} - -/** @public */ -export interface ChromeBrand { - logo?: string; - smallLogo?: string; -} - -/** @public */ -export type ChromeBreadcrumb = EuiBreadcrumb; - -/** @public */ -export interface ChromeBreadcrumbsAppendExtension { - content: MountPoint; -} - -/** @public */ -export interface ChromeHelpExtension { - /** - * Provide your plugin's name to create a header for separation - */ - appName: string; - /** - * Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button - */ - links?: ChromeHelpExtensionMenuLink[]; - /** - * Custom content to occur below the list of links - */ - content?: (element: HTMLDivElement) => () => void; -} - interface ConstructorParams { browserSupportsCsp: boolean; } @@ -79,7 +47,6 @@ interface StartDeps { http: HttpStart; injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; - uiSettings: IUiSettingsClient; } /** @internal */ @@ -132,7 +99,6 @@ export class ChromeService { http, injectedMetadata, notifications, - uiSettings, }: StartDeps): Promise { this.initVisibility(application); @@ -149,6 +115,17 @@ export class ChromeService { const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const headerBanner$ = new BehaviorSubject(undefined); + const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe( + map(([headerBanner, isVisible]) => { + return [ + 'kbnBody', + headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner', + isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden', + ]; + }) + ); + const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); @@ -220,6 +197,7 @@ export class ChromeService { loadingCount$={http.getLoadingCount$()} application={application} appTitle$={appTitle$.pipe(takeUntil(this.stop$))} + headerBanner$={headerBanner$.pipe(takeUntil(this.stop$))} badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} @@ -312,6 +290,12 @@ export class ChromeService { setCustomNavLink: (customNavLink?: ChromeNavLink) => { customNavLink$.next(customNavLink); }, + + setHeaderBanner: (headerBanner?: ChromeUserBanner) => { + headerBanner$.next(headerBanner); + }, + + getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)), }; } @@ -320,173 +304,3 @@ export class ChromeService { this.stop$.next(); } } - -/** - * ChromeStart allows plugins to customize the global chrome header UI and - * enrich the UX with additional information about the current location of the - * browser. - * - * @remarks - * While ChromeStart exposes many APIs, they should be used sparingly and the - * developer should understand how they affect other plugins and applications. - * - * @example - * How to add a recently accessed item to the sidebar: - * ```ts - * core.chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); - * ``` - * - * @example - * How to set the help dropdown extension: - * ```tsx - * core.chrome.setHelpExtension(elem => { - * ReactDOM.render(, elem); - * return () => ReactDOM.unmountComponentAtNode(elem); - * }); - * ``` - * - * @public - */ -export interface ChromeStart { - /** {@inheritdoc ChromeNavLinks} */ - navLinks: ChromeNavLinks; - /** {@inheritdoc ChromeNavControls} */ - navControls: ChromeNavControls; - /** {@inheritdoc ChromeRecentlyAccessed} */ - recentlyAccessed: ChromeRecentlyAccessed; - /** {@inheritdoc ChromeDocTitle} */ - docTitle: ChromeDocTitle; - - /** - * Sets the current app's title - * - * @internalRemarks - * This should be handled by the application service once it is in charge - * of mounting applications. - */ - setAppTitle(appTitle: string): void; - - /** - * Get an observable of the current brand information. - */ - getBrand$(): Observable; - - /** - * Set the brand configuration. - * - * @remarks - * Normally the `logo` property will be rendered as the - * CSS background for the home link in the chrome navigation, but when the page is - * rendered in a small window the `smallLogo` will be used and rendered at about - * 45px wide. - * - * @example - * ```js - * chrome.setBrand({ - * logo: 'url(/plugins/app/logo.png) center no-repeat' - * smallLogo: 'url(/plugins/app/logo-small.png) center no-repeat' - * }) - * ``` - * - */ - setBrand(brand: ChromeBrand): void; - - /** - * Get an observable of the current visibility state of the chrome. - */ - getIsVisible$(): Observable; - - /** - * Set the temporary visibility for the chrome. This does nothing if the chrome is hidden - * by default and should be used to hide the chrome for things like full-screen modes - * with an exit button. - */ - setIsVisible(isVisible: boolean): void; - - /** - * Get the current set of classNames that will be set on the application container. - */ - getApplicationClasses$(): Observable; - - /** - * Add a className that should be set on the application container. - */ - addApplicationClass(className: string): void; - - /** - * Remove a className added with `addApplicationClass()`. If className is unknown it is ignored. - */ - removeApplicationClass(className: string): void; - - /** - * Get an observable of the current badge - */ - getBadge$(): Observable; - - /** - * Override the current badge - */ - setBadge(badge?: ChromeBadge): void; - - /** - * Get an observable of the current list of breadcrumbs - */ - getBreadcrumbs$(): Observable; - - /** - * Override the current set of breadcrumbs - */ - setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; - - /** - * Get an observable of the current extension appended to breadcrumbs - */ - getBreadcrumbsAppendExtension$(): Observable; - - /** - * Mount an element next to the last breadcrumb - */ - setBreadcrumbsAppendExtension( - breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension - ): void; - - /** - * Get an observable of the current custom nav link - */ - getCustomNavLink$(): Observable | undefined>; - - /** - * Override the current set of custom nav link - */ - setCustomNavLink(newCustomNavLink?: Partial): void; - - /** - * Get an observable of the current custom help conttent - */ - getHelpExtension$(): Observable; - - /** - * Override the current set of custom help content - */ - setHelpExtension(helpExtension?: ChromeHelpExtension): void; - - /** - * Override the default support URL shown in the help menu - * @param url The updated support URL - */ - setHelpSupportUrl(url: string): void; - - /** - * Get an observable of the current locked state of the nav drawer. - */ - getIsNavDrawerLocked$(): Observable; -} - -/** @internal */ -export interface InternalChromeStart extends ChromeStart { - /** - * Used only by MountingService to render the header UI - * @internal - */ - getHeaderComponent(): JSX.Element; -} diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index cf4106a42e0d4..069d29ca70d01 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -6,15 +6,7 @@ * Side Public License, v 1. */ -export { - ChromeBadge, - ChromeBreadcrumb, - ChromeService, - ChromeStart, - InternalChromeStart, - ChromeBrand, - ChromeHelpExtension, -} from './chrome_service'; +export { ChromeService } from './chrome_service'; export { ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuLink, @@ -28,3 +20,13 @@ export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './ export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; export { ChromeNavControl, ChromeNavControls } from './nav_controls'; export { ChromeDocTitle } from './doc_title'; +export { + InternalChromeStart, + ChromeStart, + ChromeHelpExtension, + ChromeBreadcrumbsAppendExtension, + ChromeBreadcrumb, + ChromeBrand, + ChromeBadge, + ChromeUserBanner, +} from './types'; diff --git a/src/core/public/chrome/types.ts b/src/core/public/chrome/types.ts new file mode 100644 index 0000000000000..732236f1ba4a1 --- /dev/null +++ b/src/core/public/chrome/types.ts @@ -0,0 +1,241 @@ +/* + * 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 { EuiBreadcrumb, IconType } from '@elastic/eui'; +import { Observable } from 'rxjs'; +import { MountPoint } from '../types'; +import { ChromeDocTitle } from './doc_title'; +import { ChromeNavControls } from './nav_controls'; +import { ChromeNavLinks, ChromeNavLink } from './nav_links'; +import { ChromeRecentlyAccessed } from './recently_accessed'; +import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; + +/** @public */ +export interface ChromeBadge { + text: string; + tooltip: string; + iconType?: IconType; +} + +/** @public */ +export interface ChromeBrand { + logo?: string; + smallLogo?: string; +} + +/** @public */ +export type ChromeBreadcrumb = EuiBreadcrumb; + +/** @public */ +export interface ChromeBreadcrumbsAppendExtension { + content: MountPoint; +} + +/** @public */ +export interface ChromeUserBanner { + content: MountPoint; +} + +/** @public */ +export interface ChromeHelpExtension { + /** + * Provide your plugin's name to create a header for separation + */ + appName: string; + /** + * Creates unified links for sending users to documentation, GitHub, Discuss, or a custom link/button + */ + links?: ChromeHelpExtensionMenuLink[]; + /** + * Custom content to occur below the list of links + */ + content?: (element: HTMLDivElement) => () => void; +} + +/** + * ChromeStart allows plugins to customize the global chrome header UI and + * enrich the UX with additional information about the current location of the + * browser. + * + * @remarks + * While ChromeStart exposes many APIs, they should be used sparingly and the + * developer should understand how they affect other plugins and applications. + * + * @example + * How to add a recently accessed item to the sidebar: + * ```ts + * core.chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); + * ``` + * + * @example + * How to set the help dropdown extension: + * ```tsx + * core.chrome.setHelpExtension(elem => { + * ReactDOM.render(, elem); + * return () => ReactDOM.unmountComponentAtNode(elem); + * }); + * ``` + * + * @public + */ +export interface ChromeStart { + /** {@inheritdoc ChromeNavLinks} */ + navLinks: ChromeNavLinks; + /** {@inheritdoc ChromeNavControls} */ + navControls: ChromeNavControls; + /** {@inheritdoc ChromeRecentlyAccessed} */ + recentlyAccessed: ChromeRecentlyAccessed; + /** {@inheritdoc ChromeDocTitle} */ + docTitle: ChromeDocTitle; + + /** + * Sets the current app's title + * + * @internalRemarks + * This should be handled by the application service once it is in charge + * of mounting applications. + */ + setAppTitle(appTitle: string): void; + + /** + * Get an observable of the current brand information. + */ + getBrand$(): Observable; + + /** + * Set the brand configuration. + * + * @remarks + * Normally the `logo` property will be rendered as the + * CSS background for the home link in the chrome navigation, but when the page is + * rendered in a small window the `smallLogo` will be used and rendered at about + * 45px wide. + * + * @example + * ```js + * chrome.setBrand({ + * logo: 'url(/plugins/app/logo.png) center no-repeat' + * smallLogo: 'url(/plugins/app/logo-small.png) center no-repeat' + * }) + * ``` + * + */ + setBrand(brand: ChromeBrand): void; + + /** + * Get an observable of the current visibility state of the chrome. + */ + getIsVisible$(): Observable; + + /** + * Set the temporary visibility for the chrome. This does nothing if the chrome is hidden + * by default and should be used to hide the chrome for things like full-screen modes + * with an exit button. + */ + setIsVisible(isVisible: boolean): void; + + /** + * Get the current set of classNames that will be set on the application container. + */ + getApplicationClasses$(): Observable; + + /** + * Add a className that should be set on the application container. + */ + addApplicationClass(className: string): void; + + /** + * Remove a className added with `addApplicationClass()`. If className is unknown it is ignored. + */ + removeApplicationClass(className: string): void; + + /** + * Get an observable of the current badge + */ + getBadge$(): Observable; + + /** + * Override the current badge + */ + setBadge(badge?: ChromeBadge): void; + + /** + * Get an observable of the current list of breadcrumbs + */ + getBreadcrumbs$(): Observable; + + /** + * Override the current set of breadcrumbs + */ + setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + + /** + * Get an observable of the current extension appended to breadcrumbs + */ + getBreadcrumbsAppendExtension$(): Observable; + + /** + * Mount an element next to the last breadcrumb + */ + setBreadcrumbsAppendExtension( + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension + ): void; + + /** + * Get an observable of the current custom nav link + */ + getCustomNavLink$(): Observable | undefined>; + + /** + * Override the current set of custom nav link + */ + setCustomNavLink(newCustomNavLink?: Partial): void; + + /** + * Get an observable of the current custom help conttent + */ + getHelpExtension$(): Observable; + + /** + * Override the current set of custom help content + */ + setHelpExtension(helpExtension?: ChromeHelpExtension): void; + + /** + * Override the default support URL shown in the help menu + * @param url The updated support URL + */ + setHelpSupportUrl(url: string): void; + + /** + * Get an observable of the current locked state of the nav drawer. + */ + getIsNavDrawerLocked$(): Observable; + + /** + * Set the banner that will appear on top of the chrome header. + * + * @remarks Using `undefined` when invoking this API will remove the banner. + */ + setHeaderBanner(headerBanner?: ChromeUserBanner): void; +} + +/** @internal */ +export interface InternalChromeStart extends ChromeStart { + /** + * Used only by the rendering service to render the header UI + * @internal + */ + getHeaderComponent(): JSX.Element; + /** + * Used only by the rendering service to retrieve the set of classNames + * that will be set on the body element. + * @internal + */ + getBodyClasses$(): Observable; +} diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 9e7906250949e..4f31c952b8826 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -452,6 +452,55 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + headerBanner$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } helpExtension$={ BehaviorSubject { "_isScalar": false, @@ -1699,14 +1748,67 @@ exports[`Header renders 1`] = ` } } > +
({ htmlIdGenerator: () => () => 'mockId', @@ -63,6 +63,7 @@ describe('Header', () => { const navLinks$ = new BehaviorSubject([ { id: 'kibana', title: 'kibana', baseUrl: '', href: '' }, ]); + const headerBanner$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject({ id: 'cloud-deployment-link', title: 'Manage cloud deployment', @@ -85,6 +86,7 @@ describe('Header', () => { isLocked$={isLocked$} customNavLink$={customNavLink$} breadcrumbsAppendExtension$={breadcrumbsAppendExtension$} + headerBanner$={headerBanner$} /> ); expect(component.find('EuiHeader').exists()).toBeFalsy(); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index b55e7fc412b61..16c89fdca380a 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -32,7 +32,11 @@ import { } from '../..'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; -import { ChromeBreadcrumbsAppendExtension, ChromeHelpExtension } from '../../chrome_service'; +import { + ChromeBreadcrumbsAppendExtension, + ChromeHelpExtension, + ChromeUserBanner, +} from '../../types'; import { OnIsLockedUpdate } from './'; import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; @@ -42,10 +46,12 @@ import { HeaderLogo } from './header_logo'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; import { HeaderExtension } from './header_extension'; +import { HeaderTopBanner } from './header_top_banner'; export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; + headerBanner$: Observable; appTitle$: Observable; badge$: Observable; breadcrumbs$: Observable; @@ -84,7 +90,12 @@ export function Header({ const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); if (!isVisible) { - return ; + return ( + <> + + + + ); } const toggleCollapsibleNavRef = createRef(); @@ -97,11 +108,13 @@ export function Header({ return ( <> +
-
+
- + ; diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 4db79d0233ae9..0e2bae82a3ad3 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -11,7 +11,7 @@ import classNames from 'classnames'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ChromeBreadcrumb } from '../../chrome_service'; +import { ChromeBreadcrumb } from '../../types'; interface Props { appTitle$: Observable; diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index a20613f7e77ef..c6a09c1177a5e 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -26,7 +26,7 @@ import { import { InternalApplicationStart } from '../../../application'; import { GITHUB_CREATE_ISSUE_LINK, KIBANA_FEEDBACK_LINK } from '../../constants'; -import { ChromeHelpExtension } from '../../chrome_service'; +import { ChromeHelpExtension } from '../../types'; import { HeaderExtension } from './header_extension'; import { isModifiedOrPrevented } from './nav_link'; diff --git a/src/core/public/chrome/ui/header/header_top_banner.tsx b/src/core/public/chrome/ui/header/header_top_banner.tsx new file mode 100644 index 0000000000000..667cf9025880f --- /dev/null +++ b/src/core/public/chrome/ui/header/header_top_banner.tsx @@ -0,0 +1,34 @@ +/* + * 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 React, { FC } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; +import { ChromeUserBanner } from '../../types'; +import { HeaderExtension } from './header_extension'; + +export interface HeaderTopBannerProps { + headerBanner$: Observable; +} + +export const HeaderTopBanner: FC = ({ headerBanner$ }) => { + const headerBanner = useObservable(headerBanner$, undefined); + if (!headerBanner) { + return null; + } + + return ( +
+ +
+ ); +}; diff --git a/src/core/public/core_app/styles/_mixins.scss b/src/core/public/core_app/styles/_mixins.scss index 6da7fab8c2f76..d088a47144f33 100644 --- a/src/core/public/core_app/styles/_mixins.scss +++ b/src/core/public/core_app/styles/_mixins.scss @@ -1,3 +1,5 @@ +@import '../../variables'; + @mixin flexParent($grow: 1, $shrink: 1, $basis: auto, $direction: column) { flex: $grow $shrink $basis; display: flex; @@ -82,6 +84,12 @@ overflow: auto; animation: kibanaFullScreenGraphics_FadeIn $euiAnimSpeedExtraSlow $euiAnimSlightResistance 0s forwards; + @at-root { + .kbnBody--hasHeaderBanner & { + top: $kbnHeaderBannerHeight; + } + } + &::before { position: absolute; top: 0; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 02e12ddf4b78b..278bbe469e862 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -194,7 +194,6 @@ export class CoreSystem { http, injectedMetadata, notifications, - uiSettings, }); this.coreApp.start({ application, http, notifications, uiSettings }); diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 6ba9254e5d381..04e2759c91d5d 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,4 +1,5 @@ @import './variables'; +@import './mixins'; @import './core'; @import './chrome/index'; @import './overlays/index'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 423b2c84072b8..3cb6e1beb4e6e 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -46,6 +46,7 @@ import { ChromeStart, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, + ChromeUserBanner, NavType, } from './chrome'; import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; @@ -300,6 +301,7 @@ export { ChromeDocTitle, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, + ChromeUserBanner, ChromeStart, DocLinksStart, FatalErrorInfo, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 99579ada8ec58..b4a2c40f3003b 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -378,11 +378,18 @@ export interface ChromeStart { setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension): void; setCustomNavLink(newCustomNavLink?: Partial): void; + setHeaderBanner(headerBanner?: ChromeUserBanner): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; setIsVisible(isVisible: boolean): void; } +// @public (undocumented) +export interface ChromeUserBanner { + // (undocumented) + content: MountPoint; +} + // @internal (undocumented) export interface CoreContext { // Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts @@ -1519,6 +1526,7 @@ export interface UiSettingsParams { name?: string; optionLabels?: Record; options?: string[]; + order?: number; readonly?: boolean; requiresPageReload?: boolean; // (undocumented) @@ -1537,7 +1545,7 @@ export interface UiSettingsState { } // @public -export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; +export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image' | 'color'; // @public export type UnmountCallback = () => void; diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index b806ac270331d..de13785a17f5b 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,4 +1,4 @@ -@include euiHeaderAffordForFixed($kbnHeaderOffset); +@import '../mixins'; /** * stretch the root element of the Kibana application to set the base-size that @@ -15,11 +15,8 @@ display: flex; flex-flow: column nowrap; margin: 0 auto; - min-height: calc(100vh - #{$kbnHeaderOffset}); - &.hidden-chrome { - min-height: 100vh; - } + @include kibanaFullBodyMinHeight(); } .app-wrapper-panel { @@ -33,3 +30,28 @@ flex-shrink: 0; } } + +// adapted from euiHeaderAffordForFixed as we need to handle the top banner +@mixin kbnAffordForHeader($headerHeight) { + padding-top: $headerHeight; + + .euiFlyout, + .euiCollapsibleNav { + top: $headerHeight; + height: calc(100% - #{$headerHeight}); + } +} + +.kbnBody { + @include kbnAffordForHeader($kbnHeaderOffset); + + &.kbnBody--hasHeaderBanner { + @include kbnAffordForHeader($kbnHeaderOffsetWithBanner); + } + &.kbnBody--chromeHidden { + @include kbnAffordForHeader(0); + } + &.kbnBody--chromeHidden.kbnBody--hasHeaderBanner { + @include kbnAffordForHeader($kbnHeaderBannerHeight); + } +} diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 47b340eed8468..843f2a253f33e 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -9,6 +9,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import { pairwise, startWith } from 'rxjs/operators'; import { InternalChromeStart } from '../chrome'; import { InternalApplicationStart } from '../application'; @@ -32,19 +33,27 @@ interface StartDeps { */ export class RenderingService { start({ application, chrome, overlays, targetDomElement }: StartDeps) { - const chromeUi = chrome.getHeaderComponent(); - const appUi = application.getComponent(); - const bannerUi = overlays.banners.getComponent(); + const chromeHeader = chrome.getHeaderComponent(); + const appComponent = application.getComponent(); + const bannerComponent = overlays.banners.getComponent(); + + const body = document.querySelector('body')!; + chrome + .getBodyClasses$() + .pipe(startWith([]), pairwise()) + .subscribe(([previousClasses, newClasses]) => { + body.classList.remove(...previousClasses); + body.classList.add(...newClasses); + }); ReactDOM.render(
- {chromeUi} - + {chromeHeader}
-
{bannerUi}
- {appUi} +
{bannerComponent}
+ {appComponent}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 67330e7d4dfb3..5419441bbb1e2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -3106,6 +3106,7 @@ export interface UiSettingsParams { name?: string; optionLabels?: Record; options?: string[]; + order?: number; readonly?: boolean; requiresPageReload?: boolean; // (undocumented) @@ -3128,7 +3129,7 @@ export interface UiSettingsServiceStart { } // @public -export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image'; +export type UiSettingsType = 'undefined' | 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string' | 'array' | 'image' | 'color'; // @public export interface UserProvidedValues { diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index e50dc18d9ff1f..235553293d153 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -22,7 +22,8 @@ export type UiSettingsType = | 'boolean' | 'string' | 'array' - | 'image'; + | 'image' + | 'color'; /** * UiSettings deprecation field options. @@ -65,6 +66,13 @@ export interface UiSettingsParams { type?: UiSettingsType; /** optional deprecation information. Used to generate a deprecation warning. */ deprecation?: DeprecationSettings; + /** + * index of the settings within its category (ascending order, smallest will be displayed first). + * Used for ordering in the UI. + * + * @remark settings without order defined will be displayed last and ordered by name + */ + order?: number; /* * Allows defining a custom validation applicable to value change on the client. * @deprecated diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 0be582a4c0294..c7a8c0a6135c7 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -12,7 +12,7 @@ import { UnregisterCallback } from 'history'; import { parse } from 'query-string'; import { UiCounterMetricType } from '@kbn/analytics'; -import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; import { IUiSettingsClient, @@ -28,7 +28,7 @@ import { Form } from './components/form'; import { AdvancedSettingsVoiceAnnouncement } from './components/advanced_settings_voice_announcement'; import { ComponentRegistry } from '../'; -import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; +import { getAriaName, toEditableConfig, fieldSorter, DEFAULT_CATEGORY } from './lib'; import { FieldSetting, SettingsChanges } from './types'; import { parseErrorMsg } from './components/search/search'; @@ -185,17 +185,17 @@ export class AdvancedSettings extends Component { + .map(([settingId, settingDef]) => { return toEditableConfig({ - def: setting[1], - name: setting[0], - value: setting[1].userValue, - isCustom: config.isCustom(setting[0]), - isOverridden: config.isOverridden(setting[0]), + def: settingDef, + name: settingId, + value: settingDef.userValue, + isCustom: config.isCustom(settingId), + isOverridden: config.isOverridden(settingId), }); }) - .filter((c) => !c.readonly) - .sort(Comparators.property('name', Comparators.default('asc'))); + .filter((c) => !c.readOnly) + .sort(fieldSorter); } mapSettings(settings: FieldSetting[]) { diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index 19bf9e6d73757..517a6238c2519 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -866,6 +866,419 @@ exports[`Field for boolean setting should render user value if there is user val `; +exports[`Field for color setting should render as read only if saving is disabled 1`] = ` + +
+ + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + +

+ } +> + + + + +`; + +exports[`Field for color setting should render as read only with help text if overridden 1`] = ` + +
+ + + + + + null + , + } + } + /> + + + + + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + +

+ } +> + + + + } + label="color:test:setting" + labelType="label" + > + + + +`; + +exports[`Field for color setting should render custom setting icon if it is custom 1`] = ` + +
+ + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + } + type="asterisk" + /> + +

+ } +> + + + + +`; + +exports[`Field for color setting should render default value if there is no user value set 1`] = ` + +
+ + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + +

+ } +> + + + + +`; + +exports[`Field for color setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + } + type="asterisk" + /> + +

+ } +> + + + +

+ Setting is currently not saved. +

+
+
+ +`; + +exports[`Field for color setting should render user value if there is user value is set 1`] = ` + +
+ + + + + + null + , + } + } + /> + + + + + } + fullWidth={true} + id="color:test:setting-group" + title={ +

+ + Color test setting + + + +

+ } +> + + + + + +     + + + } + label="color:test:setting" + labelType="label" + > + + + +`; + exports[`Field for image setting should render as read only if saving is disabled 1`] = ` = { isOverridden: false, ...defaults, }, + color: { + name: 'color:test:setting', + ariaName: 'color test setting', + displayName: 'Color test setting', + description: 'Description for Color test setting', + type: 'color', + value: undefined, + defVal: null, + isCustom: false, + isOverridden: false, + ...defaults, + }, }; const userValues = { array: ['user', 'value'], @@ -174,6 +187,7 @@ const userValues = { select: 'banana', string: 'foo', stringWithValidation: 'fooUserValue', + color: '#FACF0C', }; const invalidUserValues = { @@ -187,6 +201,8 @@ const getFieldSettingValue = (wrapper: ReactWrapper, name: string, type: string) const field = findTestSubject(wrapper, `advancedSetting-editField-${name}`); if (type === 'boolean') { return field.props()['aria-checked']; + } else if (type === 'color') { + return field.props().color; } else { return field.props().value; } @@ -423,6 +439,36 @@ describe('Field', () => { }); } }); + } else if (type === 'color') { + describe(`for changing ${type} setting`, () => { + const { wrapper, component } = setup(); + const userValue = userValues[type]; + + it('should be able to change value', async () => { + await (component.instance() as Field).onFieldChange(userValue); + const updated = wrapper.update(); + expect(handleChange).toBeCalledWith(setting.name, { value: userValue }); + updated.setProps({ unsavedChanges: { value: userValue } }); + const currentValue = wrapper.find('EuiColorPicker').prop('color'); + expect(currentValue).toEqual(userValue); + }); + + it('should be able to reset to default value', async () => { + await wrapper.setProps({ + unsavedChanges: {}, + setting: { ...setting, value: userValue }, + }); + const updated = wrapper.update(); + findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); + const expectedEditableValue = getEditableValue(setting.type, setting.defVal); + expect(handleChange).toBeCalledWith(setting.name, { + value: expectedEditableValue, + }); + updated.setProps({ unsavedChanges: { value: expectedEditableValue } }); + const currentValue = wrapper.find('EuiColorPicker').prop('color'); + expect(currentValue).toEqual(expectedEditableValue); + }); + }); } else { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 5569a6e11872a..f5db5c3e371b3 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -17,6 +17,7 @@ import { EuiBadge, EuiCode, EuiCodeBlock, + EuiColorPicker, EuiScreenReaderOnly, EuiCodeEditor, EuiDescribedFormGroup, @@ -392,6 +393,17 @@ export class Field extends PureComponent { data-test-subj={`advancedSetting-editField-${name}`} /> ); + case 'color': + return ( + + ); default: return ( ): FieldSetting => ({ + displayName: 'displayName', + name: 'field', + value: 'value', + requiresPageReload: false, + type: 'string', + category: [], + ariaName: 'ariaName', + isOverridden: false, + defVal: 'defVal', + isCustom: false, + ...parts, +}); + +describe('fieldSorter', () => { + it('sort fields based on their `order` field if present on both', () => { + const fieldA = createField({ order: 3 }); + const fieldB = createField({ order: 1 }); + const fieldC = createField({ order: 2 }); + + expect([fieldA, fieldB, fieldC].sort(fieldSorter)).toEqual([fieldB, fieldC, fieldA]); + }); + it('fields with order defined are ordered first', () => { + const fieldA = createField({ order: 2 }); + const fieldB = createField({ order: undefined }); + const fieldC = createField({ order: 1 }); + + expect([fieldA, fieldB, fieldC].sort(fieldSorter)).toEqual([fieldC, fieldA, fieldB]); + }); + it('sorts by `name` when fields have the same `order`', () => { + const fieldA = createField({ order: 2, name: 'B' }); + const fieldB = createField({ order: 1 }); + const fieldC = createField({ order: 2, name: 'A' }); + + expect([fieldA, fieldB, fieldC].sort(fieldSorter)).toEqual([fieldB, fieldC, fieldA]); + }); + + it('sorts by `name` when fields have no `order`', () => { + const fieldA = createField({ order: undefined, name: 'B' }); + const fieldB = createField({ order: undefined, name: 'A' }); + const fieldC = createField({ order: 1 }); + + expect([fieldA, fieldB, fieldC].sort(fieldSorter)).toEqual([fieldC, fieldB, fieldA]); + }); +}); diff --git a/src/plugins/advanced_settings/public/management_app/lib/sort_fields.ts b/src/plugins/advanced_settings/public/management_app/lib/sort_fields.ts new file mode 100644 index 0000000000000..90bfa18d2198e --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/lib/sort_fields.ts @@ -0,0 +1,31 @@ +/* + * 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 { Comparators } from '@elastic/eui'; +import { FieldSetting } from '../types'; + +const cmp = Comparators.default('asc'); + +export const fieldSorter = (a: FieldSetting, b: FieldSetting): number => { + const aOrder = a.order !== undefined; + const bOrder = b.order !== undefined; + + if (aOrder && bOrder) { + if (a.order === b.order) { + return cmp(a.name, b.name); + } + return cmp(a.order, b.order); + } + if (aOrder) { + return -1; + } + if (bOrder) { + return 1; + } + return cmp(a.name, b.name); +}; diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index b2b7f1c1016cd..49abe3b279a28 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -12,6 +12,7 @@ import { StringValidationRegexString, SavedObjectAttribute, } from 'src/core/public'; +import { FieldSetting } from '../types'; import { getValType } from './get_val_type'; import { getAriaName } from './get_aria_name'; import { DEFAULT_CATEGORY } from './default_category'; @@ -41,7 +42,7 @@ export function toEditableConfig({ const validationTyped = def.validation as StringValidationRegexString; - const conf = { + const conf: FieldSetting = { name, displayName: def.name || name, ariaName: def.name || getAriaName(name), @@ -49,7 +50,7 @@ export function toEditableConfig({ category: def.category && def.category.length ? def.category : [DEFAULT_CATEGORY], isCustom, isOverridden, - readonly: !!def.readonly, + readOnly: !!def.readonly, defVal: def.value, type: getValType(def, value), description: def.description, @@ -63,6 +64,7 @@ export function toEditableConfig({ : def.validation, options: def.options, optionLabels: def.optionLabels, + order: def.order, requiresPageReload: !!def.requiresPageReload, metric: def.metric, }; diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 0563fa310bc77..50b39114d2143 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -25,6 +25,7 @@ export interface FieldSetting { isCustom: boolean; validation?: StringValidation | ImageValidation; readOnly?: boolean; + order?: number; deprecation?: { message: string; docLinksKey: string; diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss index 90bfd84c4d54e..02e60700d49d8 100644 --- a/src/plugins/discover/public/application/components/discover.scss +++ b/src/plugins/discover/public/application/components/discover.scss @@ -1,10 +1,12 @@ +@import '../../../../../core/public/mixins'; + discover-app { flex-grow: 1; } .dscPage { @include euiBreakpoint('m', 'l', 'xl') { - height: calc(100vh - #{($euiHeaderHeightCompensation * 2)}); + @include kibanaFullBodyHeight(); } flex-direction: column; diff --git a/src/plugins/home/public/application/components/_home.scss b/src/plugins/home/public/application/components/_home.scss index 5ff0d0f21b985..913e1511a6314 100644 --- a/src/plugins/home/public/application/components/_home.scss +++ b/src/plugins/home/public/application/components/_home.scss @@ -1,8 +1,10 @@ +@import '../../../../../core/public/mixins'; + .homWrapper { + @include kibanaFullBodyMinHeight(); background-color: $euiColorEmptyShade; display: flex; flex-direction: column; - min-height: calc(100vh - #{$euiHeaderHeightCompensation}); } .homContent { diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index cec815a1a9bc6..3c1ba8eea22ca 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -51,6 +51,9 @@ export class Home extends Component { componentWillUnmount() { this._isMounted = false; + + const body = document.querySelector('body'); + body.classList.remove('isHomPage'); } componentDidMount() { diff --git a/src/plugins/kibana_overview/public/components/_overview.scss b/src/plugins/kibana_overview/public/components/_overview.scss index 5b750202310fb..94555013d0a77 100644 --- a/src/plugins/kibana_overview/public/components/_overview.scss +++ b/src/plugins/kibana_overview/public/components/_overview.scss @@ -1,8 +1,10 @@ +@import '../../../../core/public/mixins'; + .kbnOverviewWrapper { + @include kibanaFullBodyMinHeight(); background-color: $euiColorEmptyShade; display: flex; flex-direction: column; - min-height: calc(100vh - #{$euiHeaderHeightCompensation}); } .kbnOverviewContent { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 86a14a4289ecd..a6ad4d522f8ee 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -58,7 +58,8 @@ "xpack.uptime": ["plugins/uptime"], "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher", - "xpack.observability": "plugins/observability" + "xpack.observability": "plugins/observability", + "xpack.banners": "plugins/banners" }, "exclude": ["examples"], "translations": [ diff --git a/x-pack/plugins/banners/README.md b/x-pack/plugins/banners/README.md new file mode 100644 index 0000000000000..890c194e1bcb0 --- /dev/null +++ b/x-pack/plugins/banners/README.md @@ -0,0 +1,38 @@ +# Kibana banners plugin + +Allow to add a header banner that will be displayed on every page of the Kibana application + +## Configuration + +The plugin's configuration prefix is `xpack.banners` + +The options are + +- `placement` + +The placement of the banner. The allowed values are: + - `disabled` - The banner will be disabled + - `header` - The banner will be displayed in the header + +- `textContent` + +The text content that will be displayed inside the banner, either plain text or markdown + +- `textColor` + +The color of the banner's text. Must be a valid hex color + +- `backgroundColor` + +The color for the banner's background. Must be a valid hex color + +### Configuration example + +`kibana.yml` +```yaml +xpack.banners: + placement: 'header' + textContent: 'Production environment - Proceed with **special levels** of caution' + textColor: '#FF0000' + backgroundColor: '#CC2211' +``` \ No newline at end of file diff --git a/x-pack/plugins/banners/common/index.ts b/x-pack/plugins/banners/common/index.ts new file mode 100644 index 0000000000000..a4c38a58ab572 --- /dev/null +++ b/x-pack/plugins/banners/common/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { BannerInfoResponse, BannerPlacement, BannerConfiguration } from './types'; diff --git a/x-pack/plugins/banners/common/types.ts b/x-pack/plugins/banners/common/types.ts new file mode 100644 index 0000000000000..0c785f516ddb3 --- /dev/null +++ b/x-pack/plugins/banners/common/types.ts @@ -0,0 +1,20 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface BannerInfoResponse { + allowed: boolean; + banner: BannerConfiguration; +} + +export type BannerPlacement = 'disabled' | 'header'; + +export interface BannerConfiguration { + placement: BannerPlacement; + textContent: string; + textColor: string; + backgroundColor: string; +} diff --git a/x-pack/plugins/banners/jest.config.js b/x-pack/plugins/banners/jest.config.js new file mode 100644 index 0000000000000..e2d103c8e4a28 --- /dev/null +++ b/x-pack/plugins/banners/jest.config.js @@ -0,0 +1,12 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/banners'], +}; diff --git a/x-pack/plugins/banners/kibana.json b/x-pack/plugins/banners/kibana.json new file mode 100644 index 0000000000000..3e9441aaa2726 --- /dev/null +++ b/x-pack/plugins/banners/kibana.json @@ -0,0 +1,11 @@ +{ + "id": "banners", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["licensing"], + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"], + "configPath": ["xpack", "banners"] +} diff --git a/x-pack/plugins/banners/public/components/banner.scss b/x-pack/plugins/banners/public/components/banner.scss new file mode 100644 index 0000000000000..586605becb45a --- /dev/null +++ b/x-pack/plugins/banners/public/components/banner.scss @@ -0,0 +1,7 @@ +.kbnUserBanner__container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: $euiFontSizeS; +} diff --git a/x-pack/plugins/banners/public/components/banner.tsx b/x-pack/plugins/banners/public/components/banner.tsx new file mode 100644 index 0000000000000..ea30e46881d0c --- /dev/null +++ b/x-pack/plugins/banners/public/components/banner.tsx @@ -0,0 +1,33 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { Markdown } from '../../../../../src/plugins/kibana_react/public'; +import { BannerConfiguration } from '../../common'; + +import './banner.scss'; + +interface BannerProps { + bannerConfig: BannerConfiguration; +} + +export const Banner: FC = ({ bannerConfig }) => { + const { textContent, textColor, backgroundColor } = bannerConfig; + return ( +
+
+ +
+
+ ); +}; diff --git a/x-pack/plugins/banners/public/components/index.ts b/x-pack/plugins/banners/public/components/index.ts new file mode 100644 index 0000000000000..c23c24fd9c163 --- /dev/null +++ b/x-pack/plugins/banners/public/components/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Banner } from './banner'; diff --git a/x-pack/plugins/banners/public/get_banner_info.test.ts b/x-pack/plugins/banners/public/get_banner_info.test.ts new file mode 100644 index 0000000000000..cfb9bc26db47b --- /dev/null +++ b/x-pack/plugins/banners/public/get_banner_info.test.ts @@ -0,0 +1,35 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../src/core/public/mocks'; +import { getBannerInfo } from './get_banner_info'; + +describe('getBannerInfo', () => { + let http: ReturnType; + + beforeEach(() => { + http = httpServiceMock.createStartContract(); + }); + + it('calls `http.get` with the correct parameters', async () => { + await getBannerInfo(http); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith('/api/banners/info'); + }); + + it('returns the value from the service', async () => { + const expected = { + allowed: true, + }; + http.get.mockResolvedValue(expected); + + const response = await getBannerInfo(http); + + expect(response).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/banners/public/get_banner_info.ts b/x-pack/plugins/banners/public/get_banner_info.ts new file mode 100644 index 0000000000000..56b32b26bef7c --- /dev/null +++ b/x-pack/plugins/banners/public/get_banner_info.ts @@ -0,0 +1,13 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from 'src/core/public'; +import { BannerInfoResponse } from '../common'; + +export const getBannerInfo = async (http: HttpStart): Promise => { + return await http.get('/api/banners/info'); +}; diff --git a/x-pack/plugins/banners/public/index.ts b/x-pack/plugins/banners/public/index.ts new file mode 100644 index 0000000000000..d38a4d4785e09 --- /dev/null +++ b/x-pack/plugins/banners/public/index.ts @@ -0,0 +1,12 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer } from 'src/core/public'; +import { BannersPlugin } from './plugin'; + +export const plugin: PluginInitializer<{}, {}, {}, {}> = (contextInitializer) => + new BannersPlugin(contextInitializer); diff --git a/x-pack/plugins/banners/public/plugin.test.mocks.ts b/x-pack/plugins/banners/public/plugin.test.mocks.ts new file mode 100644 index 0000000000000..cadd10dc96f94 --- /dev/null +++ b/x-pack/plugins/banners/public/plugin.test.mocks.ts @@ -0,0 +1,11 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getBannerInfoMock = jest.fn(); +jest.doMock('./get_banner_info', () => ({ + getBannerInfo: getBannerInfoMock, +})); diff --git a/x-pack/plugins/banners/public/plugin.test.tsx b/x-pack/plugins/banners/public/plugin.test.tsx new file mode 100644 index 0000000000000..036ad17e2598e --- /dev/null +++ b/x-pack/plugins/banners/public/plugin.test.tsx @@ -0,0 +1,86 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getBannerInfoMock } from './plugin.test.mocks'; +import { coreMock } from '../../../../src/core/public/mocks'; +import { BannersPlugin } from './plugin'; +import { BannerClientConfig } from './types'; + +const nextTick = async () => await new Promise((resolve) => resolve()); + +describe('BannersPlugin', () => { + let plugin: BannersPlugin; + let pluginInitContext: ReturnType; + let coreSetup: ReturnType; + let coreStart: ReturnType; + + beforeEach(() => { + pluginInitContext = coreMock.createPluginInitializerContext(); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + + getBannerInfoMock.mockResolvedValue({ + allowed: false, + }); + }); + + const startPlugin = async (config: BannerClientConfig) => { + pluginInitContext = coreMock.createPluginInitializerContext(config); + plugin = new BannersPlugin(pluginInitContext); + plugin.setup(coreSetup); + plugin.start(coreStart); + // await for the `getBannerInfo` promise to resolve + await nextTick(); + }; + + afterEach(() => { + getBannerInfoMock.mockReset(); + }); + + it('calls `getBannerInfo` if `config.placement !== disabled`', async () => { + await startPlugin({ + placement: 'header', + }); + + expect(getBannerInfoMock).toHaveBeenCalledTimes(1); + }); + + it('does not call `getBannerInfo` if `config.placement === disabled`', async () => { + await startPlugin({ + placement: 'disabled', + }); + + expect(getBannerInfoMock).not.toHaveBeenCalled(); + }); + + it('registers the header banner if `getBannerInfo` return `allowed=true`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: true, + }); + + await startPlugin({ + placement: 'header', + }); + + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(1); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledWith({ + content: expect.any(Function), + }); + }); + + it('does not register the header banner if `getBannerInfo` return `allowed=false`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: false, + }); + + await startPlugin({ + placement: 'header', + }); + + expect(coreStart.chrome.setHeaderBanner).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/banners/public/plugin.tsx b/x-pack/plugins/banners/public/plugin.tsx new file mode 100644 index 0000000000000..dca99a816a25b --- /dev/null +++ b/x-pack/plugins/banners/public/plugin.tsx @@ -0,0 +1,44 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { Banner } from './components'; +import { BannerClientConfig } from './types'; +import { getBannerInfo } from './get_banner_info'; + +export class BannersPlugin implements Plugin<{}, {}, {}, {}> { + private readonly config: BannerClientConfig; + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + } + + setup({}: CoreSetup<{}, {}>) { + return {}; + } + + start({ chrome, uiSettings, http }: CoreStart) { + if (this.config.placement !== 'disabled') { + getBannerInfo(http).then( + ({ allowed, banner }) => { + if (allowed) { + chrome.setHeaderBanner({ + content: toMountPoint(), + }); + } + }, + () => { + chrome.setHeaderBanner(undefined); + } + ); + } + + return {}; + } +} diff --git a/x-pack/plugins/banners/public/types.ts b/x-pack/plugins/banners/public/types.ts new file mode 100644 index 0000000000000..1f0ce524a785e --- /dev/null +++ b/x-pack/plugins/banners/public/types.ts @@ -0,0 +1,12 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BannerPlacement } from '../common'; + +export interface BannerClientConfig { + placement: BannerPlacement; +} diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts new file mode 100644 index 0000000000000..9a8cc9680c296 --- /dev/null +++ b/x-pack/plugins/banners/server/config.ts @@ -0,0 +1,42 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; +import { isHexColor } from './utils'; + +const configSchema = schema.object({ + placement: schema.oneOf([schema.literal('disabled'), schema.literal('header')], { + defaultValue: 'disabled', + }), + textContent: schema.string({ defaultValue: '' }), + textColor: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `must be an hex color`; + } + }, + defaultValue: '#8A6A0A', + }), + backgroundColor: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `must be an hex color`; + } + }, + defaultValue: '#FFF9E8', + }), +}); + +export type BannersConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + placement: true, + }, +}; diff --git a/x-pack/plugins/banners/server/index.ts b/x-pack/plugins/banners/server/index.ts new file mode 100644 index 0000000000000..2036eda7e6502 --- /dev/null +++ b/x-pack/plugins/banners/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer } from 'src/core/server'; +import { BannersPlugin } from './plugin'; + +export { config } from './config'; +export const plugin: PluginInitializer<{}, {}, {}, {}> = (context) => new BannersPlugin(context); diff --git a/x-pack/plugins/banners/server/plugin.ts b/x-pack/plugins/banners/server/plugin.ts new file mode 100644 index 0000000000000..66cd083189975 --- /dev/null +++ b/x-pack/plugins/banners/server/plugin.ts @@ -0,0 +1,33 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { BannerConfiguration } from '../common'; +import { BannersConfigType } from './config'; +import { BannersRequestHandlerContext } from './types'; +import { registerRoutes } from './routes'; + +export class BannersPlugin implements Plugin<{}, {}, {}, {}> { + private readonly config: BannerConfiguration; + + constructor(context: PluginInitializerContext) { + this.config = convertConfig(context.config.get()); + } + + setup({ uiSettings, getStartServices, http }: CoreSetup<{}, {}>) { + const router = http.createRouter(); + registerRoutes(router, this.config); + + return {}; + } + + start() { + return {}; + } +} + +const convertConfig = (raw: BannersConfigType): BannerConfiguration => raw; diff --git a/x-pack/plugins/banners/server/routes/index.ts b/x-pack/plugins/banners/server/routes/index.ts new file mode 100644 index 0000000000000..a4eedc3234c86 --- /dev/null +++ b/x-pack/plugins/banners/server/routes/index.ts @@ -0,0 +1,14 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BannerConfiguration } from '../../common'; +import { BannersRouter } from '../types'; +import { registerInfoRoute } from './info'; + +export const registerRoutes = (router: BannersRouter, config: BannerConfiguration) => { + registerInfoRoute(router, config); +}; diff --git a/x-pack/plugins/banners/server/routes/info.ts b/x-pack/plugins/banners/server/routes/info.ts new file mode 100644 index 0000000000000..e0db842028c37 --- /dev/null +++ b/x-pack/plugins/banners/server/routes/info.ts @@ -0,0 +1,36 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ILicense } from '../../../licensing/server'; +import { BannerInfoResponse, BannerConfiguration } from '../../common'; +import { BannersRouter } from '../types'; + +export const registerInfoRoute = (router: BannersRouter, config: BannerConfiguration) => { + router.get( + { + path: '/api/banners/info', + validate: false, + options: { + authRequired: false, + }, + }, + (ctx, req, res) => { + const allowed = isValidLicense(ctx.licensing.license); + + return res.ok({ + body: { + allowed, + banner: config, + } as BannerInfoResponse, + }); + } + ); +}; + +const isValidLicense = (license: ILicense): boolean => { + return license.hasAtLeast('gold'); +}; diff --git a/x-pack/plugins/banners/server/types.ts b/x-pack/plugins/banners/server/types.ts new file mode 100644 index 0000000000000..96f7224e62c22 --- /dev/null +++ b/x-pack/plugins/banners/server/types.ts @@ -0,0 +1,15 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandlerContext, IRouter } from 'src/core/server'; +import { LicensingApiRequestHandlerContext } from '../../licensing/server'; + +export interface BannersRequestHandlerContext extends RequestHandlerContext { + licensing: LicensingApiRequestHandlerContext; +} + +export type BannersRouter = IRouter; diff --git a/x-pack/plugins/banners/server/utils.test.ts b/x-pack/plugins/banners/server/utils.test.ts new file mode 100644 index 0000000000000..57b7a3ede0f8f --- /dev/null +++ b/x-pack/plugins/banners/server/utils.test.ts @@ -0,0 +1,26 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isHexColor } from './utils'; + +describe('isHexColor', () => { + it('returns true for valid 3-length hex colors', () => { + expect(isHexColor('#FEC')).toBe(true); + expect(isHexColor('#0a4')).toBe(true); + }); + + it('returns true for valid 6-length hex colors', () => { + expect(isHexColor('#FF00CC')).toBe(true); + expect(isHexColor('#fab47e')).toBe(true); + }); + + it('returns false for other strings', () => { + expect(isHexColor('#FAZ')).toBe(false); + expect(isHexColor('#FFAAUU')).toBe(false); + expect(isHexColor('foobar')).toBe(false); + }); +}); diff --git a/x-pack/plugins/banners/server/utils.ts b/x-pack/plugins/banners/server/utils.ts new file mode 100644 index 0000000000000..1597b3a2ace3c --- /dev/null +++ b/x-pack/plugins/banners/server/utils.ts @@ -0,0 +1,12 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const hexColorRegexp = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i; + +export const isHexColor = (color: string) => { + return hexColorRegexp.test(color); +}; diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json new file mode 100644 index 0000000000000..85608a8a78ad5 --- /dev/null +++ b/x-pack/plugins/banners/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "common/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" } + ] +} + diff --git a/x-pack/plugins/maps/public/_main.scss b/x-pack/plugins/maps/public/_main.scss index 5ce3bf4e2b998..61de65dd4bf6f 100644 --- a/x-pack/plugins/maps/public/_main.scss +++ b/x-pack/plugins/maps/public/_main.scss @@ -1,19 +1,15 @@ -@import '../../../../src/core/public/variables'; +@import '../../../../src/core/public/mixins'; // sass-lint:disable no-ids #maps-plugin { + @include kibanaFullBodyHeight(); + display: flex; flex-direction: column; - height: calc(100vh - #{$kbnHeaderOffset}); width: 100%; overflow: hidden; } -.mapFullScreen { - // sass-lint:disable no-important - height: 100vh !important; -} - #react-maps-root { flex-grow: 1; display: flex; diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index e5ed8f38a31ee..00197e744e95c 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -1,4 +1,5 @@ @import '@elastic/eui/src/global_styling/variables/header'; +@import '../../../../../src/core/public/mixins'; /** * This is a very brittle way of preventing the editor and other content from disappearing @@ -39,11 +40,11 @@ $bottomBarHeight: $euiSize * 3; line-height: 0; } -// This value is calculated to static value using SCSS because calc in calc has issues in IE11 -$headerOffset: $euiHeaderHeightCompensation * 3; +// adding dev tool top bar + bottom bar height to the body offset +$bodyOffset: $euiHeaderHeightCompensation + $bottomBarHeight; .painlessLabMainContainer { - height: calc(100vh - #{$headerOffset} - #{$bottomBarHeight}); + @include kibanaFullBodyHeight($bodyOffset); } .painlessLabPanelsContainer { diff --git a/x-pack/plugins/searchprofiler/public/application/_app.scss b/x-pack/plugins/searchprofiler/public/application/_app.scss index 6a2d1eb5e2189..3c163fa8fefec 100644 --- a/x-pack/plugins/searchprofiler/public/application/_app.scss +++ b/x-pack/plugins/searchprofiler/public/application/_app.scss @@ -1,3 +1,5 @@ +@import '../../../../../src/core/public/mixins'; + .prfDevTool__page { flex: 1 1 auto; @@ -28,11 +30,11 @@ } } -// This value is calculated to static value using SCSS because calc in calc has issues in IE11 -$headerHeightOffset: $euiHeaderHeightCompensation * 3; +// adding dev tool top bar to the body offset +$bodyOffset: $euiHeaderHeightCompensation; .appRoot { - height: calc(100vh - #{$headerHeightOffset}); + @include kibanaFullBodyHeight($bodyOffset); overflow: hidden; flex-shrink: 1; } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 6209503e75610..4b56ebc83d989 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -40,6 +40,7 @@ { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, { "path": "../plugins/alerts/tsconfig.json" }, + { "path": "../plugins/banners/tsconfig.json" }, { "path": "../plugins/beats_management/tsconfig.json" }, { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/code/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 5589c62010db1..6b874f6253843 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -7,6 +7,7 @@ "plugins/apm/e2e/cypress/**/*", "plugins/apm/ftr_e2e/**/*", "plugins/apm/scripts/**/*", + "plugins/banners/**/*", "plugins/canvas/**/*", "plugins/console_extensions/**/*", "plugins/code/**/*",