mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
5403c46ce4
commit
879025e603
28 changed files with 410 additions and 207 deletions
|
@ -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}`);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
@import 'ui/public/styles/styling_constants';
|
||||
|
||||
foo {
|
||||
bar {
|
||||
display: flex;
|
||||
background: $euiFocusBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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=... */"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.'
|
||||
}));
|
||||
});
|
||||
|
|
20
src/ui/public/styles/bootstrap/_colors_dark.less
Normal file
20
src/ui/public/styles/bootstrap/_colors_dark.less
Normal 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;
|
20
src/ui/public/styles/bootstrap/_colors_light.less
Normal file
20
src/ui/public/styles/bootstrap/_colors_light.less
Normal 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;
|
|
@ -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.
|
||||
|
|
|
@ -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";
|
||||
|
76
src/ui/public/styles/bootstrap/bootstrap_light.less
vendored
Normal file
76
src/ui/public/styles/bootstrap/bootstrap_light.less
vendored
Normal 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";
|
|
@ -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";
|
8
src/ui/public/styles/bootstrap_light.less
vendored
Normal file
8
src/ui/public/styles/bootstrap_light.less
vendored
Normal 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";
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
@import (reference) '../styles/bootstrap/_custom_variables.less';
|
||||
|
||||
.kbn-timepicker {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,7 @@ block content
|
|||
background-color: #F5F7FA;
|
||||
}
|
||||
.kibanaWelcomeView {
|
||||
background-color: #F5F7FA;
|
||||
background-color: #{darkMode ? '#242424' : '#F5F7FA'};
|
||||
}
|
||||
|
||||
.kibanaWelcomeText {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue