mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Implement system
option for theme:darkMode
uiSetting (#173044)
## Summary Fix https://github.com/elastic/kibana/issues/89340 Implements a third option, `system`, for the `theme:darkMode` uiSettings, which will follow the system's theme preference (light/dark) when Kibana loads.82078697
-8bf5-41df-add1-4ecfed6e1dea **Note: system theme refresh still requires the user to reload Kibana - please see the next section for the reasons if you're interested** ## How theming works in Kibana, again? This is an excellent question, thanks for asking. And the answer is, "well, it's complicated". We have multiples sources of "themed" styles in Kibana, ordered from "best" to "worse": #### 1. the EUI/JSS Theming It was initially implemented in https://github.com/elastic/kibana/pull/117368. All react applications and react mountpoints are supposed to be wrapped by a `KibanaThemeProvider` that bridges core's `theme$` values to the `EuiProvider`.477505a2dd/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.tsx (L11)
This one was already dynamic and just works perfectly. If `core.theme.theme$` changes, the new values is received by the theme provider, which automatically changes the styles accordingly, creating this sexy "it just works" effect:f3e61ca7
-f3ed-4c37-aa46-76ee68c1a628 If everything theme-related was using this approach, dynamic theme reload would have been possible. However, Kibana has a lot of legacy, so as you can imagine, it wasn't that easy. So, **don't get false hopes** (as I did when I tried it...) from this video. Dynamic theme swap **could not** be implemented in this PR. And the reasons are just below. #### 2. Per-theme css files6443b57164/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts (L40-L54)
We have a bunch of dark/light variations of some css files that are computed in the rendering service, server-side, to be injected into the page template. Of course, this doesn't play well with dynamic theming, given the UI then doesn't know which css should be swapped, and which one should be added instead. However, porting the responsibilities of which theme css files to load to the browser was achievable, and done in this PR. core's browser-side `theme` provider now receives the list of theme files for both light and dark theme (via the injected metadata), and inject them in the document (instead of having them pre-injected in the page template by the rendering service server-side). So this one wasn't a blocker for dynamic theme reload. #### 3. Plugin styles This is where the problems start. Plugins can ship their own styles too, by importing them from components or from their entrypoint. E.g this componentf1dc1e1869/src/plugins/controls/public/control_group/component/control_group_component.tsx (L9)
importing this file:bafb23580b/src/plugins/controls/public/control_group/control_group.scss (L107-L110)
Which relies on a theme variable, `$euiFormBackgroundColor` So how does it works under the hood? How is that `$euiFormBackgroundColor` variable interpolated? Using which theme? Well, technically, how the styles are effectively loaded differs slightly between dev and production (different webpack loaders/config), but in both cases, it depends on the `__kbnThemeTag__` variable that is injected to the global scope by the `bootstrap.js` script. This `__kbnThemeTag__` tag (apparently) **can** be modified after page load. However it doesn't magically reload everything, so styles already loaded for one theme will not reload. If a component and its imported styles were already compiled / injected, then they won't reload As a short video is better than 3 blocks of text, just see:3087ffd6
-80d8-42bf-ab17-691ec408ea6f That was the first blocker for supporting "dynamic" reloads of the system theme. #### 4. Static inline styles Last but not least, we have some static style injected in the template, that also depend on the current theme.6443b57164/packages/core/rendering/core-rendering-server-internal/src/views/styles.tsx (L52-L54)
Of course this plays very badly with dynamic theming. And worth noting, those styles are used by the "Loading Elastic" screen, where Core (and therefore Core's theming service) isn't loaded yet, which made the things even more complicated. This was the second blocker for supporting "dynamic" reloads of the system theme. #### 5. `euiThemeVars` Actually TIL (not that I was asking for it) We're exposing the EUI theme variable via the `euiThemeVars` of the `@kbn/ui-theme` package: E.g.c7e785383a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx (L41)
c7e785383a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx (L50)
So I did my best, and made it that this export was a proxy, and that Core's theme service was dynamically swapping the target of the proxy depending on the system's theme...b0a0017811/packages/kbn-ui-theme/src/theme.ts (L30-L42)
Unfortunately it doesn't fully work for dynamic system theme switch, given modules/code can keep references of the property directly (e.g. the snippet a few lines on top), and there's nothing to dynamically reload components when the proxy changes. So yet another blocker for dynamic theme switch. ## Release Notes Add a new option, `system`, to the `theme:darkMode` Kibana advanced setting, that can be used to have Kibana's theme follow the system's (light or dark) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a8b8e26344
commit
523e91b63a
41 changed files with 1159 additions and 260 deletions
|
@ -184,8 +184,10 @@ this setting stores part of the URL in your browser session to keep the URL
|
|||
short.
|
||||
|
||||
[[theme-darkmode]]`theme:darkMode`::
|
||||
Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page
|
||||
to apply the setting.
|
||||
The UI theme that the {kib} UI should use.
|
||||
Set to `enabled` or `disabled` to enable or disable the dark theme.
|
||||
Set to `system` to have the {kib} UI theme follow the system theme.
|
||||
You must refresh the page to apply the setting.
|
||||
|
||||
[[theme-version]]`theme:version`::
|
||||
Kibana only ships with the v8 theme now, so this setting can no longer be edited.
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, div, span, svg {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kbnWelcomeView {
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kbnWelcomeTitle {
|
||||
color: #000;
|
||||
font-size: 20px;
|
||||
font-family: sans-serif;
|
||||
margin: 16px 0;
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
opacity: 0;
|
||||
animation-delay: 1.0s;
|
||||
}
|
||||
|
||||
.kbnWelcomeText {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
line-height: 40px !important;
|
||||
height: 40px !important;
|
||||
color: #98A2B3;
|
||||
}
|
||||
|
||||
.kbnLoaderWrap {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
font-family: sans-serif;
|
||||
letter-spacing: -.005em;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-kerning: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kbnLoaderWrap svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.kbnLoader path {
|
||||
stroke: #FFFFFF;
|
||||
}
|
||||
|
||||
.kbnProgress {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.kbnProgress:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: scaleX(0) translateX(0%);
|
||||
animation: kbnProgress 1s cubic-bezier(.694, .0482, .335, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes kbnProgress {
|
||||
0% {
|
||||
transform: scaleX(1) translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1) translateX(100%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-var */
|
||||
|
||||
function systemIsDark() {
|
||||
try {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createInlineStyles(content) {
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var style = document.createElement('style');
|
||||
style.textContent = content;
|
||||
head.appendChild(style);
|
||||
}
|
||||
|
||||
// must be kept in sync with
|
||||
// packages/core/rendering/core-rendering-server-internal/src/views/styles.tsx
|
||||
|
||||
var lightStyles = [
|
||||
'html { background-color: #F8FAFD; }',
|
||||
'.kbnWelcomeText { color: #69707D; }',
|
||||
'.kbnProgress { background-color: #F5F7FA; }',
|
||||
'.kbnProgress:before { background-color: #006DE4; }',
|
||||
].join('\n');
|
||||
|
||||
var darkStyles = [
|
||||
'html { background-color: #141519; }',
|
||||
'.kbnWelcomeText { color: #98A2B3; }',
|
||||
'.kbnProgress { background-color: #25262E; }',
|
||||
'.kbnProgress:before { background-color: #1BA9F5; }',
|
||||
].join('\n');
|
||||
|
||||
if (systemIsDark()) {
|
||||
createInlineStyles(darkStyles);
|
||||
} else {
|
||||
createInlineStyles(lightStyles);
|
||||
}
|
|
@ -6,12 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ThemeVersion } from '@kbn/ui-shared-deps-npm';
|
||||
import {
|
||||
InjectedMetadata,
|
||||
InjectedMetadataClusterInfo,
|
||||
InjectedMetadataExternalUrlPolicy,
|
||||
InjectedMetadataPlugin,
|
||||
InjectedMetadataTheme,
|
||||
} from '@kbn/core-injected-metadata-common-internal';
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
|
||||
|
@ -39,10 +39,7 @@ export interface InternalInjectedMetadataSetup {
|
|||
getExternalUrlConfig: () => {
|
||||
policy: InjectedMetadataExternalUrlPolicy[];
|
||||
};
|
||||
getTheme: () => {
|
||||
darkMode: boolean;
|
||||
version: ThemeVersion;
|
||||
};
|
||||
getTheme: () => InjectedMetadataTheme;
|
||||
getElasticsearchInfo: () => InjectedMetadataClusterInfo;
|
||||
/**
|
||||
* An array of frontend plugins in topological order.
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/std",
|
||||
"@kbn/ui-shared-deps-npm",
|
||||
"@kbn/core-base-common",
|
||||
"@kbn/core-injected-metadata-common-internal",
|
||||
"@kbn/core-custom-branding-common",
|
||||
|
|
|
@ -55,7 +55,14 @@ const createSetupContractMock = () => {
|
|||
},
|
||||
} as any);
|
||||
setupContract.getPlugins.mockReturnValue([]);
|
||||
setupContract.getTheme.mockReturnValue({ darkMode: false, version: 'v8' });
|
||||
setupContract.getTheme.mockReturnValue({
|
||||
darkMode: false,
|
||||
version: 'v8',
|
||||
stylesheetPaths: {
|
||||
default: ['light-1.css'],
|
||||
dark: ['dark-1.css'],
|
||||
},
|
||||
});
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,5 +10,6 @@ export type {
|
|||
InjectedMetadata,
|
||||
InjectedMetadataClusterInfo,
|
||||
InjectedMetadataExternalUrlPolicy,
|
||||
InjectedMetadataTheme,
|
||||
InjectedMetadataPlugin,
|
||||
} from './src/types';
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { PluginName, DiscoveredPlugin } from '@kbn/core-base-common';
|
|||
import type { ThemeVersion } from '@kbn/ui-shared-deps-npm';
|
||||
import type { EnvironmentMode, PackageInfo } from '@kbn/config';
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import type { DarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
import type { BrowserLoggingConfig } from '@kbn/core-logging-common-internal';
|
||||
|
||||
/** @internal */
|
||||
|
@ -36,6 +37,16 @@ export interface InjectedMetadataExternalUrlPolicy {
|
|||
protocol?: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InjectedMetadataTheme {
|
||||
darkMode: DarkModeValue;
|
||||
version: ThemeVersion;
|
||||
stylesheetPaths: {
|
||||
default: string[];
|
||||
dark: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InjectedMetadata {
|
||||
version: string;
|
||||
|
@ -55,10 +66,7 @@ export interface InjectedMetadata {
|
|||
i18n: {
|
||||
translationsUrl: string;
|
||||
};
|
||||
theme: {
|
||||
darkMode: boolean;
|
||||
version: ThemeVersion;
|
||||
};
|
||||
theme: InjectedMetadataTheme;
|
||||
csp: {
|
||||
warnLegacyBrowsers: boolean;
|
||||
};
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"@kbn/ui-shared-deps-npm",
|
||||
"@kbn/core-base-common",
|
||||
"@kbn/core-custom-branding-common",
|
||||
"@kbn/core-logging-common-internal"
|
||||
"@kbn/core-logging-common-internal",
|
||||
"@kbn/core-ui-settings-common"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -62,6 +62,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -128,6 +138,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -198,6 +218,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -264,6 +294,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -330,6 +370,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -400,6 +450,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -466,6 +526,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -532,6 +602,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -607,6 +687,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -673,6 +763,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -748,6 +848,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -819,6 +929,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -885,6 +1005,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -960,6 +1090,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -1031,6 +1171,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
@ -1102,6 +1252,16 @@ Object {
|
|||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
|
|
|
@ -269,6 +269,23 @@ describe('bootstrapRenderer', () => {
|
|||
darkMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls getThemeTag with the correct parameters when darkMode is `system`', async () => {
|
||||
uiSettingsClient.get.mockResolvedValue('system');
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
|
||||
expect(getThemeTagMock).toHaveBeenCalledWith({
|
||||
themeVersion: 'v8',
|
||||
darkMode: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the auth status is `unauthenticated`', () => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { createHash } from 'crypto';
|
|||
import { PackageInfo } from '@kbn/config';
|
||||
import { ThemeVersion } from '@kbn/ui-shared-deps-npm';
|
||||
import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server';
|
||||
import { type DarkModeValue, parseDarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
|
||||
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
|
||||
import { InternalUserSettingsServiceSetup } from '@kbn/core-user-settings-server-internal';
|
||||
|
@ -56,7 +57,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
|
|||
};
|
||||
|
||||
return async function bootstrapRenderer({ uiSettingsClient, request, isAnonymousPage = false }) {
|
||||
let darkMode = false;
|
||||
let darkMode: DarkModeValue = false;
|
||||
const themeVersion: ThemeVersion = 'v8';
|
||||
|
||||
try {
|
||||
|
@ -68,13 +69,18 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
|
|||
if (userSettingDarkMode !== undefined) {
|
||||
darkMode = userSettingDarkMode;
|
||||
} else {
|
||||
darkMode = await uiSettingsClient.get('theme:darkMode');
|
||||
darkMode = parseDarkModeValue(await uiSettingsClient.get('theme:darkMode'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// just use the default values in case of connectivity issues with ES
|
||||
}
|
||||
|
||||
// keeping legacy themeTag support - note that the browser is now overriding it during setup.
|
||||
if (darkMode === 'system') {
|
||||
darkMode = false;
|
||||
}
|
||||
|
||||
const themeTag = getThemeTag({
|
||||
themeVersion,
|
||||
darkMode,
|
||||
|
|
|
@ -6,23 +6,65 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getStylesheetPaths } from './render_utils';
|
||||
import { getThemeStylesheetPaths, getCommonStylesheetPaths, getScriptPaths } from './render_utils';
|
||||
|
||||
describe('getScriptPaths', () => {
|
||||
it('returns the correct list when darkMode is `system`', () => {
|
||||
expect(
|
||||
getScriptPaths({
|
||||
baseHref: '/base-path',
|
||||
darkMode: 'system',
|
||||
})
|
||||
).toEqual(['/base-path/ui/legacy_theme.js']);
|
||||
});
|
||||
|
||||
it('returns the correct list when darkMode is `true`', () => {
|
||||
expect(
|
||||
getScriptPaths({
|
||||
baseHref: '/base-path',
|
||||
darkMode: true,
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the correct list when darkMode is `false`', () => {
|
||||
expect(
|
||||
getScriptPaths({
|
||||
baseHref: '/base-path',
|
||||
darkMode: false,
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommonStylesheetPaths', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getCommonStylesheetPaths({
|
||||
baseHref: '/base-path',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
|
||||
"/base-path/ui/legacy_styles.css",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStylesheetPaths', () => {
|
||||
describe('when darkMode is `true`', () => {
|
||||
describe('when themeVersion is `v8`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
getThemeStylesheetPaths({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/base-path/buildShaShort',
|
||||
buildNum: 17,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.dark.css",
|
||||
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
|
||||
"/base-path/buildShaShort/ui/legacy_dark_theme.min.css",
|
||||
]
|
||||
`);
|
||||
|
@ -33,16 +75,14 @@ describe('getStylesheetPaths', () => {
|
|||
describe('when themeVersion is `v8`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
getThemeStylesheetPaths({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/base-path/buildShaShort',
|
||||
buildNum: 69,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css",
|
||||
"/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css",
|
||||
"/base-path/buildShaShort/ui/legacy_light_theme.min.css",
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
config as loggingConfigDef,
|
||||
type LoggingConfigWithBrowserType,
|
||||
} from '@kbn/core-logging-server-internal';
|
||||
import type { DarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
|
||||
export const getSettingValue = <T>(
|
||||
settingName: string,
|
||||
|
@ -31,15 +32,35 @@ export const getSettingValue = <T>(
|
|||
|
||||
export const getBundlesHref = (baseHref: string): string => `${baseHref}/bundles`;
|
||||
|
||||
export const getStylesheetPaths = ({
|
||||
themeVersion,
|
||||
darkMode,
|
||||
export const getScriptPaths = ({
|
||||
baseHref,
|
||||
darkMode,
|
||||
}: {
|
||||
baseHref: string;
|
||||
darkMode: DarkModeValue;
|
||||
}) => {
|
||||
if (darkMode === 'system') {
|
||||
return [`${baseHref}/ui/legacy_theme.js`];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getCommonStylesheetPaths = ({ baseHref }: { baseHref: string }) => {
|
||||
const bundlesHref = getBundlesHref(baseHref);
|
||||
return [
|
||||
`${bundlesHref}/kbn-ui-shared-deps-src/${UiSharedDepsSrc.cssDistFilename}`,
|
||||
`${baseHref}/ui/legacy_styles.css`,
|
||||
];
|
||||
};
|
||||
|
||||
export const getThemeStylesheetPaths = ({
|
||||
darkMode,
|
||||
themeVersion,
|
||||
baseHref,
|
||||
buildNum,
|
||||
}: {
|
||||
themeVersion: UiSharedDepsNpm.ThemeVersion;
|
||||
darkMode: boolean;
|
||||
buildNum: number;
|
||||
themeVersion: UiSharedDepsNpm.ThemeVersion;
|
||||
baseHref: string;
|
||||
}) => {
|
||||
const bundlesHref = getBundlesHref(baseHref);
|
||||
|
@ -49,14 +70,12 @@ export const getStylesheetPaths = ({
|
|||
`${bundlesHref}/kbn-ui-shared-deps-npm/${UiSharedDepsNpm.darkCssDistFilename(
|
||||
themeVersion
|
||||
)}`,
|
||||
`${bundlesHref}/kbn-ui-shared-deps-src/${UiSharedDepsSrc.cssDistFilename}`,
|
||||
`${baseHref}/ui/legacy_dark_theme.min.css`,
|
||||
]
|
||||
: [
|
||||
`${bundlesHref}/kbn-ui-shared-deps-npm/${UiSharedDepsNpm.lightCssDistFilename(
|
||||
themeVersion
|
||||
)}`,
|
||||
`${bundlesHref}/kbn-ui-shared-deps-src/${UiSharedDepsSrc.cssDistFilename}`,
|
||||
`${baseHref}/ui/legacy_light_theme.min.css`,
|
||||
]),
|
||||
];
|
||||
|
|
|
@ -16,11 +16,15 @@ jest.doMock('./bootstrap', () => ({
|
|||
}));
|
||||
|
||||
export const getSettingValueMock = jest.fn();
|
||||
export const getStylesheetPathsMock = jest.fn();
|
||||
export const getCommonStylesheetPathsMock = jest.fn();
|
||||
export const getThemeStylesheetPathsMock = jest.fn();
|
||||
export const getScriptPathsMock = jest.fn();
|
||||
export const getBrowserLoggingConfigMock = jest.fn();
|
||||
|
||||
jest.doMock('./render_utils', () => ({
|
||||
getSettingValue: getSettingValueMock,
|
||||
getStylesheetPaths: getStylesheetPathsMock,
|
||||
getCommonStylesheetPaths: getCommonStylesheetPathsMock,
|
||||
getThemeStylesheetPaths: getThemeStylesheetPathsMock,
|
||||
getScriptPaths: getScriptPathsMock,
|
||||
getBrowserLoggingConfig: getBrowserLoggingConfigMock,
|
||||
}));
|
||||
|
|
|
@ -10,7 +10,9 @@ import {
|
|||
registerBootstrapRouteMock,
|
||||
bootstrapRendererMock,
|
||||
getSettingValueMock,
|
||||
getStylesheetPathsMock,
|
||||
getCommonStylesheetPathsMock,
|
||||
getThemeStylesheetPathsMock,
|
||||
getScriptPathsMock,
|
||||
getBrowserLoggingConfigMock,
|
||||
} from './rendering_service.test.mocks';
|
||||
|
||||
|
@ -167,7 +169,7 @@ function renderTestCases(
|
|||
expect(data.legacyMetadata.globalUiSettings.user).toEqual({}); // user settings are not injected
|
||||
});
|
||||
|
||||
it('calls `getStylesheetPaths` with the correct parameters', async () => {
|
||||
it('calls `getCommonStylesheetPaths` with the correct parameters', async () => {
|
||||
getSettingValueMock.mockImplementation((settingName: string) => {
|
||||
if (settingName === 'theme:darkMode') {
|
||||
return true;
|
||||
|
@ -178,12 +180,51 @@ function renderTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getCommonStylesheetPathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getCommonStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
baseHref: '/mock-server-basepath',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `getScriptPaths` with the correct parameters', async () => {
|
||||
getSettingValueMock.mockImplementation((settingName: string) => {
|
||||
if (settingName === 'theme:darkMode') {
|
||||
return true;
|
||||
}
|
||||
return settingName;
|
||||
});
|
||||
|
||||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getScriptPathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getScriptPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
baseHref: '/mock-server-basepath',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `getThemeStylesheetPaths` with the correct parameters', async () => {
|
||||
getSettingValueMock.mockImplementation((settingName: string) => {
|
||||
if (settingName === 'theme:darkMode') {
|
||||
return true;
|
||||
}
|
||||
return settingName;
|
||||
});
|
||||
|
||||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledTimes(2);
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -263,11 +304,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -289,11 +329,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -313,11 +352,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -337,11 +375,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -361,11 +398,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -385,11 +421,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -409,11 +444,10 @@ function renderDarkModeTestCases(
|
|||
const [render] = await getRender();
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
expect(getThemeStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
baseHref: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -436,8 +470,10 @@ describe('RenderingService', () => {
|
|||
jest.clearAllMocks();
|
||||
service = new RenderingService(mockRenderingServiceParams);
|
||||
|
||||
getSettingValueMock.mockReset().mockImplementation((settingName: string) => settingName);
|
||||
getStylesheetPathsMock.mockReset().mockReturnValue(['/style-1.css', '/style-2.css']);
|
||||
getSettingValueMock.mockImplementation((settingName: string) => settingName);
|
||||
getCommonStylesheetPathsMock.mockReturnValue(['/common-1.css']);
|
||||
getThemeStylesheetPathsMock.mockReturnValue(['/style-1.css', '/style-2.css']);
|
||||
getScriptPathsMock.mockReturnValue(['/script-1.js']);
|
||||
getBrowserLoggingConfigMock.mockReset().mockReturnValue({});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,8 +17,12 @@ import type { CoreContext } from '@kbn/core-base-server-internal';
|
|||
import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
|
||||
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
|
||||
import { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import { UserProvidedValues } from '@kbn/core-ui-settings-common';
|
||||
import type { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import {
|
||||
type UserProvidedValues,
|
||||
type DarkModeValue,
|
||||
parseDarkModeValue,
|
||||
} from '@kbn/core-ui-settings-common';
|
||||
import { Template } from './views';
|
||||
import {
|
||||
IRenderOptions,
|
||||
|
@ -29,7 +33,13 @@ import {
|
|||
RenderingMetadata,
|
||||
} from './types';
|
||||
import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap';
|
||||
import { getSettingValue, getStylesheetPaths, getBrowserLoggingConfig } from './render_utils';
|
||||
import {
|
||||
getSettingValue,
|
||||
getCommonStylesheetPaths,
|
||||
getThemeStylesheetPaths,
|
||||
getScriptPaths,
|
||||
getBrowserLoggingConfig,
|
||||
} from './render_utils';
|
||||
import { filterUiPlugins } from './filter_ui_plugins';
|
||||
import type { InternalRenderingRequestHandlerContext } from './internal_types';
|
||||
|
||||
|
@ -42,6 +52,8 @@ type RenderOptions =
|
|||
userSettings?: never;
|
||||
});
|
||||
|
||||
const themeVersion: ThemeVersion = 'v8';
|
||||
|
||||
/** @internal */
|
||||
export class RenderingService {
|
||||
constructor(private readonly coreContext: CoreContext) {}
|
||||
|
@ -113,7 +125,6 @@ export class RenderingService {
|
|||
mode: this.coreContext.env.mode,
|
||||
packageInfo: this.coreContext.env.packageInfo,
|
||||
};
|
||||
const buildNum = env.packageInfo.buildNum;
|
||||
const staticAssetsHrefBase = http.staticAssets.getHrefBase();
|
||||
const basePath = http.basePath.get(request);
|
||||
const { serverBasePath, publicBaseUrl } = http.basePath;
|
||||
|
@ -160,29 +171,32 @@ export class RenderingService {
|
|||
// swallow error
|
||||
}
|
||||
|
||||
let userSettingDarkMode: boolean | undefined;
|
||||
|
||||
if (!isAnonymousPage) {
|
||||
userSettingDarkMode = await userSettings?.getUserSettingDarkMode(request);
|
||||
}
|
||||
|
||||
let darkMode: boolean;
|
||||
// dark mode
|
||||
const userSettingDarkMode = isAnonymousPage
|
||||
? undefined
|
||||
: await userSettings?.getUserSettingDarkMode(request);
|
||||
|
||||
const isThemeOverridden = settings.user['theme:darkMode']?.isOverridden ?? false;
|
||||
|
||||
let darkMode: DarkModeValue;
|
||||
if (userSettingDarkMode !== undefined && !isThemeOverridden) {
|
||||
darkMode = userSettingDarkMode;
|
||||
} else {
|
||||
darkMode = getSettingValue('theme:darkMode', settings, Boolean);
|
||||
darkMode = getSettingValue<DarkModeValue>('theme:darkMode', settings, parseDarkModeValue);
|
||||
}
|
||||
|
||||
const themeVersion: ThemeVersion = 'v8';
|
||||
|
||||
const stylesheetPaths = getStylesheetPaths({
|
||||
darkMode,
|
||||
themeVersion,
|
||||
const themeStylesheetPaths = (mode: boolean) =>
|
||||
getThemeStylesheetPaths({
|
||||
darkMode: mode,
|
||||
themeVersion,
|
||||
baseHref: staticAssetsHrefBase,
|
||||
});
|
||||
const commonStylesheetPaths = getCommonStylesheetPaths({
|
||||
baseHref: staticAssetsHrefBase,
|
||||
});
|
||||
const scriptPaths = getScriptPaths({
|
||||
darkMode,
|
||||
baseHref: staticAssetsHrefBase,
|
||||
buildNum,
|
||||
});
|
||||
|
||||
const loggingConfig = await getBrowserLoggingConfig(this.coreContext.configService);
|
||||
|
@ -195,9 +209,10 @@ export class RenderingService {
|
|||
bootstrapScriptUrl: `${basePath}/${bootstrapScript}`,
|
||||
i18n: i18n.translate,
|
||||
locale: i18n.getLocale(),
|
||||
darkMode,
|
||||
themeVersion,
|
||||
stylesheetPaths,
|
||||
darkMode,
|
||||
stylesheetPaths: commonStylesheetPaths,
|
||||
scriptPaths,
|
||||
customBranding: {
|
||||
faviconSVG: branding?.faviconSVG,
|
||||
faviconPNG: branding?.faviconPNG,
|
||||
|
@ -223,6 +238,10 @@ export class RenderingService {
|
|||
theme: {
|
||||
darkMode,
|
||||
version: themeVersion,
|
||||
stylesheetPaths: {
|
||||
default: themeStylesheetPaths(false),
|
||||
dark: themeStylesheetPaths(true),
|
||||
},
|
||||
},
|
||||
customBranding: {
|
||||
logo: branding?.logo,
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
} from '@kbn/core-http-server-internal';
|
||||
import type { InternalElasticsearchServiceSetup } from '@kbn/core-elasticsearch-server-internal';
|
||||
import type { InternalStatusServiceSetup } from '@kbn/core-status-server-internal';
|
||||
import type { DarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
|
||||
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
|
||||
import type { InternalCustomBrandingSetup } from '@kbn/core-custom-branding-server-internal';
|
||||
|
@ -29,9 +30,10 @@ export interface RenderingMetadata {
|
|||
bootstrapScriptUrl: string;
|
||||
i18n: typeof i18n.translate;
|
||||
locale: string;
|
||||
darkMode: boolean;
|
||||
themeVersion: ThemeVersion;
|
||||
darkMode: DarkModeValue;
|
||||
stylesheetPaths: string[];
|
||||
scriptPaths: string[];
|
||||
injectedMetadata: InjectedMetadata;
|
||||
customBranding: CustomBranding;
|
||||
}
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import type { DarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
|
||||
interface Props {
|
||||
darkMode: boolean;
|
||||
darkMode: DarkModeValue;
|
||||
stylesheetPaths: string[];
|
||||
}
|
||||
|
||||
export const Styles: FC<Props> = ({ darkMode, stylesheetPaths }) => {
|
||||
return (
|
||||
<>
|
||||
<InlineStyles darkMode={darkMode} />
|
||||
{darkMode !== 'system' && <InlineStyles darkMode={darkMode} />}
|
||||
{stylesheetPaths.map((path) => (
|
||||
<link key={path} rel="stylesheet" type="text/css" href={path} />
|
||||
))}
|
||||
|
@ -25,136 +26,30 @@ export const Styles: FC<Props> = ({ darkMode, stylesheetPaths }) => {
|
|||
};
|
||||
|
||||
const InlineStyles: FC<{ darkMode: boolean }> = ({ darkMode }) => {
|
||||
// must be kept in sync with
|
||||
// packages/core/apps/core-apps-server-internal/assets/legacy_theme.js
|
||||
/* eslint-disable react/no-danger */
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, div, span, svg {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: ${darkMode ? '#141519' : '#F8FAFD'}
|
||||
}
|
||||
|
||||
.kbnWelcomeView {
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kbnWelcomeTitle {
|
||||
color: #000;
|
||||
font-size: 20px;
|
||||
font-family: sans-serif;
|
||||
margin: 16px 0;
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
opacity: 0;
|
||||
animation-delay: 1.0s;
|
||||
}
|
||||
|
||||
.kbnWelcomeText {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
line-height: 40px !important;
|
||||
height: 40px !important;
|
||||
color: #98a2b3;
|
||||
color: ${darkMode ? '#98A2B3' : '#69707D'};
|
||||
}
|
||||
|
||||
.kbnLoaderWrap {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
letter-spacing: -.005em;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-kerning: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kbnLoaderWrap svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.kbnLoader path {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.kbnProgress {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
background-color: ${darkMode ? '#25262E' : '#F5F7FA'};
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.kbnProgress:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: scaleX(0) translateX(0%);
|
||||
animation: kbnProgress 1s cubic-bezier(.694, .0482, .335, 1) infinite;
|
||||
background-color: ${darkMode ? '#1BA9F5' : '#006DE4'};
|
||||
}
|
||||
|
||||
@keyframes kbnProgress {
|
||||
0% {
|
||||
transform: scaleX(1) translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1) translateX(100%);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
*/
|
||||
|
||||
import React, { FunctionComponent, createElement } from 'react';
|
||||
|
||||
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
|
||||
import { RenderingMetadata } from '../types';
|
||||
import { Fonts } from './fonts';
|
||||
import { Styles } from './styles';
|
||||
import { Logo } from './logo';
|
||||
import { Styles } from './styles';
|
||||
|
||||
interface Props {
|
||||
metadata: RenderingMetadata;
|
||||
|
@ -24,6 +23,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
locale,
|
||||
darkMode,
|
||||
stylesheetPaths,
|
||||
scriptPaths,
|
||||
injectedMetadata,
|
||||
i18n,
|
||||
bootstrapScriptUrl,
|
||||
|
@ -56,6 +56,9 @@ export const Template: FunctionComponent<Props> = ({
|
|||
<meta name={EUI_STYLES_GLOBAL} />
|
||||
<meta name="emotion" />
|
||||
<Styles darkMode={darkMode} stylesheetPaths={stylesheetPaths} />
|
||||
{scriptPaths.map((path) => (
|
||||
<script key={path} src={path} />
|
||||
))}
|
||||
{/* Inject stylesheets into the <head> before scripts so that KP plugins with bundled styles will override them */}
|
||||
<meta name="add-styles-here" />
|
||||
<meta name="add-scripts-here" />
|
||||
|
|
|
@ -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 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.
|
||||
*/
|
||||
|
||||
export const systemThemeIsDark = (): boolean => {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
};
|
||||
|
||||
export const onSystemThemeChange = (handler: (darkMode: boolean) => void) => {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
handler(e.matches);
|
||||
});
|
||||
};
|
||||
|
||||
export const browsersSupportsSystemTheme = (): boolean => {
|
||||
try {
|
||||
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
return matchMedia.matches !== undefined && matchMedia.addEventListener !== undefined;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const systemThemeIsDarkMock = jest.fn();
|
||||
export const onSystemThemeChangeMock = jest.fn();
|
||||
export const browsersSupportsSystemThemeMock = jest.fn();
|
||||
|
||||
jest.doMock('./system_theme', () => {
|
||||
const actual = jest.requireActual('./utils');
|
||||
return {
|
||||
...actual,
|
||||
systemThemeIsDark: systemThemeIsDarkMock,
|
||||
onSystemThemeChange: onSystemThemeChangeMock,
|
||||
browsersSupportsSystemTheme: browsersSupportsSystemThemeMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const createStyleSheetMock = jest.fn();
|
||||
|
||||
jest.doMock('./utils', () => {
|
||||
const actual = jest.requireActual('./utils');
|
||||
return {
|
||||
...actual,
|
||||
createStyleSheet: createStyleSheetMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const setDarkModeMock = jest.fn();
|
||||
|
||||
jest.doMock('@kbn/ui-theme', () => {
|
||||
const actual = jest.requireActual('@kbn/ui-theme');
|
||||
return {
|
||||
...actual,
|
||||
_setDarkMode: setDarkModeMock,
|
||||
};
|
||||
});
|
|
@ -6,10 +6,24 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
browsersSupportsSystemThemeMock,
|
||||
onSystemThemeChangeMock,
|
||||
systemThemeIsDarkMock,
|
||||
createStyleSheetMock,
|
||||
setDarkModeMock,
|
||||
} from './theme_service.test.mocks';
|
||||
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks';
|
||||
import { ThemeService } from './theme_service';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__kbnThemeTag__: string;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ThemeService', () => {
|
||||
let themeService: ThemeService;
|
||||
let injectedMetadata: ReturnType<typeof injectedMetadataServiceMock.createSetupContract>;
|
||||
|
@ -17,24 +31,204 @@ describe('ThemeService', () => {
|
|||
beforeEach(() => {
|
||||
themeService = new ThemeService();
|
||||
injectedMetadata = injectedMetadataServiceMock.createSetupContract();
|
||||
|
||||
browsersSupportsSystemThemeMock.mockReset().mockReturnValue(true);
|
||||
systemThemeIsDarkMock.mockReset().mockReturnValue(false);
|
||||
onSystemThemeChangeMock.mockReset();
|
||||
createStyleSheetMock.mockReset().mockReturnValue({ remove: jest.fn() });
|
||||
setDarkModeMock.mockReset();
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
it('exposes a `theme$` observable with the values provided by the injected metadata', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
describe('darkMode is `false`', () => {
|
||||
beforeEach(() => {
|
||||
injectedMetadata.getTheme.mockReturnValue({
|
||||
version: 'v8',
|
||||
darkMode: false,
|
||||
stylesheetPaths: {
|
||||
dark: ['dark-1.css'],
|
||||
default: ['light-1.css'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('exposed the correct `$theme` value from the observable', async () => {
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
expect(theme).toEqual({
|
||||
darkMode: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets __kbnThemeTag__ to the correct value', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(window.__kbnThemeTag__).toEqual('v8light');
|
||||
});
|
||||
|
||||
it('calls createStyleSheet with the correct parameters', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(createStyleSheetMock).toHaveBeenCalledTimes(1);
|
||||
expect(createStyleSheetMock).toHaveBeenCalledWith({ href: 'light-1.css' });
|
||||
});
|
||||
|
||||
it('calls _setDarkMode with the correct parameters', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(setDarkModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDarkModeMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('does not call onSystemThemeChange', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(onSystemThemeChangeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTheme() returns the current theme', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
const setup = themeService.setup({ injectedMetadata });
|
||||
const theme = setup.getTheme();
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
describe('darkMode is `true`', () => {
|
||||
beforeEach(() => {
|
||||
injectedMetadata.getTheme.mockReturnValue({
|
||||
version: 'v8',
|
||||
darkMode: true,
|
||||
stylesheetPaths: {
|
||||
dark: ['dark-1.css'],
|
||||
default: ['light-1.css'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('exposed the correct `$theme` value from the observable', async () => {
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets __kbnThemeTag__ to the correct value', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(window.__kbnThemeTag__).toEqual('v8dark');
|
||||
});
|
||||
|
||||
it('calls createStyleSheet with the correct parameters', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(createStyleSheetMock).toHaveBeenCalledTimes(1);
|
||||
expect(createStyleSheetMock).toHaveBeenCalledWith({ href: 'dark-1.css' });
|
||||
});
|
||||
|
||||
it('calls _setDarkMode with the correct parameters', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(setDarkModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDarkModeMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('does not call onSystemThemeChange', async () => {
|
||||
themeService.setup({ injectedMetadata });
|
||||
expect(onSystemThemeChangeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('darkMode is `system`', () => {
|
||||
beforeEach(() => {
|
||||
injectedMetadata.getTheme.mockReturnValue({
|
||||
version: 'v8',
|
||||
darkMode: 'system',
|
||||
stylesheetPaths: {
|
||||
dark: ['dark-1.css'],
|
||||
default: ['light-1.css'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('when browser does not support system theme', () => {
|
||||
beforeEach(() => {
|
||||
browsersSupportsSystemThemeMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('fallbacks to default light theme', async () => {
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
|
||||
expect(theme).toEqual({
|
||||
darkMode: false,
|
||||
});
|
||||
|
||||
expect(window.__kbnThemeTag__).toEqual('v8light');
|
||||
|
||||
expect(setDarkModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDarkModeMock).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(createStyleSheetMock).toHaveBeenCalledTimes(1);
|
||||
expect(createStyleSheetMock).toHaveBeenCalledWith({ href: 'light-1.css' });
|
||||
|
||||
expect(onSystemThemeChangeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when browser supports system theme', () => {
|
||||
beforeEach(() => {
|
||||
browsersSupportsSystemThemeMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('uses the system theme when light', async () => {
|
||||
systemThemeIsDarkMock.mockReturnValue(false);
|
||||
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
|
||||
expect(theme).toEqual({
|
||||
darkMode: false,
|
||||
});
|
||||
|
||||
expect(window.__kbnThemeTag__).toEqual('v8light');
|
||||
|
||||
expect(setDarkModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDarkModeMock).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(createStyleSheetMock).toHaveBeenCalledTimes(1);
|
||||
expect(createStyleSheetMock).toHaveBeenCalledWith({ href: 'light-1.css' });
|
||||
});
|
||||
|
||||
it('uses the system theme when dark', async () => {
|
||||
systemThemeIsDarkMock.mockReturnValue(true);
|
||||
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await firstValueFrom(theme$);
|
||||
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
|
||||
expect(window.__kbnThemeTag__).toEqual('v8dark');
|
||||
|
||||
expect(setDarkModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(setDarkModeMock).toHaveBeenCalledWith(true);
|
||||
|
||||
expect(createStyleSheetMock).toHaveBeenCalledTimes(1);
|
||||
expect(createStyleSheetMock).toHaveBeenCalledWith({ href: 'dark-1.css' });
|
||||
});
|
||||
|
||||
// unsupported and disabled for now
|
||||
it.skip('reacts to system theme change', async () => {
|
||||
systemThemeIsDarkMock.mockReturnValue(false);
|
||||
|
||||
let handler: (mode: boolean) => void;
|
||||
onSystemThemeChangeMock.mockImplementation((_handler: (mode: boolean) => void) => {
|
||||
handler = _handler;
|
||||
});
|
||||
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
|
||||
expect(await firstValueFrom(theme$)).toEqual({
|
||||
darkMode: false,
|
||||
});
|
||||
expect(window.__kbnThemeTag__).toEqual('v8light');
|
||||
|
||||
handler!(true);
|
||||
|
||||
expect(await firstValueFrom(theme$)).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
expect(window.__kbnThemeTag__).toEqual('v8dark');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -47,7 +241,14 @@ describe('ThemeService', () => {
|
|||
});
|
||||
|
||||
it('exposes a `theme$` observable with the values provided by the injected metadata', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
injectedMetadata.getTheme.mockReturnValue({
|
||||
version: 'v8',
|
||||
darkMode: true,
|
||||
stylesheetPaths: {
|
||||
dark: [],
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
themeService.setup({ injectedMetadata });
|
||||
const { theme$ } = themeService.start();
|
||||
const theme = await firstValueFrom(theme$);
|
||||
|
@ -55,15 +256,5 @@ describe('ThemeService', () => {
|
|||
darkMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTheme() returns the current theme', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
themeService.setup({ injectedMetadata });
|
||||
const start = themeService.start();
|
||||
const theme = start.getTheme();
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
import { _setDarkMode } from '@kbn/ui-theme';
|
||||
import type { InjectedMetadataTheme } from '@kbn/core-injected-metadata-common-internal';
|
||||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { CoreTheme, ThemeServiceSetup, ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import { systemThemeIsDark, browsersSupportsSystemTheme } from './system_theme';
|
||||
import { createStyleSheet } from './utils';
|
||||
|
||||
/** @internal */
|
||||
export interface ThemeServiceSetupDeps {
|
||||
|
@ -18,15 +22,26 @@ export interface ThemeServiceSetupDeps {
|
|||
/** @internal */
|
||||
export class ThemeService {
|
||||
private contract?: ThemeServiceSetup;
|
||||
private stop$ = new Subject<void>();
|
||||
private themeMetadata?: InjectedMetadataTheme;
|
||||
private stylesheets: HTMLLinkElement[] = [];
|
||||
|
||||
public setup({ injectedMetadata }: ThemeServiceSetupDeps): ThemeServiceSetup {
|
||||
const themeMeta = injectedMetadata.getTheme();
|
||||
const theme: CoreTheme = { darkMode: themeMeta.darkMode };
|
||||
const themeMetadata = injectedMetadata.getTheme();
|
||||
this.themeMetadata = themeMetadata;
|
||||
|
||||
let theme: CoreTheme;
|
||||
if (themeMetadata.darkMode === 'system' && browsersSupportsSystemTheme()) {
|
||||
theme = { darkMode: systemThemeIsDark() };
|
||||
} else {
|
||||
const darkMode = themeMetadata.darkMode === 'system' ? false : themeMetadata.darkMode;
|
||||
theme = { darkMode };
|
||||
}
|
||||
|
||||
this.applyTheme(theme);
|
||||
|
||||
this.contract = {
|
||||
theme$: of(theme),
|
||||
getTheme: () => theme,
|
||||
theme$: of(theme),
|
||||
};
|
||||
|
||||
return this.contract;
|
||||
|
@ -40,7 +55,28 @@ export class ThemeService {
|
|||
return this.contract;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.stop$.next();
|
||||
public stop() {}
|
||||
|
||||
private applyTheme(theme: CoreTheme) {
|
||||
const { darkMode } = theme;
|
||||
this.stylesheets.forEach((stylesheet) => {
|
||||
stylesheet.remove();
|
||||
});
|
||||
this.stylesheets = [];
|
||||
const newStylesheets = darkMode
|
||||
? this.themeMetadata!.stylesheetPaths.dark
|
||||
: this.themeMetadata!.stylesheetPaths.default;
|
||||
|
||||
newStylesheets.forEach((stylesheet) => {
|
||||
this.stylesheets.push(createStyleSheet({ href: stylesheet }));
|
||||
});
|
||||
|
||||
_setDarkMode(darkMode);
|
||||
updateKbnThemeTag(darkMode);
|
||||
}
|
||||
}
|
||||
|
||||
const updateKbnThemeTag = (darkMode: boolean) => {
|
||||
const globals: any = typeof window === 'undefined' ? {} : window;
|
||||
globals.__kbnThemeTag__ = darkMode ? 'v8dark' : 'v8light';
|
||||
};
|
||||
|
|
18
packages/core/theme/core-theme-browser-internal/src/utils.ts
Normal file
18
packages/core/theme/core-theme-browser-internal/src/utils.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const createStyleSheet = ({ href }: { href: string }) => {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = href;
|
||||
link.media = 'all';
|
||||
head.appendChild(link);
|
||||
return link;
|
||||
};
|
|
@ -18,6 +18,8 @@
|
|||
"@kbn/core-injected-metadata-browser-mocks",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/core-injected-metadata-common-internal",
|
||||
"@kbn/ui-theme",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -14,5 +14,6 @@ export type {
|
|||
UserProvidedValues,
|
||||
UiSettingsScope,
|
||||
} from './src/ui_settings';
|
||||
export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode';
|
||||
|
||||
export { TIMEZONE_OPTIONS } from './src/timezones';
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { parseDarkModeValue } from './dark_mode';
|
||||
|
||||
describe('parseDarkModeValue', () => {
|
||||
it('should return true when rawValue is true or "true" or "enabled"', () => {
|
||||
expect(parseDarkModeValue(true)).toBe(true);
|
||||
expect(parseDarkModeValue('true')).toBe(true);
|
||||
expect(parseDarkModeValue('enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when rawValue is false or "false" or "disabled"', () => {
|
||||
expect(parseDarkModeValue(false)).toBe(false);
|
||||
expect(parseDarkModeValue('false')).toBe(false);
|
||||
expect(parseDarkModeValue('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return "system" when rawValue is "system"', () => {
|
||||
expect(parseDarkModeValue('system')).toBe('system');
|
||||
});
|
||||
|
||||
it('should return Boolean(rawValue) when rawValue is not one of the predefined values', () => {
|
||||
expect(parseDarkModeValue('randomText')).toBe(true);
|
||||
expect(parseDarkModeValue('')).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The list of possible values for the dark mode UI setting.
|
||||
* - false: dark mode is disabled
|
||||
* - true: dark mode is enabled
|
||||
* - "system": dark mode will follow the user system preference.
|
||||
*/
|
||||
export type DarkModeValue = true | false | 'system';
|
||||
|
||||
export const parseDarkModeValue = (rawValue: unknown): DarkModeValue => {
|
||||
if (rawValue === true || rawValue === 'true' || rawValue === 'enabled') {
|
||||
return true;
|
||||
}
|
||||
if (rawValue === false || rawValue === 'false' || rawValue === 'disabled') {
|
||||
return false;
|
||||
}
|
||||
if (rawValue === 'system') {
|
||||
return 'system';
|
||||
}
|
||||
return Boolean(rawValue);
|
||||
};
|
|
@ -18,15 +18,16 @@ describe('theme settings', () => {
|
|||
describe('theme:darkMode', () => {
|
||||
const validate = getValidationFn(themeSettings['theme:darkMode']);
|
||||
|
||||
it('should only accept boolean values', () => {
|
||||
it('should only accept expected values', () => {
|
||||
expect(() => validate(true)).not.toThrow();
|
||||
expect(() => validate(false)).not.toThrow();
|
||||
expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [boolean] but got [string]"`
|
||||
);
|
||||
expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [boolean] but got [number]"`
|
||||
);
|
||||
|
||||
expect(() => validate('enabled')).not.toThrow();
|
||||
expect(() => validate('disabled')).not.toThrow();
|
||||
expect(() => validate('system')).not.toThrow();
|
||||
|
||||
expect(() => validate('foo')).toThrowError();
|
||||
expect(() => validate(12)).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -35,22 +36,22 @@ describe('process.env.KBN_OPTIMIZER_THEMES handling', () => {
|
|||
it('defaults to properties of first tag', () => {
|
||||
process.env.KBN_OPTIMIZER_THEMES = 'v8dark,v8light';
|
||||
let settings = getThemeSettings({ isDist: false });
|
||||
expect(settings['theme:darkMode'].value).toBe(true);
|
||||
expect(settings['theme:darkMode'].value).toBe('enabled');
|
||||
|
||||
process.env.KBN_OPTIMIZER_THEMES = 'v8light,v8dark';
|
||||
settings = getThemeSettings({ isDist: false });
|
||||
expect(settings['theme:darkMode'].value).toBe(false);
|
||||
expect(settings['theme:darkMode'].value).toBe('disabled');
|
||||
});
|
||||
|
||||
it('ignores the value when isDist is undefined', () => {
|
||||
process.env.KBN_OPTIMIZER_THEMES = 'v8dark';
|
||||
const settings = getThemeSettings({ isDist: undefined });
|
||||
expect(settings['theme:darkMode'].value).toBe(false);
|
||||
expect(settings['theme:darkMode'].value).toBe('disabled');
|
||||
});
|
||||
|
||||
it('ignores the value when isDist is true', () => {
|
||||
process.env.KBN_OPTIMIZER_THEMES = 'v8dark';
|
||||
const settings = getThemeSettings({ isDist: true });
|
||||
expect(settings['theme:darkMode'].value).toBe(false);
|
||||
expect(settings['theme:darkMode'].value).toBe('disabled');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,12 +46,35 @@ export const getThemeSettings = (
|
|||
name: i18n.translate('core.ui_settings.params.darkModeTitle', {
|
||||
defaultMessage: 'Dark mode',
|
||||
}),
|
||||
value: defaultDarkMode,
|
||||
value: defaultDarkMode ? 'enabled' : 'disabled',
|
||||
description: i18n.translate('core.ui_settings.params.darkModeText', {
|
||||
defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`,
|
||||
defaultMessage:
|
||||
`The UI theme that the Kibana UI should use. ` +
|
||||
`Set to 'enabled' or 'disabled' to enable or disable the dark theme. ` +
|
||||
`Set to 'system' to have the Kibana UI theme follow the system theme. ` +
|
||||
`A page refresh is required for the setting to be applied.`,
|
||||
}),
|
||||
type: 'select',
|
||||
options: ['enabled', 'disabled', 'system'],
|
||||
optionLabels: {
|
||||
enabled: i18n.translate('core.ui_settings.params.darkMode.options.enabled', {
|
||||
defaultMessage: `Enabled`,
|
||||
}),
|
||||
disabled: i18n.translate('core.ui_settings.params.darkMode.options.disabled', {
|
||||
defaultMessage: `Disabled`,
|
||||
}),
|
||||
system: i18n.translate('core.ui_settings.params.darkMode.options.system', {
|
||||
defaultMessage: `Sync with system`,
|
||||
}),
|
||||
},
|
||||
requiresPageReload: true,
|
||||
schema: schema.boolean(),
|
||||
schema: schema.oneOf([
|
||||
schema.literal('enabled'),
|
||||
schema.literal('disabled'),
|
||||
schema.literal('system'),
|
||||
// for backward-compatibility
|
||||
schema.boolean(),
|
||||
]),
|
||||
},
|
||||
/**
|
||||
* Theme is sticking around as there are still a number of places reading it and
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@kbn/logging",
|
||||
"@kbn/core-http-server-mocks",
|
||||
"@kbn/core-user-settings-server",
|
||||
"@kbn/core-ui-settings-common",
|
||||
],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { CoreContext } from '@kbn/core-base-server-internal';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { DarkModeValue } from '@kbn/core-ui-settings-common';
|
||||
import { UserProfileSettingsClientContract } from '@kbn/core-user-settings-server';
|
||||
|
||||
/**
|
||||
|
@ -16,7 +17,7 @@ import { UserProfileSettingsClientContract } from '@kbn/core-user-settings-serve
|
|||
*/
|
||||
export interface InternalUserSettingsServiceSetup {
|
||||
setUserProfileSettings: (client: UserProfileSettingsClientContract) => void;
|
||||
getUserSettingDarkMode: (request: KibanaRequest) => Promise<boolean | undefined>;
|
||||
getUserSettingDarkMode: (request: KibanaRequest) => Promise<DarkModeValue | undefined>;
|
||||
}
|
||||
|
||||
export class UserSettingsService {
|
||||
|
@ -52,7 +53,7 @@ export class UserSettingsService {
|
|||
|
||||
private async getUserSettingDarkMode(
|
||||
userSettings: Record<string, string>
|
||||
): Promise<boolean | undefined> {
|
||||
): Promise<DarkModeValue | undefined> {
|
||||
let result;
|
||||
|
||||
if (userSettings?.darkMode) {
|
||||
|
|
|
@ -8,4 +8,12 @@
|
|||
|
||||
export type { Theme } from './src/theme';
|
||||
|
||||
export { darkMode, euiDarkVars, euiLightVars, euiThemeVars, tag, version } from './src/theme';
|
||||
export {
|
||||
darkMode,
|
||||
tag,
|
||||
version,
|
||||
euiDarkVars,
|
||||
euiLightVars,
|
||||
euiThemeVars,
|
||||
_setDarkMode,
|
||||
} from './src/theme';
|
||||
|
|
|
@ -17,19 +17,26 @@ export type Theme = typeof v8Light;
|
|||
|
||||
// in the Kibana app we can rely on this global being defined, but in
|
||||
// some cases (like jest) the global is undefined
|
||||
/** @deprecated theme can be dynamic now, access is discouraged */
|
||||
export const tag: string = globals.__kbnThemeTag__ || 'v8light';
|
||||
/** @deprecated theme can be dynamic now, access is discouraged */
|
||||
export const version = 8;
|
||||
/** @deprecated theme can be dynamic now, access is discouraged */
|
||||
export const darkMode = tag.endsWith('dark');
|
||||
|
||||
export const euiLightVars: Theme = v8Light;
|
||||
export const euiDarkVars: Theme = v8Dark;
|
||||
|
||||
let isDarkMode = darkMode;
|
||||
export const _setDarkMode = (mode: boolean) => {
|
||||
isDarkMode = mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* EUI Theme vars that automatically adjust to light/dark theme
|
||||
*/
|
||||
export let euiThemeVars: Theme;
|
||||
if (darkMode) {
|
||||
euiThemeVars = euiDarkVars;
|
||||
} else {
|
||||
euiThemeVars = euiLightVars;
|
||||
}
|
||||
export const euiThemeVars: Theme = new Proxy(isDarkMode ? euiDarkVars : euiLightVars, {
|
||||
get(accessedTarget, accessedKey, ...rest) {
|
||||
return Reflect.get(isDarkMode ? euiDarkVars : euiLightVars, accessedKey, ...rest);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -481,6 +481,12 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"kibana_landing_page",
|
||||
],
|
||||
|
@ -529,6 +535,18 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"kibana_landing_page",
|
||||
],
|
||||
|
@ -568,6 +586,9 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -586,6 +607,14 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "kibana_landing_page",
|
||||
|
@ -650,6 +679,22 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "kibana_landing_page",
|
||||
|
@ -702,6 +747,10 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -791,6 +840,12 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"kibana_landing_page",
|
||||
],
|
||||
|
@ -839,6 +894,18 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"kibana_landing_page",
|
||||
],
|
||||
|
@ -878,6 +945,9 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
Array [
|
||||
"/app/integrations/browse",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
|
@ -896,6 +966,14 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "kibana_landing_page",
|
||||
|
@ -960,6 +1038,22 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "kibana_landing_page",
|
||||
|
@ -1012,6 +1106,10 @@ exports[`Overview renders correctly when there is no user data view 1`] = `
|
|||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "/app/integrations/browse",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
|
||||
|
||||
export const hasUserDataView = jest.fn();
|
||||
|
@ -45,6 +45,9 @@ jest.doMock('@kbn/kibana-react-plugin/public', () => ({
|
|||
},
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
theme$: of({ darkMode: false }),
|
||||
},
|
||||
},
|
||||
}),
|
||||
RedirectAppLinks: jest.fn((element: JSX.Element) => element),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { snakeCase } from 'lodash';
|
||||
import React, { FC, useState, useEffect } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import {
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
|
@ -64,14 +65,14 @@ export const Overview: FC<Props> = ({ newsFetchResult, solutions, features }) =>
|
|||
docLinks,
|
||||
dataViews,
|
||||
share,
|
||||
uiSettings,
|
||||
application,
|
||||
chrome,
|
||||
dataViewEditor,
|
||||
customBranding,
|
||||
theme,
|
||||
} = services;
|
||||
const addBasePath = http.basePath.prepend;
|
||||
const IS_DARK_THEME = uiSettings.get('theme:darkMode');
|
||||
const currentTheme = useObservable(theme.theme$, { darkMode: false });
|
||||
|
||||
// Home does not have a locator implemented, so hard-code it here.
|
||||
const addDataHref = addBasePath('/app/integrations/browse');
|
||||
|
@ -145,7 +146,7 @@ export const Overview: FC<Props> = ({ newsFetchResult, solutions, features }) =>
|
|||
}}
|
||||
image={addBasePath(
|
||||
`/plugins/${PLUGIN_ID}/assets/kibana_${appId}_${
|
||||
IS_DARK_THEME ? 'dark' : 'light'
|
||||
currentTheme.darkMode ? 'dark' : 'light'
|
||||
}.svg`
|
||||
)}
|
||||
title={app.title}
|
||||
|
|
|
@ -324,7 +324,7 @@ describe('useUserProfileForm', () => {
|
|||
const data: UserProfileData = {};
|
||||
|
||||
const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false });
|
||||
coreStart.settings.client.get.mockReturnValue(true);
|
||||
coreStart.theme.getTheme.mockReturnValue({ darkMode: true });
|
||||
coreStart.settings.client.isOverridden.mockReturnValue(true);
|
||||
|
||||
const testWrapper = mount(
|
||||
|
@ -361,7 +361,7 @@ describe('useUserProfileForm', () => {
|
|||
const data: UserProfileData = {};
|
||||
|
||||
const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false });
|
||||
coreStart.settings.client.get.mockReturnValue(false);
|
||||
coreStart.theme.getTheme.mockReturnValue({ darkMode: false });
|
||||
coreStart.settings.client.isOverridden.mockReturnValue(true);
|
||||
|
||||
const testWrapper = mount(
|
||||
|
|
|
@ -34,7 +34,7 @@ import type { FunctionComponent } from 'react';
|
|||
import React, { useRef, useState } from 'react';
|
||||
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||
|
||||
import type { CoreStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import type { CoreStart, IUiSettingsClient, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -656,7 +656,8 @@ export const UserProfile: FunctionComponent<UserProfileProps> = ({ user, data })
|
|||
const isCloudUser = user.elastic_cloud_user;
|
||||
|
||||
const { isThemeOverridden, isOverriddenThemeDarkMode } = determineIfThemeOverridden(
|
||||
services.settings.client
|
||||
services.settings.client,
|
||||
services.theme
|
||||
);
|
||||
|
||||
const rightSideItems = [
|
||||
|
@ -998,12 +999,15 @@ export const SaveChangesBottomBar: FunctionComponent = () => {
|
|||
);
|
||||
};
|
||||
|
||||
function determineIfThemeOverridden(settingsClient: IUiSettingsClient): {
|
||||
function determineIfThemeOverridden(
|
||||
settingsClient: IUiSettingsClient,
|
||||
theme: ThemeServiceStart
|
||||
): {
|
||||
isThemeOverridden: boolean;
|
||||
isOverriddenThemeDarkMode: boolean;
|
||||
} {
|
||||
return {
|
||||
isThemeOverridden: settingsClient.isOverridden('theme:darkMode'),
|
||||
isOverriddenThemeDarkMode: settingsClient.get<boolean>('theme:darkMode'),
|
||||
isOverriddenThemeDarkMode: theme.getTheme().darkMode,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -119,14 +119,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
shouldUseHashForSubUrl: false,
|
||||
});
|
||||
|
||||
let advancedSetting = await pageObjects.settings.getAdvancedSettingCheckbox(
|
||||
'theme:darkMode'
|
||||
);
|
||||
expect(advancedSetting).to.be(null);
|
||||
let advancedSetting = await pageObjects.settings.getAdvancedSettings('theme:darkMode');
|
||||
expect(advancedSetting).to.be('disabled');
|
||||
|
||||
await pageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode', true);
|
||||
advancedSetting = await pageObjects.settings.getAdvancedSettingCheckbox('theme:darkMode');
|
||||
expect(advancedSetting).to.be('true');
|
||||
await pageObjects.settings.setAdvancedSettingsSelect('theme:darkMode', 'enabled');
|
||||
advancedSetting = await pageObjects.settings.getAdvancedSettings('theme:darkMode');
|
||||
expect(advancedSetting).to.be('enabled');
|
||||
|
||||
await pageObjects.common.navigateToApp('security_account');
|
||||
|
||||
|
@ -152,9 +150,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
shouldUseHashForSubUrl: false,
|
||||
});
|
||||
|
||||
await pageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode', false);
|
||||
advancedSetting = await pageObjects.settings.getAdvancedSettingCheckbox('theme:darkMode');
|
||||
expect(advancedSetting).to.be(null);
|
||||
await pageObjects.settings.setAdvancedSettingsSelect('theme:darkMode', 'disabled');
|
||||
advancedSetting = await pageObjects.settings.getAdvancedSettings('theme:darkMode');
|
||||
expect(advancedSetting).to.be('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue