Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-01-10 08:38:03 -08:00
commit e7f9090060
107 changed files with 744 additions and 357 deletions

View file

@ -5,7 +5,7 @@ bower_components
/.es /.es
/plugins /plugins
/optimize /optimize
/dlls /built_assets
/src/fixtures/vislib/mock_data /src/fixtures/vislib/mock_data
/src/ui/public/angular-bootstrap /src/ui/public/angular-bootstrap
/src/ui/public/flot-charts /src/ui/public/flot-charts

3
.gitignore vendored
View file

@ -9,7 +9,7 @@ node_modules
!/src/dev/notice/__fixtures__/node_modules !/src/dev/notice/__fixtures__/node_modules
trash trash
/optimize /optimize
/dlls /built_assets
target target
/build /build
.jruby .jruby
@ -44,4 +44,3 @@ package-lock.json
*.sublime-* *.sublime-*
npm-debug.log* npm-debug.log*
.tern-project .tern-project
**/public/index.css

View file

@ -39,6 +39,7 @@ document.
`discover:sort:defaultOrder`:: Controls the default sort direction for time based index patterns in the Discover app. `discover:sort:defaultOrder`:: Controls the default sort direction for time based index patterns in the Discover app.
`doc_table:highlight`:: Highlight results in Discover and Saved Searches Dashboard. Highlighting makes request slow when `doc_table:highlight`:: Highlight results in Discover and Saved Searches Dashboard. Highlighting makes request slow when
working on big documents. Set this property to `false` to disable highlighting. working on big documents. Set this property to `false` to disable highlighting.
`doc_table:hideTimeColumn`:: Hide the 'Time' column in Discover and in all Saved Searches on Dashboards.
`search:includeFrozen`:: Will include {ref}/frozen-indices.html[frozen indices] in results if enabled. Searching through frozen indices `search:includeFrozen`:: Will include {ref}/frozen-indices.html[frozen indices] in results if enabled. Searching through frozen indices
might increase the search time. might increase the search time.
`courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to `courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to

View file

@ -94,13 +94,13 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless
=== Creating a Scripted Field === Creating a Scripted Field
To create a scripted field: To create a scripted field:
. Go to *Settings > Indices* . Go to *Management > Kibana > Index Patterns*
. Select the index pattern you want to add a scripted field to. . Select the index pattern you want to add a scripted field to.
. Go to the pattern's *Scripted Fields* tab. . Go to the pattern's *Scripted fields* tab.
. Click *Add Scripted Field*. . Click *Add scripted field*.
. Enter a name for the scripted field. . Enter a name for the scripted field.
. Enter the expression that you want to use to compute a value on the fly from your index data. . Enter the expression that you want to use to compute a value on the fly from your index data.
. Click *Save Scripted Field*. . Click *Create field*.
For more information about scripted fields in Elasticsearch, see For more information about scripted fields in Elasticsearch, see
{ref}/modules-scripting.html[Scripting]. {ref}/modules-scripting.html[Scripting].
@ -110,9 +110,10 @@ For more information about scripted fields in Elasticsearch, see
=== Updating a Scripted Field === Updating a Scripted Field
To modify a scripted field: To modify a scripted field:
. Go to *Settings > Indices* . Go to *Management > Kibana > Index Patterns*
. Click the index pattern's *Scripted fields* tab.
. Click the *Edit* button for the scripted field you want to change. . Click the *Edit* button for the scripted field you want to change.
. Make your changes and then click *Save Scripted Field* to update the field. . Make your changes and then click *Save field* to update the field.
WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get
exceptions whenever you try to view the dynamically generated data. exceptions whenever you try to view the dynamically generated data.
@ -122,6 +123,7 @@ exceptions whenever you try to view the dynamically generated data.
=== Deleting a Scripted Field === Deleting a Scripted Field
To delete a scripted field: To delete a scripted field:
. Go to *Settings > Indices* . Go to *Management > Kibana > Index Patterns*
. Click the index pattern's *Scripted fields* tab.
. Click the *Delete* button for the scripted field you want to remove. . Click the *Delete* button for the scripted field you want to remove.
. Confirm that you really want to delete the field. . Click *Delete* in the confirmation window.

View file

@ -27,7 +27,7 @@
"extraPatterns": [ "extraPatterns": [
"build", "build",
"optimize", "optimize",
"dlls", "built_assets",
".eslintcache" ".eslintcache"
] ]
} }

View file

@ -64,7 +64,7 @@ export const CleanClientModulesOnDLLTask = {
]; ];
// Resolve the client vendors dll manifest path // Resolve the client vendors dll manifest path
const dllManifestPath = `${baseDir}/dlls/vendors.manifest.dll.json`; const dllManifestPath = `${baseDir}/built_assets/dlls/vendors.manifest.dll.json`;
// Get dll entries filtering out the ones // Get dll entries filtering out the ones
// from any whitelisted module // from any whitelisted module

View file

@ -34,7 +34,7 @@ export const TranspileScssTask = {
const uiExports = collectUiExports(enabledPlugins); const uiExports = collectUiExports(enabledPlugins);
try { try {
const bundles = await buildAll(uiExports.styleSheetPaths, log); 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.source}`));
} catch (error) { } catch (error) {
const { message, line, file } = error; const { message, line, file } = error;

View file

@ -42,7 +42,7 @@ export async function generateNoticeFromSource({ productName, directory, log })
cwd: directory, cwd: directory,
nodir: true, nodir: true,
ignore: [ ignore: [
'{node_modules,build,target,dist,optimize,dlls}/**', '{node_modules,build,target,dist,optimize,built_assets}/**',
'packages/*/{node_modules,build,target,dist}/**', 'packages/*/{node_modules,build,target,dist}/**',
'x-pack/{node_modules,build,target,dist,optimize}/**', 'x-pack/{node_modules,build,target,dist,optimize}/**',
'x-pack/packages/*/{node_modules,build,target,dist}/**', 'x-pack/packages/*/{node_modules,build,target,dist}/**',

View file

@ -380,7 +380,8 @@ function discoverController(
} }
const timeFieldName = $scope.indexPattern.timeFieldName; const timeFieldName = $scope.indexPattern.timeFieldName;
const fields = timeFieldName ? [timeFieldName, ...selectedFields] : selectedFields; const hideTimeColumn = config.get('doc_table:hideTimeColumn');
const fields = (timeFieldName && !hideTimeColumn) ? [timeFieldName, ...selectedFields] : selectedFields;
return { return {
searchFields: fields, searchFields: fields,
selectFields: fields selectFields: fields

View file

@ -158,11 +158,12 @@ export function onPremInstructions(apmIndexPattern) {
index: apmIndexPattern, index: apmIndexPattern,
query: { query: {
bool: { bool: {
filter: { should: [
exists: { { term: { 'processor.name': 'error' } },
field: 'processor.name', { term: { 'processor.name': 'transaction' } },
}, { term: { 'processor.name': 'metric' } },
}, { term: { 'processor.name': 'sourcemap' } },
],
}, },
}, },
}, },

View file

@ -269,6 +269,16 @@ export function getUiSettingDefaults() {
}), }),
category: ['discover'], category: ['discover'],
}, },
'doc_table:hideTimeColumn': {
name: i18n.translate('kbn.advancedSettings.docTableHideTimeColumnTitle', {
defaultMessage: 'Hide \'Time\' column',
}),
value: false,
description: i18n.translate('kbn.advancedSettings.docTableHideTimeColumnText', {
defaultMessage: 'Hide the \'Time\' column in Discover and in all Saved Searches on Dashboards.',
}),
category: ['discover'],
},
'courier:maxSegmentCount': { 'courier:maxSegmentCount': {
name: i18n.translate('kbn.advancedSettings.courier.maxSegmentCountTitle', { name: i18n.translate('kbn.advancedSettings.courier.maxSegmentCountTitle', {
defaultMessage: 'Maximum segment count', defaultMessage: 'Maximum segment count',

View file

@ -70,7 +70,7 @@ export default (kibana) => {
} }
testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`);
testGlobs.push(`${plugin.publicDir}/**/*.css`); testGlobs.push(`built_assets/css/plugins/${plugin.id}/**/*.css`);
}); });
} else { } else {
// add the modules from all of the apps // add the modules from all of the apps
@ -80,7 +80,7 @@ export default (kibana) => {
for (const plugin of plugins) { for (const plugin of plugins) {
testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`);
testGlobs.push(`${plugin.publicDir}/**/*.css`); testGlobs.push(`built_assets/css/plugins/${plugin.id}/**/*.css`);
} }
} }

View file

@ -185,7 +185,7 @@ export class VegaParser {
delete this.spec.width; delete this.spec.width;
delete this.spec.height; delete this.spec.height;
} else { } else {
this._onWarning(i18n.translate('vega.vegaParser.widthAndHeightParamsAreIngroredWithAutosizeFitWarningMessage', { this._onWarning(i18n.translate('vega.vegaParser.widthAndHeightParamsAreIgnoredWithAutosizeFitWarningMessage', {
defaultMessage: 'The {widthParam} and {heightParam} params are ignored with {autosizeParam}', defaultMessage: 'The {widthParam} and {heightParam} params are ignored with {autosizeParam}',
values: { values: {
autosizeParam: 'autosize=fit', autosizeParam: 'autosize=fit',

View file

@ -42,7 +42,7 @@ VisTypesRegistryProvider.register((Private) => {
return VisFactory.createBaseVisualization({ return VisFactory.createBaseVisualization({
name: 'vega', name: 'vega',
title: 'Vega', title: 'Vega',
description: i18n.translate('vega.type.vegaВescription', { description: i18n.translate('vega.type.vegaDescription', {
defaultMessage: 'Create custom visualizations using Vega and Vega-Lite', defaultMessage: 'Create custom visualizations using Vega and Vega-Lite',
description: 'Vega and Vega-Lite are product names and should not be translated', description: 'Vega and Vega-Lite are product names and should not be translated',
}), }),

View file

@ -59,7 +59,7 @@ export function createBundlesRoute({ regularBundlesPath, dllBundlesPath, basePub
return [ return [
buildRouteForBundles(basePublicPath, '/bundles/', regularBundlesPath, fileHashCache), buildRouteForBundles(basePublicPath, '/bundles/', regularBundlesPath, fileHashCache),
buildRouteForBundles(basePublicPath, '/dlls/', dllBundlesPath, fileHashCache), buildRouteForBundles(basePublicPath, '/built_assets/dlls/', dllBundlesPath, fileHashCache),
]; ];
} }

View file

@ -20,7 +20,7 @@
export function createProxyBundlesRoute({ host, port }) { export function createProxyBundlesRoute({ host, port }) {
return [ return [
buildProxyRouteForBundles('/bundles/', host, port), buildProxyRouteForBundles('/bundles/', host, port),
buildProxyRouteForBundles('/dlls/', host, port) buildProxyRouteForBundles('/built_assets/dlls/', host, port)
]; ];
} }

View file

@ -46,7 +46,7 @@ export class DllCompiler {
dllExt: '.bundle.dll.js', dllExt: '.bundle.dll.js',
manifestExt: '.manifest.dll.json', manifestExt: '.manifest.dll.json',
styleExt: '.style.dll.css', styleExt: '.style.dll.css',
outputPath: fromRoot('./dlls'), outputPath: fromRoot('built_assets/dlls'),
publicPath: PUBLIC_PATH_PLACEHOLDER publicPath: PUBLIC_PATH_PLACEHOLDER
}; };
} }

View file

@ -29,7 +29,7 @@ export default async (kbnServer, server, config) => {
// bundles in a "middleware" style. // bundles in a "middleware" style.
// //
// the server listening on 5601 may be restarted a number of times, depending // the server listening on 5601 may be restarted a number of times, depending
// on the watch setup managed by the cli. It proxies all bundles/* and dlls/* // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/*
// requests to the other server. The server on 5602 is long running, in order // requests to the other server. The server on 5602 is long running, in order
// to prevent complete rebuilds of the optimize content. // to prevent complete rebuilds of the optimize content.
const watch = config.get('optimize.watch'); const watch = config.get('optimize.watch');

View file

@ -33,7 +33,7 @@ export default async kbnServer => {
* while the optimizer is running * while the optimizer is running
* *
* server: this process runs the entire kibana server and proxies * server: this process runs the entire kibana server and proxies
* all requests for /bundles/* or /dlls/* to the optmzr process * all requests for /bundles/* or /built_assets/dlls/* to the optmzr process
* *
* @param {string} process.env.kbnWorkerType * @param {string} process.env.kbnWorkerType
*/ */

View file

@ -0,0 +1,5 @@
foo {
bar {
display: flex;
}
}

View file

@ -23,30 +23,24 @@ import fs from 'fs';
import sass from 'node-sass'; import sass from 'node-sass';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import postcss from 'postcss'; import postcss from 'postcss';
import mkdirp from 'mkdirp';
const renderSass = promisify(sass.render); const renderSass = promisify(sass.render);
const writeFile = promisify(fs.writeFile); const writeFile = promisify(fs.writeFile);
const mkdirpAsync = promisify(mkdirp);
export class Build { export class Build {
constructor(source, log) { constructor(source, log, targetPath) {
this.source = source; this.source = source;
this.log = log; this.log = log;
this.targetPath = targetPath;
this.includedFiles = [source]; this.includedFiles = [source];
} }
outputPath() {
const fileName = path.basename(this.source, path.extname(this.source)) + '.css';
return path.join(path.dirname(this.source), fileName);
}
/** /**
* Glob based on source path * Glob based on source path
*/ */
getGlob() {
return path.join(path.dirname(this.source), '**', '*.s{a,c}ss');
}
async buildIfIncluded(path) { async buildIfIncluded(path) {
if (this.includedFiles && this.includedFiles.includes(path)) { if (this.includedFiles && this.includedFiles.includes(path)) {
await this.build(); await this.build();
@ -61,11 +55,9 @@ export class Build {
*/ */
async build() { async build() {
const outFile = this.outputPath();
const rendered = await renderSass({ const rendered = await renderSass({
file: this.source, file: this.source,
outFile, outFile: this.targetPath,
sourceMap: true, sourceMap: true,
sourceMapEmbed: true, sourceMapEmbed: true,
includePaths: [ includePaths: [
@ -78,7 +70,8 @@ export class Build {
this.includedFiles = rendered.stats.includedFiles; this.includedFiles = rendered.stats.includedFiles;
await writeFile(outFile, prefixed.css); await mkdirpAsync(path.dirname(this.targetPath));
await writeFile(this.targetPath, prefixed.css);
return this; return this;
} }

View file

@ -17,34 +17,35 @@
* under the License. * under the License.
*/ */
import path from 'path'; import { resolve } from 'path';
import sass from 'node-sass'; import { readFileSync } from 'fs';
import del from 'del';
import { Build } from './build'; import { Build } from './build';
jest.mock('node-sass'); const TMP = resolve(__dirname, '__tmp__');
const FIXTURE = resolve(__dirname, '__fixtures__/index.scss');
describe('SASS builder', () => { afterEach(async () => {
jest.mock('fs'); await del(TMP);
});
it('generates a glob', () => { it('builds SASS', async () => {
const builder = new Build('/foo/style.sass'); const cssPath = resolve(TMP, 'style.css');
expect(builder.getGlob()).toEqual(path.join('/foo', '**', '*.s{a,c}ss')); await (new Build(FIXTURE, {
}); info: () => {},
warn: () => {},
error: () => {},
}, cssPath)).build();
it('builds SASS', () => { expect(readFileSync(cssPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2'))
sass.render.mockImplementation(() => Promise.resolve(null, { css: 'test' })); .toMatchInlineSnapshot(`
const builder = new Build('/foo/style.sass'); "foo bar {
builder.build(); display: -webkit-box;
display: -webkit-flex;
const sassCall = sass.render.mock.calls[0][0]; display: -ms-flexbox;
expect(sassCall.file).toEqual('/foo/style.sass'); display: flex; }
expect(sassCall.outFile).toEqual(path.join('/foo', 'style.css')); /*# sourceMappingURL=... */"
expect(sassCall.sourceMap).toBe(true); `);
expect(sassCall.sourceMapEmbed).toBe(true); });
});
it('has an output file with a different extension', () => {
const builder = new Build('/foo/style.sass');
expect(builder.outputPath()).toEqual(path.join('/foo', 'style.css'));
});
});

View file

@ -17,16 +17,18 @@
* under the License. * under the License.
*/ */
import { resolve } from 'path';
import { Build } from './build'; import { Build } from './build';
export async function buildAll(styleSheets = [], log) { export async function buildAll(styleSheets, log, buildDir) {
const bundles = await Promise.all(styleSheets.map(async styleSheet => { const bundles = await Promise.all(styleSheets.map(async styleSheet => {
if (!styleSheet.localPath.endsWith('.scss')) { if (!styleSheet.localPath.endsWith('.scss')) {
return; return;
} }
const bundle = new Build(styleSheet.localPath, log); const bundle = new Build(styleSheet.localPath, log, resolve(buildDir, styleSheet.publicPath));
await bundle.build(); await bundle.build();
return bundle; return bundle;

View file

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { IS_KIBANA_DISTRIBUTABLE } from '../../utils'; import { IS_KIBANA_DISTRIBUTABLE, fromRoot } from '../../utils';
export async function sassMixin(kbnServer, server, config) { export async function sassMixin(kbnServer, server, config) {
if (process.env.kbnWorkerType === 'optmzr') { if (process.env.kbnWorkerType === 'optmzr') {
@ -45,7 +45,7 @@ export async function sassMixin(kbnServer, server, config) {
}; };
try { try {
scssBundles = await buildAll(kbnServer.uiExports.styleSheetPaths, log); scssBundles = await buildAll(kbnServer.uiExports.styleSheetPaths, log, fromRoot('built_assets/css'));
scssBundles.forEach(bundle => { scssBundles.forEach(bundle => {
bundle.includedFiles.forEach(file => trackedFiles.add(file)); bundle.includedFiles.forEach(file => trackedFiles.add(file));

View file

@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElasticIndex fetchInfo handles v7 indices 1`] = `
Object {
"aliases": Object {
"foo": ".baz",
},
"exists": true,
"indexName": ".baz",
"mappings": Object {
"doc": Object {
"dynamic": "strict",
"properties": Object {
"a": "b",
},
},
},
}
`;
exports[`ElasticIndex write writes documents in bulk to the index 1`] = ` exports[`ElasticIndex write writes documents in bulk to the index 1`] = `
Array [ Array [
"bulk", "bulk",

View file

@ -56,6 +56,23 @@ describe('ElasticIndex', () => {
); );
}); });
test('handles v7 indices', async () => {
const callCluster = sinon.spy(async (path: string, { index }: any) => {
return {
[index]: {
aliases: { foo: index },
mappings: {
dynamic: 'strict',
properties: { a: 'b' },
},
},
};
});
const result = await Index.fetchInfo(callCluster, '.baz');
expect(result).toMatchSnapshot();
});
test('fails if there are multiple root types', async () => { test('fails if there are multiple root types', async () => {
const callCluster = sinon.spy(async (path: string, { index }: any) => { const callCluster = sinon.spy(async (path: string, { index }: any) => {
return { return {

View file

@ -65,7 +65,7 @@ export async function fetchInfo(callCluster: CallCluster, index: string): Promis
const [indexName, indexInfo] = Object.entries(result)[0]; const [indexName, indexInfo] = Object.entries(result)[0];
return assertIsSupportedIndex({ ...indexInfo, exists: true, indexName }); return assertIsSupportedIndex({ ...normalizeV6AndV7(indexInfo), exists: true, indexName });
} }
/** /**
@ -287,6 +287,26 @@ export async function claimAlias(
await callCluster('indices.refresh', { index }); await callCluster('indices.refresh', { index });
} }
/**
* ES7 removed the "doc" property from mappings. This function takes a v6 or v7
* index info object and returns an object in v6 form.
*/
function normalizeV6AndV7(indexInfo: FullIndexInfo) {
const mappings = indexInfo.mappings as any;
const isV7Index = !mappings.doc && mappings.dynamic && mappings.properties;
if (!isV7Index) {
return indexInfo;
}
return {
...indexInfo,
mappings: {
doc: mappings,
},
};
}
/** /**
* This is a rough check to ensure that the index being migrated satisfies at least * This is a rough check to ensure that the index being migrated satisfies at least
* some rudimentary expectations. Past Kibana indices had multiple root documents, etc * some rudimentary expectations. Past Kibana indices had multiple root documents, etc
@ -296,7 +316,7 @@ export async function claimAlias(
* *
* @param {FullIndexInfo} indexInfo * @param {FullIndexInfo} indexInfo
*/ */
async function assertIsSupportedIndex(indexInfo: FullIndexInfo) { function assertIsSupportedIndex(indexInfo: FullIndexInfo) {
const currentTypes = getTypes(indexInfo.mappings); const currentTypes = getTypes(indexInfo.mappings);
const isV5Index = currentTypes.length > 1 || currentTypes[0] !== ROOT_TYPE; const isV5Index = currentTypes.length > 1 || currentTypes[0] !== ROOT_TYPE;
if (isV5Index) { if (isV5Index) {

View file

@ -1,7 +1,7 @@
<tr> <tr>
<td width="1%"></td> <td width="1%"></td>
<th <th
ng-if="indexPattern.timeFieldName" ng-if="indexPattern.timeFieldName && !hideTimeColumn"
data-test-subj="docTableHeaderField" data-test-subj="docTableHeaderField"
scope="col" scope="col"
> >

View file

@ -36,7 +36,9 @@ module.directive('kbnTableHeader', function (shortDotsFilter) {
onMoveColumn: '=?', onMoveColumn: '=?',
}, },
template: headerHtml, template: headerHtml,
controller: function ($scope) { controller: function ($scope, config) {
$scope.hideTimeColumn = config.get('doc_table:hideTimeColumn');
$scope.isSortableColumn = function isSortableColumn(columnName) { $scope.isSortableColumn = function isSortableColumn(columnName) {
return ( return (
!!$scope.indexPattern !!$scope.indexPattern

View file

@ -45,7 +45,7 @@ const MIN_LINE_LENGTH = 20;
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr> * <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
* ``` * ```
*/ */
module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl) { module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl, config) {
const cellTemplate = _.template(noWhiteSpace(require('ui/doc_table/components/table_row/cell.html'))); const cellTemplate = _.template(noWhiteSpace(require('ui/doc_table/components/table_row/cell.html')));
const truncateByHeightTemplate = _.template(noWhiteSpace(require('ui/partials/truncate_by_height.html'))); const truncateByHeightTemplate = _.template(noWhiteSpace(require('ui/partials/truncate_by_height.html')));
@ -139,7 +139,8 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl
]; ];
const mapping = indexPattern.fields.byName; const mapping = indexPattern.fields.byName;
if (indexPattern.timeFieldName) { const hideTimeColumn = config.get('doc_table:hideTimeColumn');
if (indexPattern.timeFieldName && !hideTimeColumn) {
newHtmls.push(cellTemplate({ newHtmls.push(cellTemplate({
timefield: true, timefield: true,
formatted: _displayField(row, indexPattern.timeFieldName), formatted: _displayField(row, indexPattern.timeFieldName),

View file

@ -18,6 +18,7 @@
*/ */
import path from 'path'; import path from 'path';
import { existsSync } from 'fs';
import { flatConcatAtType } from './reduce'; import { flatConcatAtType } from './reduce';
import { mapSpec, wrap } from './modify_reduce'; import { mapSpec, wrap } from './modify_reduce';
@ -46,16 +47,22 @@ 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;
}
// get the path of the stylesheet relative to the public dir for the plugin // get the path of the stylesheet relative to the public dir for the plugin
let relativePath = path.relative(publicDir, localPath); let relativePath = path.relative(publicDir, localCssPath);
// replace back slashes on windows // replace back slashes on windows
relativePath = relativePath.split('\\').join('/'); relativePath = relativePath.split('\\').join('/');
// replace the extension of relativePath to be .css
// publicPath will always point to the css file
relativePath = relativePath.slice(0, -extname.length) + '.css';
const publicPath = `plugins/${pluginSpec.getId()}/${relativePath}`; const publicPath = `plugins/${pluginSpec.getId()}/${relativePath}`;
return { return {
@ -64,4 +71,4 @@ function normalize(localPath, type, pluginSpec) {
}; };
} }
export const styleSheetPaths = wrap(mapSpec(normalize), flatConcatAtType); export const styleSheetPaths = wrap(mapSpec(normalize), flatConcatAtType);

View file

@ -23,6 +23,7 @@ import { resolve } from 'path';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { AppBootstrap } from './bootstrap'; import { AppBootstrap } from './bootstrap';
import { mergeVariables } from './lib'; import { mergeVariables } from './lib';
import { fromRoot } from '../../utils';
export function uiRenderMixin(kbnServer, server, config) { export function uiRenderMixin(kbnServer, server, config) {
function replaceInjectedVars(request, injectedVars) { function replaceInjectedVars(request, injectedVars) {
@ -50,6 +51,9 @@ export function uiRenderMixin(kbnServer, server, config) {
// render all views from ./views // render all views from ./views
server.setupViews(resolve(__dirname, 'views')); server.setupViews(resolve(__dirname, 'views'));
// expose built css
server.exposeStaticDir('/built_assets/css/{path*}', fromRoot('built_assets/css'));
server.route({ server.route({
path: '/bundles/app/{id}/bootstrap.js', path: '/bundles/app/{id}/bootstrap.js',
method: 'GET', method: 'GET',
@ -63,12 +67,19 @@ export function uiRenderMixin(kbnServer, server, config) {
const basePath = config.get('server.basePath'); const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`; const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/dlls`; const dllBundlePath = `${basePath}/built_assets/dlls`;
const styleSheetPaths = [ const styleSheetPaths = [
`${dllBundlePath}/vendors.style.dll.css`, `${dllBundlePath}/vendors.style.dll.css`,
`${regularBundlePath}/commons.style.css`, `${regularBundlePath}/commons.style.css`,
`${regularBundlePath}/${app.getId()}.style.css`, `${regularBundlePath}/${app.getId()}.style.css`,
].concat(kbnServer.uiExports.styleSheetPaths.map(path => `${basePath}/${path.publicPath}`).reverse()); ...kbnServer.uiExports.styleSheetPaths
.map(path => (
path.localPath.endsWith('.scss')
? `${basePath}/built_assets/css/${path.publicPath}`
: `${basePath}/${path.publicPath}`
))
.reverse()
];
const bootstrap = new AppBootstrap({ const bootstrap = new AppBootstrap({
templateData: { templateData: {

View file

@ -85,17 +85,17 @@ module.exports = function (grunt) {
// list of files / patterns to load in the browser // list of files / patterns to load in the browser
files: [ files: [
'http://localhost:5610/dlls/vendors.bundle.dll.js', 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js',
'http://localhost:5610/bundles/tests.bundle.js', 'http://localhost:5610/bundles/tests.bundle.js',
'http://localhost:5610/dlls/vendors.style.dll.css', 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css',
'http://localhost:5610/bundles/tests.style.css' 'http://localhost:5610/bundles/tests.style.css'
], ],
proxies: { proxies: {
'/tests/': 'http://localhost:5610/tests/', '/tests/': 'http://localhost:5610/tests/',
'/bundles/': 'http://localhost:5610/bundles/', '/bundles/': 'http://localhost:5610/bundles/',
'/dlls/': 'http://localhost:5610/dlls/' '/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/'
}, },
client: { client: {
@ -176,10 +176,10 @@ module.exports = function (grunt) {
singleRun: true, singleRun: true,
options: { options: {
files: [ files: [
'http://localhost:5610/dlls/vendors.bundle.dll.js', 'http://localhost:5610/built_assets/dlls/vendors.bundle.dll.js',
`http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${n}`, `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${n}`,
'http://localhost:5610/dlls/vendors.style.dll.css', 'http://localhost:5610/built_assets/dlls/vendors.style.dll.css',
'http://localhost:5610/bundles/tests.style.css' 'http://localhost:5610/bundles/tests.style.css'
] ]
} }

View file

@ -20,12 +20,12 @@ const homeTabs: IHistoryTab[] = [
{ {
path: '/services', path: '/services',
name: 'Services', name: 'Services',
component: ServiceOverview render: props => <ServiceOverview {...props} />
}, },
{ {
path: '/traces', path: '/traces',
name: 'Traces', name: 'Traces',
component: TraceOverview render: props => <TraceOverview {...props} />
} }
]; ];

View file

@ -39,14 +39,14 @@ exports[`Home component should render 1`] = `
tabs={ tabs={
Array [ Array [
Object { Object {
"component": [Function],
"name": "Services", "name": "Services",
"path": "/services", "path": "/services",
"render": [Function],
}, },
Object { Object {
"component": [Function],
"name": "Traces", "name": "Traces",
"path": "/traces", "path": "/traces",
"render": [Function],
}, },
] ]
} }

View file

@ -26,7 +26,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
name: 'Transactions', name: 'Transactions',
path: `/${serviceName}/transactions/${transactionTypes[0]}`, path: `/${serviceName}/transactions/${transactionTypes[0]}`,
routePath: `/${serviceName}/transactions/:transactionType?`, routePath: `/${serviceName}/transactions/:transactionType?`,
component: () => ( render: () => (
<TransactionOverview <TransactionOverview
urlParams={urlParams} urlParams={urlParams}
serviceTransactionTypes={transactionTypes} serviceTransactionTypes={transactionTypes}
@ -36,7 +36,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
{ {
name: 'Errors', name: 'Errors',
path: `/${serviceName}/errors`, path: `/${serviceName}/errors`,
component: () => { render: () => {
return ( return (
<ErrorGroupOverview urlParams={urlParams} location={location} /> <ErrorGroupOverview urlParams={urlParams} location={location} />
); );
@ -45,7 +45,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
{ {
name: 'Metrics', name: 'Metrics',
path: `/${serviceName}/metrics`, path: `/${serviceName}/metrics`,
component: () => <ServiceMetrics urlParams={urlParams} /> render: () => <ServiceMetrics urlParams={urlParams} />
} }
]; ];

View file

@ -90,26 +90,19 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
}; };
public addErrorToast = () => { public addErrorToast = () => {
const { location, urlParams } = this.props; const { urlParams } = this.props;
const { serviceName = 'unknown', transactionType } = urlParams; const { serviceName = 'unknown' } = urlParams;
if (!serviceName) { if (!serviceName) {
return; return;
} }
toastNotifications.addWarning({ toastNotifications.addWarning({
title: 'Job already exists', title: 'Job creation failed',
text: ( text: (
<p> <p>
There&apos;s already a job running for anomaly detection on{' '} Your current license may not allow for creating machine learning jobs,
{serviceName} ({transactionType}).{' '} or this job may already exist.
<ViewMLJob
serviceName={serviceName}
transactionType={transactionType}
location={location}
>
View existing job
</ViewMLJob>
</p> </p>
) )
}); });

View file

@ -70,12 +70,13 @@ const traceListColumns: ITableColumn[] = [
]; ];
export function TraceList({ items = [], noItemsMessage, isLoading }: Props) { export function TraceList({ items = [], noItemsMessage, isLoading }: Props) {
return isLoading ? null : ( const noItems = isLoading ? null : noItemsMessage;
return (
<ManagedTable <ManagedTable
columns={traceListColumns} columns={traceListColumns}
items={items} items={items}
initialSort={{ field: 'impact', direction: 'desc' }} initialSort={{ field: 'impact', direction: 'desc' }}
noItemsMessage={noItemsMessage} noItemsMessage={noItems}
initialPageSize={25} initialPageSize={25}
/> />
); );

View file

@ -6,6 +6,7 @@ exports[`TransactionOverviewView should render with type filter controls 1`] = `
describedByIds={Array []} describedByIds={Array []}
fullWidth={false} fullWidth={false}
hasEmptyLabelSpace={false} hasEmptyLabelSpace={false}
label="Filter by type"
> >
<EuiSelect <EuiSelect
compressed={false} compressed={false}
@ -16,11 +17,11 @@ exports[`TransactionOverviewView should render with type filter controls 1`] = `
options={ options={
Array [ Array [
Object { Object {
"text": "Filter by type: a", "text": "a",
"value": "a", "value": "a",
}, },
Object { Object {
"text": "Filter by type: b", "text": "b",
"value": "b", "value": "b",
}, },
] ]

View file

@ -48,10 +48,10 @@ export class TransactionOverviewView extends React.Component<
return ( return (
<React.Fragment> <React.Fragment>
{serviceTransactionTypes.length > 1 ? ( {serviceTransactionTypes.length > 1 ? (
<EuiFormRow> <EuiFormRow label="Filter by type">
<EuiSelect <EuiSelect
options={serviceTransactionTypes.map(type => ({ options={serviceTransactionTypes.map(type => ({
text: `Filter by type: ${type}`, text: `${type}`,
value: type value: type
}))} }))}
value={transactionType} value={transactionType}

View file

@ -40,17 +40,17 @@ describe('HistoryTabs', () => {
{ {
name: 'One', name: 'One',
path: '/one', path: '/one',
component: () => <Content name="one" /> render: props => <Content {...props} name="one" />
}, },
{ {
name: 'Two', name: 'Two',
path: '/two', path: '/two',
component: () => <Content name="two" /> render: () => <Content name="two" />
}, },
{ {
name: 'Three', name: 'Three',
path: '/three', path: '/three',
component: () => <Content name="three" /> render: () => <Content name="three" />
} }
]; ];

View file

@ -55,19 +55,19 @@ exports[`HistoryTabs should render correctly 1`] = `
size="l" size="l"
/> />
<Route <Route
component={[Function]}
key="/one" key="/one"
path="/one" path="/one"
render={[Function]}
/> />
<Route <Route
component={[Function]}
key="/two" key="/two"
path="/two" path="/two"
render={[Function]}
/> />
<Route <Route
component={[Function]}
key="/three" key="/three"
path="/three" path="/three"
render={[Function]}
/> />
</React.Fragment> </React.Fragment>
`; `;

View file

@ -17,7 +17,7 @@ export interface IHistoryTab {
path: string; path: string;
routePath?: string; routePath?: string;
name: React.ReactNode; name: React.ReactNode;
component?: React.SFC | React.ComponentClass; render?: (props: RouteComponentProps) => React.ReactNode;
} }
export interface HistoryTabsProps extends RouteComponentProps { export interface HistoryTabsProps extends RouteComponentProps {
@ -51,10 +51,10 @@ const HistoryTabsWithoutRouter = ({
</EuiTabs> </EuiTabs>
<EuiSpacer /> <EuiSpacer />
{tabs.map(tab => {tabs.map(tab =>
tab.component ? ( tab.render ? (
<Route <Route
path={tab.routePath || tab.path} path={tab.routePath || tab.path}
component={tab.component} render={tab.render}
key={tab.path} key={tab.path}
/> />
) : null ) : null

View file

@ -6,6 +6,7 @@
import { PROCESSOR_NAME } from '../../../common/constants'; import { PROCESSOR_NAME } from '../../../common/constants';
// Note: this logic is duplicated in tutorials/apm/envs/on_prem
export async function getAgentStatus({ setup }) { export async function getAgentStatus({ setup }) {
const { client, config } = setup; const { client, config } = setup;
@ -18,11 +19,12 @@ export async function getAgentStatus({ setup }) {
size: 0, size: 0,
query: { query: {
bool: { bool: {
filter: { should: [
exists: { { term: { [PROCESSOR_NAME]: 'error' } },
field: PROCESSOR_NAME { term: { [PROCESSOR_NAME]: 'transaction' } },
} { term: { [PROCESSOR_NAME]: 'metric' } },
} { term: { [PROCESSOR_NAME]: 'sourcemap' } }
]
} }
} }
} }

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
// Note: this logic is duplicated in tutorials/apm/envs/on_prem
export async function getServerStatus({ setup }) { export async function getServerStatus({ setup }) {
const { client, config } = setup; const { client, config } = setup;

View file

@ -79,7 +79,7 @@ describe('alterColumn', () => {
it('throws when converting to an invalid type', () => { it('throws when converting to an invalid type', () => {
expect(() => fn(testTable, { column: 'name', type: 'foo' })).to.throwException(e => { expect(() => fn(testTable, { column: 'name', type: 'foo' })).to.throwException(e => {
expect(e.message).to.be('Cannot convert to foo'); expect(e.message).to.be(`Cannot convert to 'foo'`);
}); });
}); });
}); });

View file

@ -59,7 +59,7 @@ describe('axisConfig', () => {
expect(fn) expect(fn)
.withArgs(testTable, { position: 'foo' }) .withArgs(testTable, { position: 'foo' })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be('Invalid position foo'); expect(e.message).to.be(`Invalid position: 'foo'`);
}); });
}); });
}); });
@ -83,7 +83,7 @@ describe('axisConfig', () => {
.withArgs(testTable, { min: 'foo' }) .withArgs(testTable, { min: 'foo' })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be( expect(e.message).to.be(
`Invalid date string 'foo' found. 'min' must be a number, date in ms, or ISO8601 date string` `Invalid date string: 'foo'. 'min' must be a number, date in ms, or ISO8601 date string`
); );
}); });
}); });
@ -108,7 +108,7 @@ describe('axisConfig', () => {
.withArgs(testTable, { max: '20/02/17' }) .withArgs(testTable, { max: '20/02/17' })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be( expect(e.message).to.be(
`Invalid date string '20/02/17' found. 'max' must be a number, date in ms, or ISO8601 date string` `Invalid date string: '20/02/17'. 'max' must be a number, date in ms, or ISO8601 date string`
); );
}); });
}); });

View file

@ -24,10 +24,14 @@ describe('compare', () => {
it('throws when invalid op is provided', () => { it('throws when invalid op is provided', () => {
expect(() => fn(1, { op: 'boo', to: 2 })).to.throwException(e => { expect(() => fn(1, { op: 'boo', to: 2 })).to.throwException(e => {
expect(e.message).to.be('Invalid compare operator. Use eq, ne, lt, gt, lte, or gte.'); expect(e.message).to.be(
`Invalid compare operator: 'boo'. Use eq, ne, lt, gt, lte, or gte.`
);
}); });
expect(() => fn(1, { op: 'boo' })).to.throwException(e => { expect(() => fn(1, { op: 'boo' })).to.throwException(e => {
expect(e.message).to.be('Invalid compare operator. Use eq, ne, lt, gt, lte, or gte.'); expect(e.message).to.be(
`Invalid compare operator: 'boo'. Use eq, ne, lt, gt, lte, or gte.`
);
}); });
}); });
}); });

View file

@ -105,7 +105,7 @@ describe('font', () => {
it('throws when provided an invalid weight', () => { it('throws when provided an invalid weight', () => {
expect(() => fn(null, { weight: 'foo' })).to.throwException(e => { expect(() => fn(null, { weight: 'foo' })).to.throwException(e => {
expect(e.message).to.be('Invalid font weight: foo'); expect(e.message).to.be(`Invalid font weight: 'foo'`);
}); });
}); });
}); });
@ -175,7 +175,7 @@ describe('font', () => {
expect(fn) expect(fn)
.withArgs(null, { align: 'foo' }) .withArgs(null, { align: 'foo' })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be('Invalid text alignment: foo'); expect(e.message).to.be(`Invalid text alignment: 'foo'`);
}); });
}); });
}); });

View file

@ -44,7 +44,7 @@ describe('getCell', () => {
it('throws when invalid column is provided', () => { it('throws when invalid column is provided', () => {
expect(() => fn(testTable, { column: 'foo' })).to.throwException(e => { expect(() => fn(testTable, { column: 'foo' })).to.throwException(e => {
expect(e.message).to.be('Column not found: foo'); expect(e.message).to.be(`Column not found: 'foo'`);
}); });
}); });
}); });
@ -66,15 +66,15 @@ describe('getCell', () => {
const invalidRow = testTable.rows.length; const invalidRow = testTable.rows.length;
expect(() => fn(testTable, { column: 'name', row: invalidRow })).to.throwException(e => { expect(() => fn(testTable, { column: 'name', row: invalidRow })).to.throwException(e => {
expect(e.message).to.be(`Row not found: ${invalidRow}`); expect(e.message).to.be(`Row not found: '${invalidRow}'`);
}); });
expect(() => fn(emptyTable, { column: 'foo' })).to.throwException(e => { expect(() => fn(emptyTable, { column: 'foo' })).to.throwException(e => {
expect(e.message).to.be('Row not found: 0'); expect(e.message).to.be(`Row not found: '0'`);
}); });
expect(() => fn(emptyTable)).to.throwException(e => { expect(() => fn(emptyTable)).to.throwException(e => {
expect(e.message).to.be('Row not found: 0'); expect(e.message).to.be(`Row not found: '0'`);
}); });
}); });
}); });

View file

@ -82,12 +82,12 @@ describe('ply', () => {
it('throws when by is an invalid column', () => { it('throws when by is an invalid column', () => {
expect(() => fn(testTable, { by: [''], expression: [averagePrice] })).to.throwException( expect(() => fn(testTable, { by: [''], expression: [averagePrice] })).to.throwException(
e => { e => {
expect(e.message).to.be('No such column: '); expect(e.message).to.be(`Column not found: ''`);
} }
); );
expect(() => fn(testTable, { by: ['foo'], expression: [averagePrice] })).to.throwException( expect(() => fn(testTable, { by: ['foo'], expression: [averagePrice] })).to.throwException(
e => { e => {
expect(e.message).to.be('No such column: foo'); expect(e.message).to.be(`Column not found: 'foo'`);
} }
); );
}); });

View file

@ -29,7 +29,7 @@ describe('progress', () => {
expect(fn) expect(fn)
.withArgs(3) .withArgs(3)
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be('Context must be between 0 and 1'); expect(e.message).to.be(`Invalid value: '3'. Value must be between 0 and 1`);
}); });
}); });
@ -65,7 +65,7 @@ describe('progress', () => {
expect(fn) expect(fn)
.withArgs(value, { max: -0.5 }) .withArgs(value, { max: -0.5 })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be(`'max' must be greater than 0`); expect(e.message).to.be(`Invalid max value: '-0.5'. 'max' must be greater than 0`);
}); });
}); });
}); });

View file

@ -29,7 +29,7 @@ describe('revealImage', () => {
origin: 'top', origin: 'top',
}) })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be.equal('input must be between 0 and 1'); expect(e.message).to.be.equal(`Invalid value: '10'. Percentage must be between 0 and 1`);
}); });
expect(fn) expect(fn)
@ -39,7 +39,9 @@ describe('revealImage', () => {
origin: 'top', origin: 'top',
}) })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be.equal('input must be between 0 and 1'); expect(e.message).to.be.equal(
`Invalid value: '-0.1'. Percentage must be between 0 and 1`
);
}); });
}); });
}); });

View file

@ -88,7 +88,7 @@ describe('timefilter', () => {
it('throws when provided an invalid date string', () => { it('throws when provided an invalid date string', () => {
expect(() => fn(emptyFilter, { from: '2018-13-42T15:00:00.950Z' })).to.throwException(e => { expect(() => fn(emptyFilter, { from: '2018-13-42T15:00:00.950Z' })).to.throwException(e => {
expect(e.message).to.be.equal('Invalid date/time string 2018-13-42T15:00:00.950Z'); expect(e.message).to.be.equal(`Invalid date/time string: '2018-13-42T15:00:00.950Z'`);
}); });
}); });
}); });

View file

@ -74,7 +74,7 @@ export const alterColumn = () => ({
case 'null': case 'null':
return () => null; return () => null;
default: default:
throw new Error(`Cannot convert to ${type}`); throw new Error(`Cannot convert to '${type}'`);
} }
})(); })();
} }

