Implement global dark theme (#28445)

* [scss] build a light/dark theme for each style sheet, only include correct stylesheet

* Created dark theme Bootstrap

Right now it’s importing both dark and light theme, but light is last so it’s overriding. Not sure how to only import the one or the other.

* migrate light/dark style to unique webpack bundle

* [build] fix log messaage about built sass

* [build/notice] deduplicate notices

* [uiRender/bootstrap] make try auth if necessary

* [visualize/editor] set reversed prop

* remove unnecessary change

* [styles] show toast when theme:darkMode is changed

* [styles] update copy

* [uiRender] default dark mode to false
This commit is contained in:
Spencer 2019-01-24 17:26:23 -08:00 committed by GitHub
parent 5403c46ce4
commit 879025e603
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 410 additions and 207 deletions

View file

@ -35,7 +35,7 @@ export const TranspileScssTask = {
try {
const bundles = await buildAll(uiExports.styleSheetPaths, log, build.resolvePath('built_assets/css'));
bundles.forEach(bundle => log.info(`Compiled SCSS: ${bundle.source}`));
bundles.forEach(bundle => log.info(`Compiled SCSS: ${bundle.sourcePath} (theme=${bundle.theme})`));
} catch (error) {
const { message, line, file } = error;
throw new Error(`${message} on line ${line} of ${file}`);

View file

@ -64,7 +64,9 @@ export async function generateNoticeFromSource({ productName, directory, log })
let match;
while ((match = NOTICE_COMMENT_RE.exec(source)) !== null) {
log.info(`Found @notice comment in ${file.relative}`);
noticeComments.push(match[1]);
if (!noticeComments.includes(match[1])) {
noticeComments.push(match[1]);
}
}
})
.on('error', reject)

View file

@ -823,6 +823,15 @@ export function getUiSettingDefaults() {
},
}),
},
'theme:darkMode': {
name: i18n.translate('kbn.advancedSettings.darkModeTitle', {
defaultMessage: 'Dark mode',
}),
value: false,
description: i18n.translate('kbn.advancedSettings.darkModeText', {
defaultMessage: `Enable a dark mode for the Kibana UI. A page refresh is required for the setting to be applied.`,
}),
},
'filters:pinnedByDefault': {
name: i18n.translate('kbn.advancedSettings.pinFiltersTitle', {
defaultMessage: 'Pin filters by default',

View file

@ -31,6 +31,8 @@ import { fetchFields } from '../lib/fetch_fields';
import chrome from 'ui/chrome';
import { I18nProvider } from '@kbn/i18n/react';
const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode');
class VisEditor extends Component {
constructor(props) {
super(props);
@ -117,7 +119,7 @@ class VisEditor extends Component {
<I18nProvider>
<Visualization
dateFormat={this.props.config.get('dateFormat')}
reversed={false}
reversed={IS_DARK_THEME}
onBrush={this.onBrush}
onUiState={this.handleUiState}
uiState={this.props.vis.getUiState()}

View file

@ -44,6 +44,16 @@ const STATS_WARNINGS_FILTER = new RegExp([
'|(chunk .* \\[mini-css-extract-plugin\\]\\\nConflicting order between:)'
].join(''));
function recursiveIssuer(m) {
if (m.issuer) {
return recursiveIssuer(m.issuer);
} else if (m.name) {
return m.name;
} else {
return false;
}
}
export default class BaseOptimizer {
constructor(opts) {
this.logWithMetadata = opts.logWithMetadata || (() => null);
@ -239,7 +249,15 @@ export default class BaseOptimizer {
node: { fs: 'empty' },
context: fromRoot('.'),
cache: true,
entry: this.uiBundles.toWebpackEntries(),
entry: {
...this.uiBundles.toWebpackEntries(),
light_theme: [
require.resolve('../ui/public/styles/bootstrap_light.less'),
],
dark_theme: [
require.resolve('../ui/public/styles/bootstrap_dark.less'),
],
},
devtool: this.sourceMaps,
profile: this.profile || false,
@ -257,9 +275,21 @@ export default class BaseOptimizer {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
chunks: chunk => chunk.canBeInitial() && chunk.name !== 'light_theme' && chunk.name !== 'dark_theme',
minChunks: 2,
reuseExistingChunk: true
},
light_theme: {
name: 'light_theme',
test: m => m.constructor.name === 'CssModule' && recursiveIssuer(m) === 'light_theme',
chunks: 'all',
enforce: true
},
dark_theme: {
name: 'dark_theme',
test: m => m.constructor.name === 'CssModule' && recursiveIssuer(m) === 'dark_theme',
chunks: 'all',
enforce: true
}
}
},

View file

@ -1,5 +1,8 @@
@import 'ui/public/styles/styling_constants';
foo {
bar {
display: flex;
background: $euiFocusBackgroundColor;
}
}

View file

@ -29,18 +29,26 @@ const renderSass = promisify(sass.render);
const writeFile = promisify(fs.writeFile);
const mkdirpAsync = promisify(mkdirp);
const DARK_THEME_IMPORTER = (url) => {
if (url.includes('k6_colors_light')) {
return { file: url.replace('k6_colors_light', 'k6_colors_dark') };
}
return { file: url };
};
export class Build {
constructor(source, log, targetPath) {
this.source = source;
constructor({ sourcePath, log, targetPath, theme }) {
this.sourcePath = sourcePath;
this.log = log;
this.targetPath = targetPath;
this.includedFiles = [source];
this.theme = theme;
this.includedFiles = [sourcePath];
}
/**
* Glob based on source path
*/
async buildIfIncluded(path) {
if (this.includedFiles && this.includedFiles.includes(path)) {
await this.build();
@ -56,14 +64,15 @@ export class Build {
async build() {
const rendered = await renderSass({
file: this.source,
file: this.sourcePath,
outFile: this.targetPath,
sourceMap: true,
sourceMapEmbed: true,
includePaths: [
path.resolve(__dirname, '../..'),
path.resolve(__dirname, '../../../node_modules')
]
path.resolve(__dirname, '../../../node_modules'),
],
importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined
});
const prefixed = postcss([ autoprefixer ]).process(rendered.css);

View file

@ -31,21 +31,54 @@ afterEach(async () => {
await del(TMP);
});
it('builds SASS', async () => {
const cssPath = resolve(TMP, 'style.css');
await (new Build(FIXTURE, {
info: () => {},
warn: () => {},
error: () => {},
}, cssPath)).build();
it('builds light themed SASS', async () => {
const targetPath = resolve(TMP, 'style.css');
await new Build({
sourcePath: FIXTURE,
log: {
info: () => {},
warn: () => {},
error: () => {},
},
theme: 'light',
targetPath
}).build();
expect(readFileSync(cssPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2'))
.toMatchInlineSnapshot(`
expect(
readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')
).toMatchInlineSnapshot(`
"foo bar {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex; }
display: flex;
background: #e6f0f8; }
/*# sourceMappingURL=... */"
`);
});
it('builds dark themed SASS', async () => {
const targetPath = resolve(TMP, 'style.css');
await new Build({
sourcePath: FIXTURE,
log: {
info: () => {},
warn: () => {},
error: () => {},
},
theme: 'dark',
targetPath
}).build();
expect(
readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')
).toMatchInlineSnapshot(`
"foo bar {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background: #191919; }
/*# sourceMappingURL=... */"
`);
});

View file

@ -28,7 +28,12 @@ export async function buildAll(styleSheets, log, buildDir) {
return;
}
const bundle = new Build(styleSheet.localPath, log, resolve(buildDir, styleSheet.publicPath));
const bundle = new Build({
sourcePath: styleSheet.localPath,
log,
theme: styleSheet.theme,
targetPath: resolve(buildDir, styleSheet.publicPath),
});
await bundle.build();
return bundle;

View file

@ -49,7 +49,7 @@ export async function sassMixin(kbnServer, server, config) {
scssBundles.forEach(bundle => {
bundle.includedFiles.forEach(file => trackedFiles.add(file));
server.log(['info', 'scss'], `Compiled CSS: ${bundle.source}`);
server.log(['info', 'scss'], `Compiled CSS: ${bundle.sourcePath} (theme=${bundle.theme})`);
});
} catch(error) {
const { message, line, file } = error;

View file

@ -17,25 +17,23 @@
* under the License.
*/
const theme = require('../theme');
// Kibana UI Framework
require('@kbn/ui-framework/dist/kui_light.css');
// Elastic UI Framework, light theme
const euiThemeLight = require('!!raw-loader!@elastic/eui/dist/eui_theme_k6_light.css');
theme.registerTheme('light', euiThemeLight);
// Elastic UI Framework, dark theme
const euiThemeDark = require('!!raw-loader!@elastic/eui/dist/eui_theme_k6_dark.css');
theme.registerTheme('dark', euiThemeDark);
// Set default theme.
theme.applyTheme('light');
import chrome from 'ui/chrome';
import { filter } from 'rxjs/operators';
import { toastNotifications } from 'ui/notify';
import { i18n } from '@kbn/i18n';
// All Kibana styles inside of the /styles dir
const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.)[^\/\\]+\.less/);
const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.|bootstrap_(light|dark))[^\/\\]+\.less/);
context.keys().forEach(key => context(key));
// manually require non-less files
require('../styles/disable_animations');
import '../styles/disable_animations';
chrome.getUiSettingsClient()
.getUpdate$()
.pipe(filter(update => update.key === 'theme:darkMode'))
.subscribe(() => {
toastNotifications.addSuccess(i18n.translate('common.ui.styles.themeAppliedToast', {
defaultMessage: 'Theme applied, please refresh your browser to take affect.'
}));
});

View file

@ -0,0 +1,20 @@
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@white: #000;
@blue: #4DA1C0;
@brand-primary: #F5F5F5;
@brand-success: #017D73;
@brand-info: @blue;
@brand-warning: #C06C4C;
@brand-danger: #BF4D4D;
@gray-base: #FFF;
@gray-darker: #F5F5F5;
@gray-dark: #ababab;
@gray5: #8A8A8A;
@gray: #444;
@gray-light: darken(#444, 9%);
@gray-lighter: #333;
@gray-lightest: #242424;

View file

@ -0,0 +1,20 @@
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@white: #FFF;
@blue: #006BB4;
@brand-primary: #343741;
@brand-success: #017D73;
@brand-info: @blue;
@brand-warning: #F5A700;
@brand-danger: #BD271E;
@gray-base: #000;
@gray-darker: #343741;
@gray-dark: #7b7b7b;
@gray5: #69707D;
@gray: #98A2B3;
@gray-light: lighten(#98A2B3, 9%); // ~#b4b4b4
@gray-lighter: #D3DAE6;
@gray-lightest: #F5F7FA;

View file

@ -1,26 +1,3 @@
@import "variables.less";
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@white: #FFF;
@blue: #006BB4;
@brand-primary: #343741;
@brand-success: #017D73;
@brand-info: @blue;
@brand-warning: #F5A700;
@brand-danger: #BD271E;
@gray-base: #000;
@gray-darker: #343741;
@gray-dark: #7b7b7b;
@gray5: #69707D;
@gray: #98A2B3;
@gray-light: lighten(#98A2B3, 9%); // ~#b4b4b4
@gray-lighter: #D3DAE6;
@gray-lightest: #F5F7FA;
//== Scaffolding
//
// ## Settings for some of the most global styles.

View file

@ -36,6 +36,8 @@
// There's an audit in the comments to cover what's left to remove.
// Core variables and mixins
@import "variables.less";
@import "_colors_dark.less";
@import "_custom_variables.less";
@import "mixins.less";

View file

@ -0,0 +1,76 @@
/*!
* Bootstrap v3.3.6 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/* @notice
* This product bundles bootstrap@3.3.6 which is available under a
* "MIT" license.
*
* The MIT License (MIT)
*
* Copyright (c) 2011-2015 Twitter, Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// PLANNED FOR REMOVAL
// We are trying to remove bootstrap. The below are the files we're currently using.
// There's an audit in the comments to cover what's left to remove.
// Core variables and mixins
@import "variables.less";
@import "_colors_light.less";
@import "_custom_variables.less";
@import "mixins.less";
// Hard to remove, used in many places
@import "grid.less"; // Used in a lot of places
@import "tables.less"; // Used in a lot of places
@import "forms.less"; // Used in a lot of places
// Easy to remove
@import "type.less"; // Can be search / replaced with EUI
@import "component-animations.less"; // Used in angular bootstrap
@import "buttons.less";
@import "navbar.less"; // Used in Graph
@import "close.less"; // Only in angular-bootstrap
@import "modals.less"; // Only in angular-bootstrap
@import "progress-bars.less"; // Used in ML, angular-bootstrap
@import "list-group.less"; // Used in Timelion, Graph
@import "navs.less"; // Used in ML
@import "alerts.less"; // Only in angular-bootstrap
@import "tooltip.less"; // Only in angular-bootstrap
@import "responsive-utilities.less"; // Minimal usage
// Decent usage in multiple areas
@import "dropdowns.less"; // Used in console, datepicker, watcher
@import "input-groups.less"; // Used in ML, typeahead, reporting, graph
@import "pagination.less";
@import "pager.less";
@import "labels.less"; // Hard to judge usage because of generic selector names
@import "panels.less"; // Used in ML, dashboards, notify, angular-bootstrap
@import "popovers.less"; // Hard to judge usage because of generic selector names
// Utility classes
@import "utilities.less";
// OVERRIDES
@import "_overrides.less";

View file

@ -1,7 +1,8 @@
//
// COMPILER FOR BOOTSTRAP
@import "~ui/styles/bootstrap/bootstrap";
@import "~ui/styles/bootstrap/bootstrap_dark";
// Components -- waiting on EUI conversion
@import "~ui/filter_bar/filter_bar";
@import "~ui/timepicker/timepicker";

View file

@ -0,0 +1,8 @@
//
// COMPILER FOR BOOTSTRAP
@import "~ui/styles/bootstrap/bootstrap_light";
// Components -- waiting on EUI conversion
@import "~ui/filter_bar/filter_bar";
@import "~ui/timepicker/timepicker";

View file

@ -1,24 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export {
registerTheme,
applyTheme,
getCurrentTheme,
} from './theme';

View file

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const themes = {};
let currentTheme = undefined;
export function registerTheme(theme, styles) {
themes[theme] = styles;
}
export function applyTheme(newTheme) {
currentTheme = newTheme;
const styleNode = document.getElementById('themeCss');
if (styleNode) {
const css = themes[currentTheme];
styleNode.textContent = css;
}
}
export function getCurrentTheme() {
return currentTheme;
}

View file

@ -27,7 +27,6 @@ import { relativeOptions } from './relative_options';
import { parseRelativeParts } from './parse_relative_parts';
import dateMath from '@elastic/datemath';
import moment from 'moment';
import './timepicker.less';
import '../directives/input_datetime';
import '../directives/inequality';
import './refresh_intervals';

View file

@ -1,5 +1,3 @@
@import (reference) '../styles/bootstrap/_custom_variables.less';
.kbn-timepicker {
display: flex;
justify-content: flex-end;

View file

@ -24,6 +24,27 @@ import { mapSpec, wrap } from './modify_reduce';
const OK_EXTNAMES = ['.css', '.scss'];
function getPublicPath(pluginSpec, localPath) {
// get the path of the stylesheet relative to the public dir for the plugin
let relativePath = path.relative(pluginSpec.getPublicDir(), localPath);
// replace back slashes on windows
relativePath = relativePath.split('\\').join('/');
return `plugins/${pluginSpec.getId()}/${relativePath}`;
}
function getStyleSheetPath(pluginSpec, localPath, theme) {
const extname = path.extname(localPath);
const localCssPath = localPath.slice(0, -extname.length) + `.${theme}.css`;
return {
theme,
localPath: existsSync(localCssPath) ? localCssPath : localPath,
publicPath: getPublicPath(pluginSpec, localCssPath),
};
}
function normalize(localPath, type, pluginSpec) {
const pluginId = pluginSpec.getId();
const publicDir = path.normalize(pluginSpec.getPublicDir());
@ -47,28 +68,20 @@ function normalize(localPath, type, pluginSpec) {
);
}
// replace the extension of localPath to be .css
// publicPath will always point to the css file
const localCssPath = localPath.slice(0, -extname.length) + '.css';
// update localPath to point to the .css file if it exists and
// the .scss path does not, which is the case for built plugins
if (extname === '.scss' && !existsSync(localPath) && existsSync(localCssPath)) {
localPath = localCssPath;
if (extname === '.css') {
// when the localPath points to a css file, assume it will be included in every theme
// and don't create ligkt/dark variations of it
return {
theme: '*',
localPath: localPath,
publicPath: getPublicPath(pluginSpec, localPath)
};
}
// get the path of the stylesheet relative to the public dir for the plugin
let relativePath = path.relative(publicDir, localCssPath);
// replace back slashes on windows
relativePath = relativePath.split('\\').join('/');
const publicPath = `plugins/${pluginSpec.getId()}/${relativePath}`;
return {
localPath,
publicPath
};
return [
getStyleSheetPath(pluginSpec, localPath, 'light'),
getStyleSheetPath(pluginSpec, localPath, 'dark'),
];
}
export const styleSheetPaths = wrap(mapSpec(normalize), flatConcatAtType);

View file

@ -61,7 +61,13 @@ describe('uiExports.styleSheetPaths', () => {
Array [
Object {
"localPath": <absolute>/kibana/public/bar.scss,
"publicPath": "plugins/test/bar.css",
"publicPath": "plugins/test/bar.light.css",
"theme": "light",
},
Object {
"localPath": <absolute>/kibana/public/bar.scss,
"publicPath": "plugins/test/bar.dark.css",
"theme": "dark",
},
]
`);
@ -75,7 +81,13 @@ Array [
Array [
Object {
"localPath": <absolute>/kibana/public/bar.scss,
"publicPath": "plugins/test/bar.css",
"publicPath": "plugins/test/bar.light.css",
"theme": "light",
},
Object {
"localPath": <absolute>/kibana/public/bar.scss,
"publicPath": "plugins/test/bar.dark.css",
"theme": "dark",
},
]
`);
@ -89,7 +101,13 @@ Array [
Array [
Object {
"localPath": <absolute>/kibana/public\\bar.scss,
"publicPath": "plugins/test/../public/bar.css",
"publicPath": "plugins/test/../public/bar.light.css",
"theme": "light",
},
Object {
"localPath": <absolute>/kibana/public\\bar.scss,
"publicPath": "plugins/test/../public/bar.dark.css",
"theme": "dark",
},
]
`);

View file

@ -20,6 +20,7 @@
import { props, reduce as reduceAsync } from 'bluebird';
import Boom from 'boom';
import { resolve } from 'path';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { AppBootstrap } from './bootstrap';
import { mergeVariables } from './lib';
@ -53,51 +54,81 @@ export function uiRenderMixin(kbnServer, server, config) {
// expose built css
server.exposeStaticDir('/built_assets/css/{path*}', fromRoot('built_assets/css'));
server.exposeStaticDir('/node_modules/@elastic/eui/dist/{path*}', fromRoot('node_modules/@elastic/eui/dist'));
server.exposeStaticDir('/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist'));
server.route({
path: '/bundles/app/{id}/bootstrap.js',
method: 'GET',
config: { auth: false },
async handler(request, h) {
const { id } = request.params;
const app = server.getUiAppById(id) || server.getHiddenUiAppById(id);
if (!app) {
throw Boom.notFound(`Unknown app: ${id}`);
}
// register the bootstrap.js route after plugins are initialized so that we can
// detect if any default auth strategies were registered
kbnServer.afterPluginsInit(() => {
const authEnabled = !!server.auth.settings.default;
const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/built_assets/dlls`;
const styleSheetPaths = [
`${dllBundlePath}/vendors.style.dll.css`,
`${regularBundlePath}/commons.style.css`,
`${regularBundlePath}/${app.getId()}.style.css`,
...kbnServer.uiExports.styleSheetPaths
.map(path => (
path.localPath.endsWith('.scss')
? `${basePath}/built_assets/css/${path.publicPath}`
: `${basePath}/${path.publicPath}`
))
.reverse()
];
const bootstrap = new AppBootstrap({
templateData: {
appId: app.getId(),
regularBundlePath,
dllBundlePath,
styleSheetPaths,
server.route({
path: '/bundles/app/{id}/bootstrap.js',
method: 'GET',
config: {
tags: ['api'],
auth: authEnabled ? { mode: 'try' } : false,
},
async handler(request, h) {
const { id } = request.params;
const app = server.getUiAppById(id) || server.getHiddenUiAppById(id);
if (!app) {
throw Boom.notFound(`Unknown app: ${id}`);
}
});
const body = await bootstrap.getJsFile();
const etag = await bootstrap.getJsFileHash();
const uiSettings = request.getUiSettingsService();
const darkMode = !authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:darkMode')
: false;
return h.response(body)
.header('cache-control', 'must-revalidate')
.header('content-type', 'application/javascript')
.etag(etag);
}
const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/built_assets/dlls`;
const styleSheetPaths = [
`${dllBundlePath}/vendors.style.dll.css`,
...(
darkMode ?
[
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_k6_dark.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
] : [
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_k6_light.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
]
),
`${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`,
`${regularBundlePath}/commons.style.css`,
`${regularBundlePath}/${app.getId()}.style.css`,
...kbnServer.uiExports.styleSheetPaths
.filter(path => (
path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')
))
.map(path => (
path.localPath.endsWith('.scss')
? `${basePath}/built_assets/css/${path.publicPath}`
: `${basePath}/${path.publicPath}`
))
.reverse()
];
const bootstrap = new AppBootstrap({
templateData: {
appId: app.getId(),
regularBundlePath,
dllBundlePath,
styleSheetPaths,
}
});
const body = await bootstrap.getJsFile();
const etag = await bootstrap.getJsFileHash();
return h.response(body)
.header('cache-control', 'must-revalidate')
.header('content-type', 'application/javascript')
.etag(etag);
}
});
});
server.route({
@ -147,11 +178,20 @@ export function uiRenderMixin(kbnServer, server, config) {
const translations = await server.getUiTranslations();
const basePath = request.getBasePath();
const legacyMetadata = await getLegacyKibanaPayload({
app,
translations,
request,
includeUserProvidedConfig,
injectedVarsOverrides
});
return h.view('ui_app', {
uiPublicUrl: `${basePath}/ui`,
bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`,
i18n: (id, options) => i18n.translate(id, options),
locale: i18n.getLocale(),
darkMode: get(legacyMetadata.uiSettings.user, ['theme:darkMode', 'userValue'], false),
injectedMetadata: {
version: kbnServer.version,
@ -166,13 +206,7 @@ export function uiRenderMixin(kbnServer, server, config) {
),
),
legacyMetadata: await getLegacyKibanaPayload({
app,
translations,
request,
includeUserProvidedConfig,
injectedVarsOverrides
}),
legacyMetadata,
},
});
}

View file

@ -113,10 +113,6 @@ html(lang=locale)
block head
//- Load EUI component styles here. Kibana's styles are loaded afterwards by webpack, which is
//- good because we may use them to override EUI styles.
style#themeCss
body
kbn-injected-metadata(data=JSON.stringify(injectedMetadata))
block content

View file

@ -13,7 +13,7 @@ block content
background-color: #F5F7FA;
}
.kibanaWelcomeView {
background-color: #F5F7FA;
background-color: #{darkMode ? '#242424' : '#F5F7FA'};
}
.kibanaWelcomeText {

View file

@ -24,6 +24,8 @@ export default function ({ getService, getPageObjects }) {
const log = getService('log');
const inspector = getService('inspector');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'visualBuilder']);
describe('visual builder', function describeIndexTests() {
@ -240,5 +242,17 @@ export default function ({ getService, getPageObjects }) {
expect(newValue).to.eql('10');
});
});
describe('dark mode', () => {
it('uses dark mode flag', async () => {
await kibanaServer.uiSettings.update({
'theme:darkMode': true
});
await PageObjects.visualBuilder.resetPage();
const classNames = await testSubjects.getAttribute('timeseriesChart', 'class');
expect(classNames.includes('reversed')).to.be(true);
});
});
});
}