View file

@ -43,7 +43,7 @@ export const axisConfig = () => ({
fn: (context, args) => { fn: (context, args) => {
const positions = ['top', 'bottom', 'left', 'right', '']; const positions = ['top', 'bottom', 'left', 'right', ''];
if (!positions.includes(args.position)) { if (!positions.includes(args.position)) {
throw new Error(`Invalid position ${args.position}`); throw new Error(`Invalid position: '${args.position}'`);
} }
const min = typeof args.min === 'string' ? moment.utc(args.min).valueOf() : args.min; const min = typeof args.min === 'string' ? moment.utc(args.min).valueOf() : args.min;
@ -51,16 +51,16 @@ export const axisConfig = () => ({
if (min != null && isNaN(min)) { if (min != null && isNaN(min)) {
throw new Error( throw new Error(
`Invalid date string '${ `Invalid date string: '${
args.min args.min
}' found. 'min' must be a number, date in ms, or ISO8601 date string` }'. 'min' must be a number, date in ms, or ISO8601 date string`
); );
} }
if (max != null && isNaN(max)) { if (max != null && isNaN(max)) {
throw new Error( throw new Error(
`Invalid date string '${ `Invalid date string: '${
args.max args.max
}' found. 'max' must be a number, date in ms, or ISO8601 date string` }'. 'max' must be a number, date in ms, or ISO8601 date string`
); );
} }

View file

@ -61,7 +61,7 @@ export const compare = () => ({
} }
return false; return false;
default: default:
throw new Error('Invalid compare operator. Use eq, ne, lt, gt, lte, or gte.'); throw new Error(`Invalid compare operator: '${op}'. Use eq, ne, lt, gt, lte, or gte.`);
} }
return false; return false;

View file

@ -80,10 +80,10 @@ export const font = () => ({
}, },
fn: (context, args) => { fn: (context, args) => {
if (!weights.includes(args.weight)) { if (!weights.includes(args.weight)) {
throw new Error(`Invalid font weight: ${args.weight}`); throw new Error(`Invalid font weight: '${args.weight}'`);
} }
if (!alignments.includes(args.align)) { if (!alignments.includes(args.align)) {
throw new Error(`Invalid text alignment: ${args.align}`); throw new Error(`Invalid text alignment: '${args.align}'`);
} }
// the line height shouldn't ever be lower than the size // the line height shouldn't ever be lower than the size

View file

@ -26,14 +26,14 @@ export const getCell = () => ({
fn: (context, args) => { fn: (context, args) => {
const row = context.rows[args.row]; const row = context.rows[args.row];
if (!row) { if (!row) {
throw new Error(`Row not found: ${args.row}`); throw new Error(`Row not found: '${args.row}'`);
} }
const { column = context.columns[0].name } = args; const { column = context.columns[0].name } = args;
const value = row[column]; const value = row[column];
if (typeof value === 'undefined') { if (typeof value === 'undefined') {
throw new Error(`Column not found: ${column}`); throw new Error(`Column not found: '${column}'`);
} }
return value; return value;

View file

@ -95,7 +95,7 @@ export const ply = () => ({
byColumns = args.by.map(by => { byColumns = args.by.map(by => {
const column = context.columns.find(column => column.name === by); const column = context.columns.find(column => column.name === by);
if (!column) { if (!column) {
throw new Error(`No such column: ${by}`); throw new Error(`Column not found: '${by}'`);
} }
return column; return column;
}); });

View file

@ -72,10 +72,10 @@ export const progress = () => ({
}, },
fn: (value, args) => { fn: (value, args) => {
if (args.max <= 0) { if (args.max <= 0) {
throw new Error(`'max' must be greater than 0`); throw new Error(`Invalid max value: '${args.max}'. 'max' must be greater than 0`);
} }
if (value > args.max || value < 0) { if (value > args.max || value < 0) {
throw new Error(`Context must be between 0 and ${args.max}`); throw new Error(`Invalid value: '${value}'. Value must be between 0 and ${args.max}`);
} }
let label = ''; let label = '';

View file

@ -35,7 +35,7 @@ export const revealImage = () => ({
}, },
fn: (percent, args) => { fn: (percent, args) => {
if (percent > 1 || percent < 0) { if (percent > 1 || percent < 0) {
throw new Error('input must be between 0 and 1'); throw new Error(`Invalid value: '${percent}'. Percentage must be between 0 and 1`);
} }
return { return {

View file

@ -50,7 +50,7 @@ export const timefilter = () => ({
const moment = dateMath.parse(str); const moment = dateMath.parse(str);
if (!moment || !moment.isValid()) { if (!moment || !moment.isValid()) {
throw new Error(`Invalid date/time string ${str}`); throw new Error(`Invalid date/time string: '${str}'`);
} }
return moment.toISOString(); return moment.toISOString();
} }

View file

@ -36,7 +36,7 @@ describe('demodata', () => {
expect(fn) expect(fn)
.withArgs(null, { type: 'foo' }) .withArgs(null, { type: 'foo' })
.to.throwException(e => { .to.throwException(e => {
expect(e.message).to.be("Invalid data set: foo, use 'ci' or 'shirts'."); expect(e.message).to.be("Invalid data set: 'foo', use 'ci' or 'shirts'.");
}); });
}); });
}); });

View file

@ -15,5 +15,5 @@ export function getDemoRows(arg) {
if (arg === 'shirts') { if (arg === 'shirts') {
return cloneDeep(shirts); return cloneDeep(shirts);
} }
throw new Error(`Invalid data set: ${arg}, use 'ci' or 'shirts'.`); throw new Error(`Invalid data set: '${arg}', use 'ci' or 'shirts'.`);
} }

View file

@ -6,52 +6,53 @@
export const americanTypewriter = { export const americanTypewriter = {
label: 'American Typewriter', label: 'American Typewriter',
value: `'American Typewriter', 'Courier New', Courier, Monaco, mono`, value: "'American Typewriter', 'Courier New', Courier, Monaco, mono",
}; };
export const arial = { label: 'Arial', value: `Arial, sans-serif` }; export const arial = { label: 'Arial', value: 'Arial, sans-serif' };
export const baskerville = { export const baskerville = {
label: 'Baskerville', label: 'Baskerville',
value: `Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif`, value: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif",
}; };
export const bookAntiqua = { export const bookAntiqua = {
label: 'Book Antiqua', label: 'Book Antiqua',
value: `'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif`, value: "'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif",
}; };
export const brushScript = { export const brushScript = {
label: 'Brush Script', label: 'Brush Script',
value: `'Brush Script MT', 'Comic Sans', sans-serif`, value: "'Brush Script MT', 'Comic Sans', sans-serif",
}; };
export const chalkboard = { label: 'Chalkboard', value: `Chalkboard, 'Comic Sans', sans-serif` }; export const chalkboard = { label: 'Chalkboard', value: "Chalkboard, 'Comic Sans', sans-serif" };
export const didot = { export const didot = {
label: 'Didot', label: 'Didot',
value: `Didot, Georgia, Garamond, 'Times New Roman', Times, serif`, value: "Didot, Georgia, Garamond, 'Times New Roman', Times, serif",
}; };
export const futura = { label: 'Futura', value: `Futura, Impact, Helvetica, Arial, sans-serif` }; export const futura = { label: 'Futura', value: 'Futura, Impact, Helvetica, Arial, sans-serif' };
export const gillSans = { export const gillSans = {
label: 'Gill Sans', label: 'Gill Sans',
value: `'Gill Sans', 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif`, value:
"'Gill Sans', 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif",
}; };
export const helveticaNeue = { export const helveticaNeue = {
label: 'Helvetica Neue', label: 'Helvetica Neue',
value: `'Helvetica Neue', Helvetica, Arial, sans-serif`, value: "'Helvetica Neue', Helvetica, Arial, sans-serif",
}; };
export const hoeflerText = { export const hoeflerText = {
label: 'Hoefler Text', label: 'Hoefler Text',
value: `'Hoefler Text', Garamond, Georgia, 'Times New Roman', Times, serif`, value: "'Hoefler Text', Garamond, Georgia, 'Times New Roman', Times, serif",
}; };
export const lucidaGrande = { export const lucidaGrande = {
label: 'Lucida Grande', label: 'Lucida Grande',
value: `'Lucida Grande', 'Lucida Sans Unicode', Lucida, Verdana, Helvetica, Arial, sans-serif`, value: "'Lucida Grande', 'Lucida Sans Unicode', Lucida, Verdana, Helvetica, Arial, sans-serif",
}; };
export const myriad = { label: 'Myriad', value: `Myriad, Helvetica, Arial, sans-serif` }; export const myriad = { label: 'Myriad', value: 'Myriad, Helvetica, Arial, sans-serif' };
export const openSans = { label: 'Open Sans', value: `'Open Sans', Helvetica, Arial, sans-serif` }; export const openSans = { label: 'Open Sans', value: "'Open Sans', Helvetica, Arial, sans-serif" };
export const optima = { export const optima = {
label: 'Optima', label: 'Optima',
value: `Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif`, value: "Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif",
}; };
export const palatino = { export const palatino = {
label: 'Palatino', label: 'Palatino',
value: `Palatino, 'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif`, value: "Palatino, 'Book Antiqua', Georgia, Garamond, 'Times New Roman', Times, serif",
}; };
export const fonts = [ export const fonts = [
americanTypewriter, americanTypewriter,

View file

@ -33,7 +33,7 @@ export const routes = [
notify.error(err, { title: `Couldn't create workpad` }); notify.error(err, { title: `Couldn't create workpad` });
// TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced
// https://github.com/elastic/kibana/issues/20277 // https://github.com/elastic/kibana/issues/20277
if (err.response.status === 403) { if (err.response && err.response.status === 403) {
dispatch(setCanUserWrite(false)); dispatch(setCanUserWrite(false));
} }
router.redirectTo('home'); router.redirectTo('home');
@ -61,7 +61,7 @@ export const routes = [
// TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced
// https://github.com/elastic/kibana/issues/20277 // https://github.com/elastic/kibana/issues/20277
workpadService.update(params.id, fetchedWorkpad).catch(err => { workpadService.update(params.id, fetchedWorkpad).catch(err => {
if (err.response.status === 403) { if (err.response && err.response.status === 403) {
dispatch(setCanUserWrite(false)); dispatch(setCanUserWrite(false));
} }
}); });

View file

@ -28,7 +28,7 @@ export class Clipboard extends React.PureComponent {
render() { render() {
return ( return (
<div className="canvas_clipboard" onClick={this.onClick}> <div className="canvasClipboard" onClick={this.onClick}>
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -0,0 +1,3 @@
.canvasClipboard {
cursor: pointer;
}

View file

@ -10,13 +10,13 @@ import { render } from 'enzyme';
import { Download } from '../'; import { Download } from '../';
describe('<Download />', () => { describe('<Download />', () => {
it('has canvas_download class', () => { it('has canvasDownload class', () => {
const wrapper = render( const wrapper = render(
<Download fileName="hello" content="world"> <Download fileName="hello" content="world">
<button>Download it</button> <button>Download it</button>
</Download> </Download>
); );
expect(wrapper.hasClass('canvas_download')).to.be.ok; expect(wrapper.hasClass('canvasDownload')).to.be.ok;
}); });
}); });

View file

@ -28,7 +28,7 @@ export class Download extends React.PureComponent {
render() { render() {
return ( return (
<div className="canvas_download" onClick={this.onClick}> <div className="canvasDownload" onClick={this.onClick}>
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -13,6 +13,7 @@ import { getWorkpad, getPages } from '../../state/selectors/workpad';
import { getReportingBrowserType } from '../../state/selectors/app'; import { getReportingBrowserType } from '../../state/selectors/app';
import { notify } from '../../lib/notify'; import { notify } from '../../lib/notify';
import { getWindow } from '../../lib/get_window'; import { getWindow } from '../../lib/get_window';
import { downloadWorkpad } from '../../lib/download_workpad';
import { WorkpadExport as Component } from './workpad_export'; import { WorkpadExport as Component } from './workpad_export';
import { getPdfUrl, createPdf } from './utils'; import { getPdfUrl, createPdf } from './utils';
@ -43,29 +44,34 @@ export const WorkpadExport = compose(
throw new Error(`Unknown export type: ${type}`); throw new Error(`Unknown export type: ${type}`);
}, },
onCopy: type => { onCopy: type => {
if (type === 'pdf') { switch (type) {
return notify.info('The PDF generation URL was copied to your clipboard.'); case 'pdf':
return notify.info('The PDF generation URL was copied to your clipboard.');
case 'reportingConfig':
return notify.info(`Copied reporting configuration to clipboard`);
} }
throw new Error(`Unknown export type: ${type}`); throw new Error(`Unknown export type: ${type}`);
}, },
onExport: type => { onExport: type => {
if (type === 'pdf') { switch (type) {
return createPdf(workpad, { pageCount }) case 'pdf':
.then(({ data }) => { return createPdf(workpad, { pageCount })
notify.info('Exporting PDF. You can track the progress in Management.', { .then(({ data }) => {
title: `PDF export of workpad '${workpad.name}'`, notify.info('Exporting PDF. You can track the progress in Management.', {
title: `PDF export of workpad '${workpad.name}'`,
});
// register the job so a completion notification shows up when it's ready
jobCompletionNotifications.add(data.job.id);
})
.catch(err => {
notify.error(err, { title: `Failed to create PDF for '${workpad.name}'` });
}); });
case 'json':
// register the job so a completion notification shows up when it's ready return downloadWorkpad(workpad.id);
jobCompletionNotifications.add(data.job.id); default:
}) throw new Error(`Unknown export type: ${type}`);
.catch(err => {
notify.error(err, { title: `Failed to create PDF for '${workpad.name}'` });
});
} }
throw new Error(`Unknown export type: ${type}`);
}, },
})) }))
)(Component); )(Component);

View file

@ -9,12 +9,12 @@ import PropTypes from 'prop-types';
import { import {
EuiButton, EuiButton,
EuiButtonIcon, EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer, EuiSpacer,
EuiCodeBlock, EuiCodeBlock,
EuiHorizontalRule, EuiCode,
EuiFormRow, EuiContextMenu,
EuiIcon,
EuiText,
} from '@elastic/eui'; } from '@elastic/eui';
import { Popover } from '../popover'; import { Popover } from '../popover';
import { Clipboard } from '../clipboard'; import { Clipboard } from '../clipboard';
@ -27,81 +27,155 @@ export class WorkpadExport extends React.PureComponent {
getExportUrl: PropTypes.func.isRequired, getExportUrl: PropTypes.func.isRequired,
}; };
anchorElement = React.createRef();
flattenPanelTree(tree, array = []) {
array.push(tree);
if (tree.items) {
tree.items.forEach(item => {
if (item.panel) {
this.flattenPanelTree(item.panel, array);
item.panel = item.panel.id;
}
});
}
return array;
}
exportPdf = () => { exportPdf = () => {
this.props.onExport('pdf'); this.props.onExport('pdf');
}; };
renderControls = closePopover => { downloadWorkpad = () => {
this.props.onExport('json');
};
renderPDFControls = closePopover => {
const pdfUrl = this.props.getExportUrl('pdf'); const pdfUrl = this.props.getExportUrl('pdf');
return ( return (
<div> <div className="canvasWorkpadExport__panelContent">
<EuiFlexGroup justifyContent="spaceAround"> <EuiText size="s">
<EuiFlexItem grow> <p>PDFs can take a minute or two to generate based upon the size of your workpad</p>
<EuiFormRow label="Click below to create a PDF. You'll be notified when the export is complete"> </EuiText>
<EuiButton <EuiSpacer size="s" />
onClick={() => {
this.exportPdf();
closePopover();
}}
>
Export as PDF
</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule size="half" />
<EuiFormRow label="To generate a PDF from a script or with Watcher, use this URL.">
<EuiFlexGroup alignItems="center">
<EuiFlexItem style={{ overflow: 'auto' }}>
<EuiCodeBlock style={{ whiteSpace: 'nowrap' }} paddingSize="s">
{pdfUrl}
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem grow={false}> {this.props.options}
<Clipboard
content={pdfUrl} <EuiButton
onCopy={() => { fill
this.props.onCopy('pdf'); onClick={() => {
closePopover(); closePopover();
}} this.exportPdf();
> }}
<EuiButtonIcon aria-label="Copy to clipboard" iconType="copy" /> size="s"
</Clipboard> style={{ width: '100%' }}
</EuiFlexItem> >
</EuiFlexGroup> Generate PDF
</EuiFormRow> </EuiButton>
<EuiSpacer size="s" />
<EuiText size="s">
<p>
Alternatively, copy this POST URL to call generation from outside Kibana or from
Watcher.
</p>
</EuiText>
<EuiSpacer size="s" />
<Clipboard
content={pdfUrl}
onCopy={() => {
this.props.onCopy('pdf');
closePopover();
}}
>
<EuiButton
aria-label="Copy to clipboard"
iconType="copy"
size="s"
style={{ width: '100%' }}
>
Copy POST URL
</EuiButton>
</Clipboard>
</div> </div>
); );
}; };
renderPanelTree = closePopover => ({
id: 0,
title: 'Share this workpad',
items: [
{
name: 'Download as JSON',
icon: <EuiIcon type="exportAction" size="m" />,
onClick: () => {
closePopover();
this.downloadWorkpad();
},
},
{
name: 'PDF Reports',
icon: 'document',
panel: {
id: 1,
title: 'PDF Reports',
content: this.props.enabled
? this.renderPDFControls(closePopover)
: this.renderDisabled(),
},
},
],
});
renderDisabled = () => { renderDisabled = () => {
const reportingConfig = `xpack.reporting:
enabled: true
capture.browser.type: chromium`;
return ( return (
<div> <div className="canvasWorkpadExport__panelContent">
Export to PDF is disabled. You must configure reporting to use the Chromium browser. Add <EuiText size="s">
this to your kibana.yml file. <p>
Export to PDF is disabled. You must configure reporting to use the Chromium browser. Add
this to your <EuiCode>kibana.yml</EuiCode> file.
</p>
</EuiText>
<EuiSpacer /> <EuiSpacer />
<EuiCodeBlock paddingSize="s" language="yml"> <Clipboard content={reportingConfig} onCopy={() => this.props.onCopy('reportingConfig')}>
xpack.reporting.capture.browser.type: chromium <EuiCodeBlock
</EuiCodeBlock> className="canvasWorkpadExport__reportingConfig"
paddingSize="s"
fontSize="s"
language="yml"
>
{reportingConfig}
</EuiCodeBlock>
</Clipboard>
</div> </div>
); );
}; };
render() { render() {
const exportControl = togglePopover => ( const exportControl = togglePopover => (
<EuiButtonIcon iconType="exportAction" aria-label="Create PDF" onClick={togglePopover} /> <EuiButtonIcon iconType="share" aria-label="Share this workpad" onClick={togglePopover} />
); );
// TODO: replace this with `showShareContextMenu` in `ui/share` once it's been converted to React
return ( return (
<Popover button={exportControl} tooltip="Export workpad" tooltipPosition="bottom"> <Popover
button={exportControl}
panelPaddingSize="none"
tooltip="Share workpad"
tooltipPosition="bottom"
>
{({ closePopover }) => ( {({ closePopover }) => (
<EuiFlexGroup justifyContent="flexEnd"> <EuiContextMenu
<EuiFlexItem grow={false} style={{ maxWidth: '300px' }}> initialPanelId={0}
{this.props.enabled && this.renderControls(closePopover)} panels={this.flattenPanelTree(this.renderPanelTree(closePopover))}
{!this.props.enabled && this.renderDisabled()} />
</EuiFlexItem>
</EuiFlexGroup>
)} )}
</Popover> </Popover>
); );

View file

@ -0,0 +1,10 @@
.canvasWorkpadExport__panelContent {
padding: $euiSize;
}
.canvasWorkpadExport__reportingConfig {
.euiCodeBlock__pre {
@include euiScrollBar;
overflow-x: auto;
white-space: pre;
}
}

View file

@ -7,13 +7,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { compose, withState, getContext, withHandlers } from 'recompose'; import { compose, withState, getContext, withHandlers } from 'recompose';
import fileSaver from 'file-saver';
import * as workpadService from '../../lib/workpad_service'; import * as workpadService from '../../lib/workpad_service';
import { notify } from '../../lib/notify'; import { notify } from '../../lib/notify';
import { canUserWrite } from '../../state/selectors/app'; import { canUserWrite } from '../../state/selectors/app';
import { getWorkpad } from '../../state/selectors/workpad'; import { getWorkpad } from '../../state/selectors/workpad';
import { getId } from '../../lib/get_id'; import { getId } from '../../lib/get_id';
import { setCanUserWrite } from '../../state/actions/transient'; import { setCanUserWrite } from '../../state/actions/transient';
import { downloadWorkpad } from '../../lib/download_workpad';
import { WorkpadLoader as Component } from './workpad_loader'; import { WorkpadLoader as Component } from './workpad_loader';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -46,7 +46,7 @@ export const WorkpadLoader = compose(
notify.error(err, { title: `Couldn't upload workpad` }); notify.error(err, { title: `Couldn't upload workpad` });
// TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced
// https://github.com/elastic/kibana/issues/20277 // https://github.com/elastic/kibana/issues/20277
if (err.response.status === 403) { if (err.response && err.response.status === 403) {
props.setCanUserWrite(false); props.setCanUserWrite(false);
} }
} }
@ -67,15 +67,7 @@ export const WorkpadLoader = compose(
}, },
// Workpad import/export methods // Workpad import/export methods
downloadWorkpad: () => async workpadId => { downloadWorkpad: () => workpadId => downloadWorkpad(workpadId),
try {
const workpad = await workpadService.get(workpadId);
const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`);
} catch (err) {
notify.error(err, { title: `Couldn't download workpad` });
}
},
// Clone workpad given an id // Clone workpad given an id
cloneWorkpad: props => async workpadId => { cloneWorkpad: props => async workpadId => {
@ -89,7 +81,7 @@ export const WorkpadLoader = compose(
notify.error(err, { title: `Couldn't clone workpad` }); notify.error(err, { title: `Couldn't clone workpad` });
// TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced
// https://github.com/elastic/kibana/issues/20277 // https://github.com/elastic/kibana/issues/20277
if (err.response.status === 403) { if (err.response && err.response.status === 403) {
props.setCanUserWrite(false); props.setCanUserWrite(false);
} }
} }
@ -122,7 +114,7 @@ export const WorkpadLoader = compose(
errors.push(result.id); errors.push(result.id);
// TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced
// https://github.com/elastic/kibana/issues/20277 // https://github.com/elastic/kibana/issues/20277
if (result.err.response.status === 403) { if (result.err.response && result.err.response.status === 403) {
props.setCanUserWrite(false); props.setCanUserWrite(false);
} }
} else { } else {

View file

@ -142,7 +142,7 @@ export class WorkpadLoader extends React.PureComponent {
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiToolTip content="Download"> <EuiToolTip content="Download">
<EuiButtonIcon <EuiButtonIcon
iconType="sortDown" iconType="exportAction"
onClick={() => this.props.downloadWorkpad(workpad.id)} onClick={() => this.props.downloadWorkpad(workpad.id)}
aria-label="Download Workpad" aria-label="Download Workpad"
/> />
@ -288,7 +288,7 @@ export class WorkpadLoader extends React.PureComponent {
); );
const downloadButton = ( const downloadButton = (
<EuiButton color="secondary" onClick={this.downloadWorkpads} iconType="sortDown"> <EuiButton color="secondary" onClick={this.downloadWorkpads} iconType="exportAction">
{`Download (${selectedWorkpads.length})`} {`Download (${selectedWorkpads.length})`}
</EuiButton> </EuiButton>
); );

View 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;
* you may not use this file except in compliance with the Elastic License.
*/
import fileSaver from 'file-saver';
import { notify } from './notify';
import * as workpadService from './workpad_service';
export const downloadWorkpad = async workpadId => {
try {
const workpad = await workpadService.get(workpadId);
const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`);
} catch (err) {
notify.error(err, { title: `Couldn't download workpad` });
}
};

View file

@ -55,26 +55,26 @@ export const esPersistMiddleware = ({ getState }) => {
if (workpadChanged(curState, newState) || assetsChanged(curState, newState)) { if (workpadChanged(curState, newState) || assetsChanged(curState, newState)) {
const persistedWorkpad = getWorkpadPersisted(getState()); const persistedWorkpad = getWorkpadPersisted(getState());
return update(persistedWorkpad.id, persistedWorkpad).catch(err => { return update(persistedWorkpad.id, persistedWorkpad).catch(err => {
if (err.response.status === 400) { const statusCode = err.response && err.response.status;
return notify.error(err.response, { switch (statusCode) {
title: `Couldn't save your changes to Elasticsearch`, case 400:
}); return notify.error(err.response, {
}
if (err.response.status === 413) {
return notify.error(
`The server gave a response that the workpad data was too large. This
usually means uploaded image assets that are too large for Kibana or
a proxy. Try removing some assets in the asset manager.`,
{
title: `Couldn't save your changes to Elasticsearch`, title: `Couldn't save your changes to Elasticsearch`,
} });
); case 413:
return notify.error(
`The server gave a response that the workpad data was too large. This
usually means uploaded image assets that are too large for Kibana or
a proxy. Try removing some assets in the asset manager.`,
{
title: `Couldn't save your changes to Elasticsearch`,
}
);
default:
return notify.error(err, {
title: `Couldn't update workpad`,
});
} }
return notify.error(err.response, {
title: `Couldn't update workpad`,
});
}); });
} }
}; };

View file

@ -24,6 +24,7 @@
@import '../components/autocomplete/autocomplete'; @import '../components/autocomplete/autocomplete';
@import '../components/border_connection/border_connection'; @import '../components/border_connection/border_connection';
@import '../components/border_resize_handle/border_resize_handle'; @import '../components/border_resize_handle/border_resize_handle';
@import '../components/clipboard/clipboard';
@import '../components/color_dot/color_dot'; @import '../components/color_dot/color_dot';
@import '../components/color_palette/color_palette'; @import '../components/color_palette/color_palette';
@import '../components/color_picker_mini/color_picker_mini'; @import '../components/color_picker_mini/color_picker_mini';
@ -52,6 +53,7 @@
@import '../components/toolbar/toolbar'; @import '../components/toolbar/toolbar';
@import '../components/toolbar/tray/tray'; @import '../components/toolbar/tray/tray';
@import '../components/workpad/workpad'; @import '../components/workpad/workpad';
@import '../components/workpad_export/workpad_export';
@import '../components/workpad_loader/workpad_loader'; @import '../components/workpad_loader/workpad_loader';
@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_loader/workpad_dropzone/workpad_dropzone';
@import '../components/workpad_page/workpad_page'; @import '../components/workpad_page/workpad_page';

View file

@ -41,7 +41,7 @@ export function workpad(server) {
const savedObjectsClient = req.getSavedObjectsClient(); const savedObjectsClient = req.getSavedObjectsClient();
if (!req.payload) { if (!req.payload) {
return Promise.resolve(boom.badRequest('A workpad payload is required')); return Promise.reject(boom.badRequest('A workpad payload is required'));
} }
const now = new Date().toISOString(); const now = new Date().toISOString();

View file

@ -6,6 +6,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { JoinEditor } from './view'; import { JoinEditor } from './view';
import { getSelectedLayer, getSelectedLayerJoinDescriptors } from '../../../selectors/map_selectors';
import { setJoinsForLayer } from '../../../actions/store_actions'; import { setJoinsForLayer } from '../../../actions/store_actions';
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
@ -16,12 +17,10 @@ function mapDispatchToProps(dispatch) {
}; };
} }
function mapStateToProps({}, props) { function mapStateToProps(state = {}) {
return { return {
joins: props.layer.getJoins().map(join => { joins: getSelectedLayerJoinDescriptors(state),
return join.toDescriptor(); layer: getSelectedLayer(state),
}),
layer: props.layer,
}; };
} }

View file

@ -49,7 +49,7 @@ export class LayerPanel extends React.Component {
return ( return (
<EuiPanel> <EuiPanel>
<JoinEditor layer={this.props.selectedLayer}/> <JoinEditor/>
</EuiPanel> </EuiPanel>
); );
} }

View file

@ -135,15 +135,6 @@ export const getDataFilters = createSelector(
export const getDataSources = createSelector(getMetadata, metadata => metadata ? metadata.data_sources : null); export const getDataSources = createSelector(getMetadata, metadata => metadata ? metadata.data_sources : null);
export const getSelectedLayer = createSelector(
getSelectedLayerId,
getLayerListRaw,
getDataSources,
(selectedLayerId, layerList, dataSources) => {
const selectedLayer = layerList.find(layerDescriptor => layerDescriptor.id === selectedLayerId);
return createLayerInstance(selectedLayer, dataSources);
});
export const getLayerList = createSelector( export const getLayerList = createSelector(
getLayerListRaw, getLayerListRaw,
getDataSources, getDataSources,
@ -152,4 +143,19 @@ export const getLayerList = createSelector(
createLayerInstance(layerDescriptor, dataSources)); createLayerInstance(layerDescriptor, dataSources));
}); });
export const getSelectedLayer = createSelector(
getSelectedLayerId,
getLayerList,
(selectedLayerId, layerList) => {
return layerList.find(layer => layer.getId() === selectedLayerId);
});
export const getSelectedLayerJoinDescriptors = createSelector(
getSelectedLayer,
(selectedLayer) => {
return selectedLayer.getJoins().map(join => {
return join.toDescriptor();
});
});
export const getTemporaryLayers = createSelector(getLayerList, (layerList) => layerList.filter(layer => layer.isTemporary())); export const getTemporaryLayers = createSelector(getLayerList, (layerList) => layerList.filter(layer => layer.isTemporary()));

View file

@ -21,7 +21,7 @@ export const ALL_SOURCES = [
EMSFileSource, EMSFileSource,
EMSTMSSource, EMSTMSSource,
KibanaRegionmapSource, KibanaRegionmapSource,
KibanaTilemapSource,
XYZTMSSource, XYZTMSSource,
WMSSource, WMSSource,
KibanaTilemapSource
]; ];

View file

@ -20,7 +20,7 @@ import { emsServiceSettings } from '../../../kibana_services';
export class EMSFileSource extends VectorSource { export class EMSFileSource extends VectorSource {
static type = 'EMS_FILE'; static type = 'EMS_FILE';
static typeDisplayName = 'Elastic Maps Service region boundaries'; static typeDisplayName = 'Elastic Maps Service vector shapes';
static createDescriptor(id) { static createDescriptor(id) {
return { return {
@ -60,7 +60,9 @@ export class EMSFileSource extends VectorSource {
<strong>{EMSFileSource.typeDisplayName}</strong> <strong>{EMSFileSource.typeDisplayName}</strong>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">Political boundry vectors hosted by EMS.</p> <p className="euiTextColor--subdued">
Vector shapes of administrative boundaries from Elastic Maps Service
</p>
</EuiText> </EuiText>
</Fragment> </Fragment>
); );
@ -108,10 +110,6 @@ export class EMSFileSource extends VectorSource {
} }
async isTimeAware() {
return false;
}
canFormatFeatureProperties() { canFormatFeatureProperties() {
return true; return true;
} }

View file

@ -18,7 +18,7 @@ import _ from 'lodash';
export class EMSTMSSource extends TMSSource { export class EMSTMSSource extends TMSSource {
static type = 'EMS_TMS'; static type = 'EMS_TMS';
static typeDisplayName = 'Elastic Maps Service Tile Service'; static typeDisplayName = 'Elastic Maps Service tiles';
static createDescriptor(serviceId) { static createDescriptor(serviceId) {
return { return {
@ -58,7 +58,9 @@ export class EMSTMSSource extends TMSSource {
<strong>{EMSTMSSource.typeDisplayName}</strong> <strong>{EMSTMSSource.typeDisplayName}</strong>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">Tile services hosted by EMS.</p> <p className="euiTextColor--subdued">
Map tiles from Elastic Maps Service
</p>
</EuiText> </EuiText>
</Fragment> </Fragment>
); );

View file

@ -91,8 +91,7 @@ export class ESGeohashGridSource extends VectorSource {
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued"> <p className="euiTextColor--subdued">
Group documents into grid cells and display metrics for each cell. Group geospatial data in grids with metrics for each gridded cell
Great for displaying large datasets.
</p> </p>
</EuiText> </EuiText>
</Fragment> </Fragment>

View file

@ -50,7 +50,7 @@ export class ESSearchSource extends VectorSource {
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued"> <p className="euiTextColor--subdued">
Display documents from an elasticsearch index. Geospatial data from an Elasticsearch index
</p> </p>
</EuiText> </EuiText>
</Fragment>); </Fragment>);

View file

@ -16,7 +16,7 @@ import {
export class KibanaRegionmapSource extends VectorSource { export class KibanaRegionmapSource extends VectorSource {
static type = 'REGIONMAP_FILE'; static type = 'REGIONMAP_FILE';
static typeDisplayName = 'Custom region boundaries'; static typeDisplayName = 'Custom vector shapes';
constructor(descriptor, { ymlFileLayers }) { constructor(descriptor, { ymlFileLayers }) {
super(descriptor); super(descriptor);
@ -62,7 +62,7 @@ export class KibanaRegionmapSource extends VectorSource {
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued"> <p className="euiTextColor--subdued">
Region map boundary layers configured in your config/kibana.yml file. Vector shapes from static files configured in kibana.yml.
</p> </p>
</EuiText> </EuiText>
</Fragment> </Fragment>

View file

@ -16,7 +16,7 @@ import {
export class KibanaTilemapSource extends TMSSource { export class KibanaTilemapSource extends TMSSource {
static type = 'KIBANA_TILEMAP'; static type = 'KIBANA_TILEMAP';
static typeDisplayName = 'TMS from kibana.yml'; static typeDisplayName = 'Custom Tile Map Service';
static createDescriptor(url) { static createDescriptor(url) {
return { return {
@ -41,7 +41,9 @@ export class KibanaTilemapSource extends TMSSource {
<strong>{KibanaTilemapSource.typeDisplayName}</strong> <strong>{KibanaTilemapSource.typeDisplayName}</strong>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">TMS service configured in kibana.yml</p> <p className="euiTextColor--subdued">
Map tiles configured in kibana.yml
</p>
</EuiText> </EuiText>
</Fragment> </Fragment>
); );

View file

@ -57,17 +57,14 @@ export class VectorSource extends ASource {
} }
isFilterByMapBounds() { isFilterByMapBounds() {
console.warn('Should implement VectorSource#isFilterByMapBounds');
return false; return false;
} }
async getNumberFields() { async getNumberFields() {
console.warn('Should implement VectorSource#getNumberFields');
return []; return [];
} }
async getStringFields() { async getStringFields() {
console.warn('Should implement VectorSource@getStringFields');
return []; return [];
} }
@ -93,7 +90,7 @@ export class VectorSource extends ASource {
} }
async isTimeAware() { async isTimeAware() {
throw new Error('Should implement'); return false;
} }
} }

View file

@ -20,7 +20,7 @@ export class WMSSource extends TMSSource {
static type = 'WMS'; static type = 'WMS';
static typeDisplayName = 'WMS'; static typeDisplayName = 'Web Map Service';
static createDescriptor({ serviceUrl, layers, styles }) { static createDescriptor({ serviceUrl, layers, styles }) {
return { return {
@ -46,7 +46,9 @@ export class WMSSource extends TMSSource {
<strong>{WMSSource.typeDisplayName}</strong> <strong>{WMSSource.typeDisplayName}</strong>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">Web Map Service (WMS)</p> <p className="euiTextColor--subdued">
Maps from OGC Standard WMS
</p>
</EuiText> </EuiText>
</Fragment> </Fragment>
); );

View file

@ -20,7 +20,7 @@ export class XYZTMSSource extends TMSSource {
static type = 'EMS_XYZ'; static type = 'EMS_XYZ';
static typeDisplayName = 'TMS XYZ'; static typeDisplayName = 'Tile Map Service from URL';
static createDescriptor(urlTemplate) { static createDescriptor(urlTemplate) {
return { return {
@ -44,7 +44,9 @@ export class XYZTMSSource extends TMSSource {
<strong>{XYZTMSSource.typeDisplayName}</strong> <strong>{XYZTMSSource.typeDisplayName}</strong>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="s" color="subdued"> <EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">Tile Map Service with XYZ url.</p> <p className="euiTextColor--subdued">
Map tiles from a URL that includes the XYZ coordinates
</p>
</EuiText> </EuiText>
</Fragment> </Fragment>
); );

View file

@ -223,7 +223,7 @@ export class VectorLayer extends ALayer {
join: join join: join
}; };
} }
startLoading(sourceDataId, requestToken, { timeFilters: dataFilters.timeFilters }); startLoading(sourceDataId, requestToken, dataFilters);
const leftSourceName = await this.getSourceName(); const leftSourceName = await this.getSourceName();
const { const {
rawData, rawData,

View file

@ -17,5 +17,5 @@ export const enrichResponse = async (response, callWithRequest) => {
// silently swallow enricher response errors // silently swallow enricher response errors
} }
} }
return response; return enrichedResponse;
}; };

View file

@ -75,6 +75,10 @@
min-width: 150px; min-width: 150px;
} }
.mlAnomalyCategoryExamples__header {
display: inline;
}
.mlAnomalyCategoryExamples__link { .mlAnomalyCategoryExamples__link {
width: 100%; width: 100%;
} }

View file

@ -29,9 +29,11 @@ import { AnomalyDetails } from './anomaly_details';
import { mlTableService } from '../../services/table_service'; import { mlTableService } from '../../services/table_service';
import { RuleEditorFlyout } from '../../components/rule_editor'; import { RuleEditorFlyout } from '../../components/rule_editor';
import { ml } from '../../services/ml_api_service';
import { import {
INFLUENCERS_LIMIT, INFLUENCERS_LIMIT,
ANOMALIES_TABLE_TABS ANOMALIES_TABLE_TABS,
MAX_CHARS
} from './anomalies_table_constants'; } from './anomalies_table_constants';
class AnomaliesTable extends Component { class AnomaliesTable extends Component {
@ -71,18 +73,36 @@ class AnomaliesTable extends Component {
return null; return null;
} }
toggleRow = (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => { toggleRow = async (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
if (itemIdToExpandedRowMap[item.rowId]) { if (itemIdToExpandedRowMap[item.rowId]) {
delete itemIdToExpandedRowMap[item.rowId]; delete itemIdToExpandedRowMap[item.rowId];
} else { } else {
const examples = (item.entityName === 'mlcategory') ? const examples = (item.entityName === 'mlcategory') ?
_.get(this.props.tableData, ['examplesByJobId', item.jobId, item.entityValue]) : undefined; _.get(this.props.tableData, ['examplesByJobId', item.jobId, item.entityValue]) : undefined;
let definition = undefined;
if (examples !== undefined) {
try {
definition = await ml.results.getCategoryDefinition(item.jobId, item.source.mlcategory[0]);
if (definition.terms && definition.terms.length > MAX_CHARS) {
definition.terms = `${definition.terms.substring(0, MAX_CHARS)}...`;
}
if (definition.regex && definition.regex.length > MAX_CHARS) {
definition.terms = `${definition.regex.substring(0, MAX_CHARS)}...`;
}
} catch(error) {
console.log('Error fetching category definition for row item.', error);
}
}
itemIdToExpandedRowMap[item.rowId] = ( itemIdToExpandedRowMap[item.rowId] = (
<AnomalyDetails <AnomalyDetails
tabIndex={tab} tabIndex={tab}
anomaly={item} anomaly={item}
examples={examples} examples={examples}
definition={definition}
isAggregatedData={this.isShowingAggregatedData()} isAggregatedData={this.isShowingAggregatedData()}
filter={this.props.filter} filter={this.props.filter}
influencersLimit={INFLUENCERS_LIMIT} influencersLimit={INFLUENCERS_LIMIT}

View file

@ -13,3 +13,5 @@ export const ANOMALIES_TABLE_TABS = {
DETAILS: 0, DETAILS: 0,
CATEGORY_EXAMPLES: 1 CATEGORY_EXAMPLES: 1
}; };
export const MAX_CHARS = 500;

View file

@ -19,6 +19,7 @@ import {
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiIcon, EuiIcon,
EuiIconTip,
EuiLink, EuiLink,
EuiSpacer, EuiSpacer,
EuiTabbedContent, EuiTabbedContent,
@ -35,6 +36,7 @@ import {
} from '../../../common/util/anomaly_utils'; } from '../../../common/util/anomaly_utils';
import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact';
import { formatValue } from '../../formatters/format_value'; import { formatValue } from '../../formatters/format_value';
import { MAX_CHARS } from './anomalies_table_constants';
const TIME_FIELD_NAME = 'timestamp'; const TIME_FIELD_NAME = 'timestamp';
@ -218,6 +220,8 @@ export class AnomalyDetails extends Component {
} }
renderCategoryExamples() { renderCategoryExamples() {
const { examples, definition } = this.props;
return ( return (
<EuiFlexGroup <EuiFlexGroup
direction="column" direction="column"
@ -225,9 +229,54 @@ export class AnomalyDetails extends Component {
gutterSize="m" gutterSize="m"
className="mlAnomalyCategoryExamples" className="mlAnomalyCategoryExamples"
> >
{this.props.examples.map((example, i) => { {(definition !== undefined && definition.terms) &&
<Fragment>
<EuiFlexItem key={`example-terms`}>
<EuiText size="xs">
<h4 className="mlAnomalyCategoryExamples__header">Terms</h4>&nbsp;
<EuiIconTip
aria-label="Description"
type="questionInCircle"
color="subdued"
size="s"
content={`A space separated list of the common tokens that are matched in values of the category
(may have been truncated to a max character limit of ${MAX_CHARS})`}
/>
</EuiText>
<EuiText size="xs">
{definition.terms}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="m" />
</Fragment> }
{(definition !== undefined && definition.regex) &&
<Fragment>
<EuiFlexItem key={`example-regex`}>
<EuiText size="xs">
<h4 className="mlAnomalyCategoryExamples__header">Regex</h4>&nbsp;
<EuiIconTip
aria-label="Description"
type="questionInCircle"
color="subdued"
size="s"
content={`The regular expression that is used to search for values that match the category
(may have been truncated to a max character limit of ${MAX_CHARS})`}
/>
</EuiText>
<EuiText size="xs">
{definition.regex}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="l" />
</Fragment>}
{examples.map((example, i) => {
return ( return (
<EuiFlexItem key={`example${i}`}> <EuiFlexItem key={`example${i}`}>
{(i === 0 && definition !== undefined) &&
<EuiText size="s">
<h4>Examples</h4>
</EuiText>}
<span className="mlAnomalyCategoryExamples__item">{example}</span> <span className="mlAnomalyCategoryExamples__item">{example}</span>
</EuiFlexItem> </EuiFlexItem>
); );
@ -384,6 +433,7 @@ export class AnomalyDetails extends Component {
AnomalyDetails.propTypes = { AnomalyDetails.propTypes = {
anomaly: PropTypes.object.isRequired, anomaly: PropTypes.object.isRequired,
examples: PropTypes.array, examples: PropTypes.array,
definition: PropTypes.object,
isAggregatedData: PropTypes.bool, isAggregatedData: PropTypes.bool,
filter: PropTypes.func, filter: PropTypes.func,
influencersLimit: PropTypes.number, influencersLimit: PropTypes.number,

View file

@ -6,7 +6,7 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, mount } from 'enzyme';
import { AnomalyDetails } from './anomaly_details'; import { AnomalyDetails } from './anomaly_details';
const props = { const props = {
@ -86,4 +86,75 @@ describe('AnomalyDetails', () => {
); );
expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples'); expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples');
}); });
test('Renders with terms and regex when definition prop is not undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
terms: 'example terms for test',
regex: '.*?DBMS.+?ERROR.+?svc_prod.+?Err.+?Microsoft.+?ODBC.+?SQL.+?Server.+?Driver'
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(true);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(true);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
test('Renders only with examples when definition prop is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: undefined
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(false);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(false);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(false);
});
test('Renders only with terms when definition.regex is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
terms: 'example terms for test',
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(false);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(true);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
test('Renders only with regex when definition.terms is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
regex: '.*?DBMS.+?ERROR.+?svc_prod.+?Err.+?Microsoft.+?ODBC.+?SQL.+?Server.+?Driver'
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(true);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(false);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
}); });

Some files were not shown because too many files have changed in this diff Show more