Merge remote-tracking branch 'origin/master' into feature/merge-code
This commit is contained in:
commit
e7f9090060
|
@ -5,7 +5,7 @@ bower_components
|
|||
/.es
|
||||
/plugins
|
||||
/optimize
|
||||
/dlls
|
||||
/built_assets
|
||||
/src/fixtures/vislib/mock_data
|
||||
/src/ui/public/angular-bootstrap
|
||||
/src/ui/public/flot-charts
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -9,7 +9,7 @@ node_modules
|
|||
!/src/dev/notice/__fixtures__/node_modules
|
||||
trash
|
||||
/optimize
|
||||
/dlls
|
||||
/built_assets
|
||||
target
|
||||
/build
|
||||
.jruby
|
||||
|
@ -44,4 +44,3 @@ package-lock.json
|
|||
*.sublime-*
|
||||
npm-debug.log*
|
||||
.tern-project
|
||||
**/public/index.css
|
||||
|
|
|
@ -39,6 +39,7 @@ document.
|
|||
`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
|
||||
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
|
||||
might increase the search time.
|
||||
`courier:maxSegmentCount`:: Kibana splits requests in the Discover app into segments to limit the size of requests sent to
|
||||
|
|
|
@ -94,13 +94,13 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless
|
|||
=== Creating 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.
|
||||
. Go to the pattern's *Scripted Fields* tab.
|
||||
. Click *Add Scripted Field*.
|
||||
. Go to the pattern's *Scripted fields* tab.
|
||||
. Click *Add 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.
|
||||
. Click *Save Scripted Field*.
|
||||
. Click *Create field*.
|
||||
|
||||
For more information about scripted fields in Elasticsearch, see
|
||||
{ref}/modules-scripting.html[Scripting].
|
||||
|
@ -110,9 +110,10 @@ For more information about scripted fields in Elasticsearch, see
|
|||
=== Updating 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.
|
||||
. 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
|
||||
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
|
||||
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.
|
||||
. Confirm that you really want to delete the field.
|
||||
. Click *Delete* in the confirmation window.
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"extraPatterns": [
|
||||
"build",
|
||||
"optimize",
|
||||
"dlls",
|
||||
"built_assets",
|
||||
".eslintcache"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ export const CleanClientModulesOnDLLTask = {
|
|||
];
|
||||
|
||||
// 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
|
||||
// from any whitelisted module
|
||||
|
|
|
@ -34,7 +34,7 @@ export const TranspileScssTask = {
|
|||
const uiExports = collectUiExports(enabledPlugins);
|
||||
|
||||
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}`));
|
||||
} catch (error) {
|
||||
const { message, line, file } = error;
|
||||
|
|
|
@ -42,7 +42,7 @@ export async function generateNoticeFromSource({ productName, directory, log })
|
|||
cwd: directory,
|
||||
nodir: true,
|
||||
ignore: [
|
||||
'{node_modules,build,target,dist,optimize,dlls}/**',
|
||||
'{node_modules,build,target,dist,optimize,built_assets}/**',
|
||||
'packages/*/{node_modules,build,target,dist}/**',
|
||||
'x-pack/{node_modules,build,target,dist,optimize}/**',
|
||||
'x-pack/packages/*/{node_modules,build,target,dist}/**',
|
||||
|
|
|
@ -380,7 +380,8 @@ function discoverController(
|
|||
}
|
||||
|
||||
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 {
|
||||
searchFields: fields,
|
||||
selectFields: fields
|
||||
|
|
|
@ -158,11 +158,12 @@ export function onPremInstructions(apmIndexPattern) {
|
|||
index: apmIndexPattern,
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: 'processor.name',
|
||||
},
|
||||
},
|
||||
should: [
|
||||
{ term: { 'processor.name': 'error' } },
|
||||
{ term: { 'processor.name': 'transaction' } },
|
||||
{ term: { 'processor.name': 'metric' } },
|
||||
{ term: { 'processor.name': 'sourcemap' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -269,6 +269,16 @@ export function getUiSettingDefaults() {
|
|||
}),
|
||||
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': {
|
||||
name: i18n.translate('kbn.advancedSettings.courier.maxSegmentCountTitle', {
|
||||
defaultMessage: 'Maximum segment count',
|
||||
|
|
|
@ -70,7 +70,7 @@ export default (kibana) => {
|
|||
}
|
||||
|
||||
testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`);
|
||||
testGlobs.push(`${plugin.publicDir}/**/*.css`);
|
||||
testGlobs.push(`built_assets/css/plugins/${plugin.id}/**/*.css`);
|
||||
});
|
||||
} else {
|
||||
// add the modules from all of the apps
|
||||
|
@ -80,7 +80,7 @@ export default (kibana) => {
|
|||
|
||||
for (const plugin of plugins) {
|
||||
testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`);
|
||||
testGlobs.push(`${plugin.publicDir}/**/*.css`);
|
||||
testGlobs.push(`built_assets/css/plugins/${plugin.id}/**/*.css`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ export class VegaParser {
|
|||
delete this.spec.width;
|
||||
delete this.spec.height;
|
||||
} 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}',
|
||||
values: {
|
||||
autosizeParam: 'autosize=fit',
|
||||
|
|
|
@ -42,7 +42,7 @@ VisTypesRegistryProvider.register((Private) => {
|
|||
return VisFactory.createBaseVisualization({
|
||||
name: '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',
|
||||
description: 'Vega and Vega-Lite are product names and should not be translated',
|
||||
}),
|
||||
|
|
|
@ -59,7 +59,7 @@ export function createBundlesRoute({ regularBundlesPath, dllBundlesPath, basePub
|
|||
|
||||
return [
|
||||
buildRouteForBundles(basePublicPath, '/bundles/', regularBundlesPath, fileHashCache),
|
||||
buildRouteForBundles(basePublicPath, '/dlls/', dllBundlesPath, fileHashCache),
|
||||
buildRouteForBundles(basePublicPath, '/built_assets/dlls/', dllBundlesPath, fileHashCache),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
export function createProxyBundlesRoute({ host, port }) {
|
||||
return [
|
||||
buildProxyRouteForBundles('/bundles/', host, port),
|
||||
buildProxyRouteForBundles('/dlls/', host, port)
|
||||
buildProxyRouteForBundles('/built_assets/dlls/', host, port)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ export class DllCompiler {
|
|||
dllExt: '.bundle.dll.js',
|
||||
manifestExt: '.manifest.dll.json',
|
||||
styleExt: '.style.dll.css',
|
||||
outputPath: fromRoot('./dlls'),
|
||||
outputPath: fromRoot('built_assets/dlls'),
|
||||
publicPath: PUBLIC_PATH_PLACEHOLDER
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export default async (kbnServer, server, config) => {
|
|||
// bundles in a "middleware" style.
|
||||
//
|
||||
// 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
|
||||
// to prevent complete rebuilds of the optimize content.
|
||||
const watch = config.get('optimize.watch');
|
||||
|
|
|
@ -33,7 +33,7 @@ export default async kbnServer => {
|
|||
* while the optimizer is running
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
|
5
src/server/sass/__fixtures__/index.scss
Normal file
5
src/server/sass/__fixtures__/index.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
foo {
|
||||
bar {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -23,30 +23,24 @@ import fs from 'fs';
|
|||
import sass from 'node-sass';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import postcss from 'postcss';
|
||||
import mkdirp from 'mkdirp';
|
||||
|
||||
const renderSass = promisify(sass.render);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
const mkdirpAsync = promisify(mkdirp);
|
||||
|
||||
export class Build {
|
||||
constructor(source, log) {
|
||||
constructor(source, log, targetPath) {
|
||||
this.source = source;
|
||||
this.log = log;
|
||||
this.targetPath = targetPath;
|
||||
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
|
||||
*/
|
||||
|
||||
getGlob() {
|
||||
return path.join(path.dirname(this.source), '**', '*.s{a,c}ss');
|
||||
}
|
||||
|
||||
async buildIfIncluded(path) {
|
||||
if (this.includedFiles && this.includedFiles.includes(path)) {
|
||||
await this.build();
|
||||
|
@ -61,11 +55,9 @@ export class Build {
|
|||
*/
|
||||
|
||||
async build() {
|
||||
const outFile = this.outputPath();
|
||||
|
||||
const rendered = await renderSass({
|
||||
file: this.source,
|
||||
outFile,
|
||||
outFile: this.targetPath,
|
||||
sourceMap: true,
|
||||
sourceMapEmbed: true,
|
||||
includePaths: [
|
||||
|
@ -78,7 +70,8 @@ export class Build {
|
|||
|
||||
this.includedFiles = rendered.stats.includedFiles;
|
||||
|
||||
await writeFile(outFile, prefixed.css);
|
||||
await mkdirpAsync(path.dirname(this.targetPath));
|
||||
await writeFile(this.targetPath, prefixed.css);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -17,34 +17,35 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import sass from 'node-sass';
|
||||
import { resolve } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import del from 'del';
|
||||
|
||||
import { Build } from './build';
|
||||
|
||||
jest.mock('node-sass');
|
||||
const TMP = resolve(__dirname, '__tmp__');
|
||||
const FIXTURE = resolve(__dirname, '__fixtures__/index.scss');
|
||||
|
||||
describe('SASS builder', () => {
|
||||
jest.mock('fs');
|
||||
afterEach(async () => {
|
||||
await del(TMP);
|
||||
});
|
||||
|
||||
it('generates a glob', () => {
|
||||
const builder = new Build('/foo/style.sass');
|
||||
expect(builder.getGlob()).toEqual(path.join('/foo', '**', '*.s{a,c}ss'));
|
||||
});
|
||||
it('builds SASS', async () => {
|
||||
const cssPath = resolve(TMP, 'style.css');
|
||||
await (new Build(FIXTURE, {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
}, cssPath)).build();
|
||||
|
||||
it('builds SASS', () => {
|
||||
sass.render.mockImplementation(() => Promise.resolve(null, { css: 'test' }));
|
||||
const builder = new Build('/foo/style.sass');
|
||||
builder.build();
|
||||
|
||||
const sassCall = sass.render.mock.calls[0][0];
|
||||
expect(sassCall.file).toEqual('/foo/style.sass');
|
||||
expect(sassCall.outFile).toEqual(path.join('/foo', 'style.css'));
|
||||
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'));
|
||||
});
|
||||
});
|
||||
expect(readFileSync(cssPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"foo bar {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex; }
|
||||
/*# sourceMappingURL=... */"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -17,16 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
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 => {
|
||||
|
||||
if (!styleSheet.localPath.endsWith('.scss')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = new Build(styleSheet.localPath, log);
|
||||
const bundle = new Build(styleSheet.localPath, log, resolve(buildDir, styleSheet.publicPath));
|
||||
await bundle.build();
|
||||
|
||||
return bundle;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IS_KIBANA_DISTRIBUTABLE } from '../../utils';
|
||||
import { IS_KIBANA_DISTRIBUTABLE, fromRoot } from '../../utils';
|
||||
|
||||
export async function sassMixin(kbnServer, server, config) {
|
||||
if (process.env.kbnWorkerType === 'optmzr') {
|
||||
|
@ -45,7 +45,7 @@ export async function sassMixin(kbnServer, server, config) {
|
|||
};
|
||||
|
||||
try {
|
||||
scssBundles = await buildAll(kbnServer.uiExports.styleSheetPaths, log);
|
||||
scssBundles = await buildAll(kbnServer.uiExports.styleSheetPaths, log, fromRoot('built_assets/css'));
|
||||
|
||||
scssBundles.forEach(bundle => {
|
||||
bundle.includedFiles.forEach(file => trackedFiles.add(file));
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
// 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`] = `
|
||||
Array [
|
||||
"bulk",
|
||||
|
|
|
@ -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 () => {
|
||||
const callCluster = sinon.spy(async (path: string, { index }: any) => {
|
||||
return {
|
||||
|
|
|
@ -65,7 +65,7 @@ export async function fetchInfo(callCluster: CallCluster, index: string): Promis
|
|||
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* some rudimentary expectations. Past Kibana indices had multiple root documents, etc
|
||||
|
@ -296,7 +316,7 @@ export async function claimAlias(
|
|||
*
|
||||
* @param {FullIndexInfo} indexInfo
|
||||
*/
|
||||
async function assertIsSupportedIndex(indexInfo: FullIndexInfo) {
|
||||
function assertIsSupportedIndex(indexInfo: FullIndexInfo) {
|
||||
const currentTypes = getTypes(indexInfo.mappings);
|
||||
const isV5Index = currentTypes.length > 1 || currentTypes[0] !== ROOT_TYPE;
|
||||
if (isV5Index) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<tr>
|
||||
<td width="1%"></td>
|
||||
<th
|
||||
ng-if="indexPattern.timeFieldName"
|
||||
ng-if="indexPattern.timeFieldName && !hideTimeColumn"
|
||||
data-test-subj="docTableHeaderField"
|
||||
scope="col"
|
||||
>
|
||||
|
|
|
@ -36,7 +36,9 @@ module.directive('kbnTableHeader', function (shortDotsFilter) {
|
|||
onMoveColumn: '=?',
|
||||
},
|
||||
template: headerHtml,
|
||||
controller: function ($scope) {
|
||||
controller: function ($scope, config) {
|
||||
$scope.hideTimeColumn = config.get('doc_table:hideTimeColumn');
|
||||
|
||||
$scope.isSortableColumn = function isSortableColumn(columnName) {
|
||||
return (
|
||||
!!$scope.indexPattern
|
||||
|
|
|
@ -45,7 +45,7 @@ const MIN_LINE_LENGTH = 20;
|
|||
* <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 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;
|
||||
if (indexPattern.timeFieldName) {
|
||||
const hideTimeColumn = config.get('doc_table:hideTimeColumn');
|
||||
if (indexPattern.timeFieldName && !hideTimeColumn) {
|
||||
newHtmls.push(cellTemplate({
|
||||
timefield: true,
|
||||
formatted: _displayField(row, indexPattern.timeFieldName),
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { flatConcatAtType } from './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
|
||||
let relativePath = path.relative(publicDir, localPath);
|
||||
let relativePath = path.relative(publicDir, localCssPath);
|
||||
|
||||
// replace back slashes on windows
|
||||
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}`;
|
||||
|
||||
return {
|
||||
|
@ -64,4 +71,4 @@ function normalize(localPath, type, pluginSpec) {
|
|||
};
|
||||
}
|
||||
|
||||
export const styleSheetPaths = wrap(mapSpec(normalize), flatConcatAtType);
|
||||
export const styleSheetPaths = wrap(mapSpec(normalize), flatConcatAtType);
|
||||
|
|
|
@ -23,6 +23,7 @@ import { resolve } from 'path';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { AppBootstrap } from './bootstrap';
|
||||
import { mergeVariables } from './lib';
|
||||
import { fromRoot } from '../../utils';
|
||||
|
||||
export function uiRenderMixin(kbnServer, server, config) {
|
||||
function replaceInjectedVars(request, injectedVars) {
|
||||
|
@ -50,6 +51,9 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
// render all views from ./views
|
||||
server.setupViews(resolve(__dirname, 'views'));
|
||||
|
||||
// expose built css
|
||||
server.exposeStaticDir('/built_assets/css/{path*}', fromRoot('built_assets/css'));
|
||||
|
||||
server.route({
|
||||
path: '/bundles/app/{id}/bootstrap.js',
|
||||
method: 'GET',
|
||||
|
@ -63,12 +67,19 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
|
||||
const basePath = config.get('server.basePath');
|
||||
const regularBundlePath = `${basePath}/bundles`;
|
||||
const dllBundlePath = `${basePath}/dlls`;
|
||||
const dllBundlePath = `${basePath}/built_assets/dlls`;
|
||||
const styleSheetPaths = [
|
||||
`${dllBundlePath}/vendors.style.dll.css`,
|
||||
`${regularBundlePath}/commons.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({
|
||||
templateData: {
|
||||
|
|
|
@ -85,17 +85,17 @@ module.exports = function (grunt) {
|
|||
|
||||
// list of files / patterns to load in the browser
|
||||
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/dlls/vendors.style.dll.css',
|
||||
'http://localhost:5610/built_assets/dlls/vendors.style.dll.css',
|
||||
'http://localhost:5610/bundles/tests.style.css'
|
||||
],
|
||||
|
||||
proxies: {
|
||||
'/tests/': 'http://localhost:5610/tests/',
|
||||
'/bundles/': 'http://localhost:5610/bundles/',
|
||||
'/dlls/': 'http://localhost:5610/dlls/'
|
||||
'/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/'
|
||||
},
|
||||
|
||||
client: {
|
||||
|
@ -176,10 +176,10 @@ module.exports = function (grunt) {
|
|||
singleRun: true,
|
||||
options: {
|
||||
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/dlls/vendors.style.dll.css',
|
||||
'http://localhost:5610/built_assets/dlls/vendors.style.dll.css',
|
||||
'http://localhost:5610/bundles/tests.style.css'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,12 +20,12 @@ const homeTabs: IHistoryTab[] = [
|
|||
{
|
||||
path: '/services',
|
||||
name: 'Services',
|
||||
component: ServiceOverview
|
||||
render: props => <ServiceOverview {...props} />
|
||||
},
|
||||
{
|
||||
path: '/traces',
|
||||
name: 'Traces',
|
||||
component: TraceOverview
|
||||
render: props => <TraceOverview {...props} />
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -39,14 +39,14 @@ exports[`Home component should render 1`] = `
|
|||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"name": "Services",
|
||||
"path": "/services",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"name": "Traces",
|
||||
"path": "/traces",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
name: 'Transactions',
|
||||
path: `/${serviceName}/transactions/${transactionTypes[0]}`,
|
||||
routePath: `/${serviceName}/transactions/:transactionType?`,
|
||||
component: () => (
|
||||
render: () => (
|
||||
<TransactionOverview
|
||||
urlParams={urlParams}
|
||||
serviceTransactionTypes={transactionTypes}
|
||||
|
@ -36,7 +36,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
{
|
||||
name: 'Errors',
|
||||
path: `/${serviceName}/errors`,
|
||||
component: () => {
|
||||
render: () => {
|
||||
return (
|
||||
<ErrorGroupOverview urlParams={urlParams} location={location} />
|
||||
);
|
||||
|
@ -45,7 +45,7 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
{
|
||||
name: 'Metrics',
|
||||
path: `/${serviceName}/metrics`,
|
||||
component: () => <ServiceMetrics urlParams={urlParams} />
|
||||
render: () => <ServiceMetrics urlParams={urlParams} />
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -90,26 +90,19 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
};
|
||||
|
||||
public addErrorToast = () => {
|
||||
const { location, urlParams } = this.props;
|
||||
const { serviceName = 'unknown', transactionType } = urlParams;
|
||||
const { urlParams } = this.props;
|
||||
const { serviceName = 'unknown' } = urlParams;
|
||||
|
||||
if (!serviceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastNotifications.addWarning({
|
||||
title: 'Job already exists',
|
||||
title: 'Job creation failed',
|
||||
text: (
|
||||
<p>
|
||||
There's already a job running for anomaly detection on{' '}
|
||||
{serviceName} ({transactionType}).{' '}
|
||||
<ViewMLJob
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={location}
|
||||
>
|
||||
View existing job
|
||||
</ViewMLJob>
|
||||
Your current license may not allow for creating machine learning jobs,
|
||||
or this job may already exist.
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -70,12 +70,13 @@ const traceListColumns: ITableColumn[] = [
|
|||
];
|
||||
|
||||
export function TraceList({ items = [], noItemsMessage, isLoading }: Props) {
|
||||
return isLoading ? null : (
|
||||
const noItems = isLoading ? null : noItemsMessage;
|
||||
return (
|
||||
<ManagedTable
|
||||
columns={traceListColumns}
|
||||
items={items}
|
||||
initialSort={{ field: 'impact', direction: 'desc' }}
|
||||
noItemsMessage={noItemsMessage}
|
||||
noItemsMessage={noItems}
|
||||
initialPageSize={25}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ exports[`TransactionOverviewView should render with type filter controls 1`] = `
|
|||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Filter by type"
|
||||
>
|
||||
<EuiSelect
|
||||
compressed={false}
|
||||
|
@ -16,11 +17,11 @@ exports[`TransactionOverviewView should render with type filter controls 1`] = `
|
|||
options={
|
||||
Array [
|
||||
Object {
|
||||
"text": "Filter by type: a",
|
||||
"text": "a",
|
||||
"value": "a",
|
||||
},
|
||||
Object {
|
||||
"text": "Filter by type: b",
|
||||
"text": "b",
|
||||
"value": "b",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -48,10 +48,10 @@ export class TransactionOverviewView extends React.Component<
|
|||
return (
|
||||
<React.Fragment>
|
||||
{serviceTransactionTypes.length > 1 ? (
|
||||
<EuiFormRow>
|
||||
<EuiFormRow label="Filter by type">
|
||||
<EuiSelect
|
||||
options={serviceTransactionTypes.map(type => ({
|
||||
text: `Filter by type: ${type}`,
|
||||
text: `${type}`,
|
||||
value: type
|
||||
}))}
|
||||
value={transactionType}
|
||||
|
|
|
@ -40,17 +40,17 @@ describe('HistoryTabs', () => {
|
|||
{
|
||||
name: 'One',
|
||||
path: '/one',
|
||||
component: () => <Content name="one" />
|
||||
render: props => <Content {...props} name="one" />
|
||||
},
|
||||
{
|
||||
name: 'Two',
|
||||
path: '/two',
|
||||
component: () => <Content name="two" />
|
||||
render: () => <Content name="two" />
|
||||
},
|
||||
{
|
||||
name: 'Three',
|
||||
path: '/three',
|
||||
component: () => <Content name="three" />
|
||||
render: () => <Content name="three" />
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -55,19 +55,19 @@ exports[`HistoryTabs should render correctly 1`] = `
|
|||
size="l"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
key="/one"
|
||||
path="/one"
|
||||
render={[Function]}
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
key="/two"
|
||||
path="/two"
|
||||
render={[Function]}
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
key="/three"
|
||||
path="/three"
|
||||
render={[Function]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface IHistoryTab {
|
|||
path: string;
|
||||
routePath?: string;
|
||||
name: React.ReactNode;
|
||||
component?: React.SFC | React.ComponentClass;
|
||||
render?: (props: RouteComponentProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface HistoryTabsProps extends RouteComponentProps {
|
||||
|
@ -51,10 +51,10 @@ const HistoryTabsWithoutRouter = ({
|
|||
</EuiTabs>
|
||||
<EuiSpacer />
|
||||
{tabs.map(tab =>
|
||||
tab.component ? (
|
||||
tab.render ? (
|
||||
<Route
|
||||
path={tab.routePath || tab.path}
|
||||
component={tab.component}
|
||||
render={tab.render}
|
||||
key={tab.path}
|
||||
/>
|
||||
) : null
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { PROCESSOR_NAME } from '../../../common/constants';
|
||||
|
||||
// Note: this logic is duplicated in tutorials/apm/envs/on_prem
|
||||
export async function getAgentStatus({ setup }) {
|
||||
const { client, config } = setup;
|
||||
|
||||
|
@ -18,11 +19,12 @@ export async function getAgentStatus({ setup }) {
|
|||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: PROCESSOR_NAME
|
||||
}
|
||||
}
|
||||
should: [
|
||||
{ term: { [PROCESSOR_NAME]: 'error' } },
|
||||
{ term: { [PROCESSOR_NAME]: 'transaction' } },
|
||||
{ term: { [PROCESSOR_NAME]: 'metric' } },
|
||||
{ term: { [PROCESSOR_NAME]: 'sourcemap' } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 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 }) {
|
||||
const { client, config } = setup;
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ describe('alterColumn', () => {
|
|||
|
||||
it('throws when converting to an invalid type', () => {
|
||||
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'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -59,7 +59,7 @@ describe('axisConfig', () => {
|
|||
expect(fn)
|
||||
.withArgs(testTable, { position: 'foo' })
|
||||
.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' })
|
||||
.to.throwException(e => {
|
||||
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' })
|
||||
.to.throwException(e => {
|
||||
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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,10 +24,14 @@ describe('compare', () => {
|
|||
|
||||
it('throws when invalid op is provided', () => {
|
||||
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(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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('font', () => {
|
|||
|
||||
it('throws when provided an invalid weight', () => {
|
||||
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)
|
||||
.withArgs(null, { align: 'foo' })
|
||||
.to.throwException(e => {
|
||||
expect(e.message).to.be('Invalid text alignment: foo');
|
||||
expect(e.message).to.be(`Invalid text alignment: 'foo'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('getCell', () => {
|
|||
|
||||
it('throws when invalid column is provided', () => {
|
||||
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;
|
||||
|
||||
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(e.message).to.be('Row not found: 0');
|
||||
expect(e.message).to.be(`Row not found: '0'`);
|
||||
});
|
||||
|
||||
expect(() => fn(emptyTable)).to.throwException(e => {
|
||||
expect(e.message).to.be('Row not found: 0');
|
||||
expect(e.message).to.be(`Row not found: '0'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -82,12 +82,12 @@ describe('ply', () => {
|
|||
it('throws when by is an invalid column', () => {
|
||||
expect(() => fn(testTable, { by: [''], expression: [averagePrice] })).to.throwException(
|
||||
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(
|
||||
e => {
|
||||
expect(e.message).to.be('No such column: foo');
|
||||
expect(e.message).to.be(`Column not found: 'foo'`);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('progress', () => {
|
|||
expect(fn)
|
||||
.withArgs(3)
|
||||
.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)
|
||||
.withArgs(value, { max: -0.5 })
|
||||
.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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('revealImage', () => {
|
|||
origin: 'top',
|
||||
})
|
||||
.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)
|
||||
|
@ -39,7 +39,9 @@ describe('revealImage', () => {
|
|||
origin: 'top',
|
||||
})
|
||||
.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`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -88,7 +88,7 @@ describe('timefilter', () => {
|
|||
|
||||
it('throws when provided an invalid date string', () => {
|
||||
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'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ export const alterColumn = () => ({
|
|||
case 'null':
|
||||
return () => null;
|
||||
default:
|
||||
throw new Error(`Cannot convert to ${type}`);
|
||||
throw new Error(`Cannot convert to '${type}'`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ export const axisConfig = () => ({
|
|||
fn: (context, args) => {
|
||||
const positions = ['top', 'bottom', 'left', 'right', ''];
|
||||
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;
|
||||
|
@ -51,16 +51,16 @@ export const axisConfig = () => ({
|
|||
|
||||
if (min != null && isNaN(min)) {
|
||||
throw new Error(
|
||||
`Invalid date string '${
|
||||
`Invalid date string: '${
|
||||
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)) {
|
||||
throw new Error(
|
||||
`Invalid date string '${
|
||||
`Invalid date string: '${
|
||||
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`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ export const compare = () => ({
|
|||
}
|
||||
return false;
|
||||
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;
|
||||
|
|
|
@ -80,10 +80,10 @@ export const font = () => ({
|
|||
},
|
||||
fn: (context, args) => {
|
||||
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)) {
|
||||
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
|
||||
|
|
|
@ -26,14 +26,14 @@ export const getCell = () => ({
|
|||
fn: (context, args) => {
|
||||
const row = context.rows[args.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 value = row[column];
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error(`Column not found: ${column}`);
|
||||
throw new Error(`Column not found: '${column}'`);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
|
|
@ -95,7 +95,7 @@ export const ply = () => ({
|
|||
byColumns = args.by.map(by => {
|
||||
const column = context.columns.find(column => column.name === by);
|
||||
if (!column) {
|
||||
throw new Error(`No such column: ${by}`);
|
||||
throw new Error(`Column not found: '${by}'`);
|
||||
}
|
||||
return column;
|
||||
});
|
||||
|
|
|
@ -72,10 +72,10 @@ export const progress = () => ({
|
|||
},
|
||||
fn: (value, args) => {
|
||||
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) {
|
||||
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 = '';
|
||||
|
|
|
@ -35,7 +35,7 @@ export const revealImage = () => ({
|
|||
},
|
||||
fn: (percent, args) => {
|
||||
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 {
|
||||
|
|
|
@ -50,7 +50,7 @@ export const timefilter = () => ({
|
|||
|
||||
const moment = dateMath.parse(str);
|
||||
if (!moment || !moment.isValid()) {
|
||||
throw new Error(`Invalid date/time string ${str}`);
|
||||
throw new Error(`Invalid date/time string: '${str}'`);
|
||||
}
|
||||
return moment.toISOString();
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('demodata', () => {
|
|||
expect(fn)
|
||||
.withArgs(null, { type: 'foo' })
|
||||
.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'.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,5 +15,5 @@ export function getDemoRows(arg) {
|
|||
if (arg === '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'.`);
|
||||
}
|
||||
|
|
|
@ -6,52 +6,53 @@
|
|||
|
||||
export const americanTypewriter = {
|
||||
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 = {
|
||||
label: 'Baskerville',
|
||||
value: `Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif`,
|
||||
value: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif",
|
||||
};
|
||||
export const bookAntiqua = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
label: 'Helvetica Neue',
|
||||
value: `'Helvetica Neue', Helvetica, Arial, sans-serif`,
|
||||
value: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||||
};
|
||||
export const hoeflerText = {
|
||||
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 = {
|
||||
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 openSans = { label: 'Open Sans', value: `'Open Sans', 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 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 = {
|
||||
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 = [
|
||||
americanTypewriter,
|
||||
|
|
|
@ -33,7 +33,7 @@ export const routes = [
|
|||
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
|
||||
// https://github.com/elastic/kibana/issues/20277
|
||||
if (err.response.status === 403) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
dispatch(setCanUserWrite(false));
|
||||
}
|
||||
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
|
||||
// https://github.com/elastic/kibana/issues/20277
|
||||
workpadService.update(params.id, fetchedWorkpad).catch(err => {
|
||||
if (err.response.status === 403) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
dispatch(setCanUserWrite(false));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ export class Clipboard extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div className="canvas_clipboard" onClick={this.onClick}>
|
||||
<div className="canvasClipboard" onClick={this.onClick}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.canvasClipboard {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -10,13 +10,13 @@ import { render } from 'enzyme';
|
|||
import { Download } from '../';
|
||||
|
||||
describe('<Download />', () => {
|
||||
it('has canvas_download class', () => {
|
||||
it('has canvasDownload class', () => {
|
||||
const wrapper = render(
|
||||
<Download fileName="hello" content="world">
|
||||
<button>Download it</button>
|
||||
</Download>
|
||||
);
|
||||
|
||||
expect(wrapper.hasClass('canvas_download')).to.be.ok;
|
||||
expect(wrapper.hasClass('canvasDownload')).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ export class Download extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div className="canvas_download" onClick={this.onClick}>
|
||||
<div className="canvasDownload" onClick={this.onClick}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getWorkpad, getPages } from '../../state/selectors/workpad';
|
|||
import { getReportingBrowserType } from '../../state/selectors/app';
|
||||
import { notify } from '../../lib/notify';
|
||||
import { getWindow } from '../../lib/get_window';
|
||||
import { downloadWorkpad } from '../../lib/download_workpad';
|
||||
import { WorkpadExport as Component } from './workpad_export';
|
||||
import { getPdfUrl, createPdf } from './utils';
|
||||
|
||||
|
@ -43,29 +44,34 @@ export const WorkpadExport = compose(
|
|||
throw new Error(`Unknown export type: ${type}`);
|
||||
},
|
||||
onCopy: type => {
|
||||
if (type === 'pdf') {
|
||||
return notify.info('The PDF generation URL was copied to your clipboard.');
|
||||
switch (type) {
|
||||
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}`);
|
||||
},
|
||||
onExport: type => {
|
||||
if (type === 'pdf') {
|
||||
return createPdf(workpad, { pageCount })
|
||||
.then(({ data }) => {
|
||||
notify.info('Exporting PDF. You can track the progress in Management.', {
|
||||
title: `PDF export of workpad '${workpad.name}'`,
|
||||
switch (type) {
|
||||
case 'pdf':
|
||||
return createPdf(workpad, { pageCount })
|
||||
.then(({ data }) => {
|
||||
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}'` });
|
||||
});
|
||||
|
||||
// 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':
|
||||
return downloadWorkpad(workpad.id);
|
||||
default:
|
||||
throw new Error(`Unknown export type: ${type}`);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown export type: ${type}`);
|
||||
},
|
||||
}))
|
||||
)(Component);
|
||||
|
|
|
@ -9,12 +9,12 @@ import PropTypes from 'prop-types';
|
|||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
EuiHorizontalRule,
|
||||
EuiFormRow,
|
||||
EuiCode,
|
||||
EuiContextMenu,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { Popover } from '../popover';
|
||||
import { Clipboard } from '../clipboard';
|
||||
|
@ -27,81 +27,155 @@ export class WorkpadExport extends React.PureComponent {
|
|||
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 = () => {
|
||||
this.props.onExport('pdf');
|
||||
};
|
||||
|
||||
renderControls = closePopover => {
|
||||
downloadWorkpad = () => {
|
||||
this.props.onExport('json');
|
||||
};
|
||||
|
||||
renderPDFControls = closePopover => {
|
||||
const pdfUrl = this.props.getExportUrl('pdf');
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow>
|
||||
<EuiFormRow label="Click below to create a PDF. You'll be notified when the export is complete">
|
||||
<EuiButton
|
||||
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>
|
||||
<div className="canvasWorkpadExport__panelContent">
|
||||
<EuiText size="s">
|
||||
<p>PDFs can take a minute or two to generate based upon the size of your workpad</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<Clipboard
|
||||
content={pdfUrl}
|
||||
onCopy={() => {
|
||||
this.props.onCopy('pdf');
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
<EuiButtonIcon aria-label="Copy to clipboard" iconType="copy" />
|
||||
</Clipboard>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
{this.props.options}
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
this.exportPdf();
|
||||
}}
|
||||
size="s"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
Generate PDF
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
const reportingConfig = `xpack.reporting:
|
||||
enabled: true
|
||||
capture.browser.type: chromium`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
Export to PDF is disabled. You must configure reporting to use the Chromium browser. Add
|
||||
this to your kibana.yml file.
|
||||
<div className="canvasWorkpadExport__panelContent">
|
||||
<EuiText size="s">
|
||||
<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 />
|
||||
<EuiCodeBlock paddingSize="s" language="yml">
|
||||
xpack.reporting.capture.browser.type: chromium
|
||||
</EuiCodeBlock>
|
||||
<Clipboard content={reportingConfig} onCopy={() => this.props.onCopy('reportingConfig')}>
|
||||
<EuiCodeBlock
|
||||
className="canvasWorkpadExport__reportingConfig"
|
||||
paddingSize="s"
|
||||
fontSize="s"
|
||||
language="yml"
|
||||
>
|
||||
{reportingConfig}
|
||||
</EuiCodeBlock>
|
||||
</Clipboard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
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 (
|
||||
<Popover button={exportControl} tooltip="Export workpad" tooltipPosition="bottom">
|
||||
<Popover
|
||||
button={exportControl}
|
||||
panelPaddingSize="none"
|
||||
tooltip="Share workpad"
|
||||
tooltipPosition="bottom"
|
||||
>
|
||||
{({ closePopover }) => (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false} style={{ maxWidth: '300px' }}>
|
||||
{this.props.enabled && this.renderControls(closePopover)}
|
||||
{!this.props.enabled && this.renderDisabled()}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={this.flattenPanelTree(this.renderPanelTree(closePopover))}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
.canvasWorkpadExport__panelContent {
|
||||
padding: $euiSize;
|
||||
}
|
||||
.canvasWorkpadExport__reportingConfig {
|
||||
.euiCodeBlock__pre {
|
||||
@include euiScrollBar;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
|
@ -7,13 +7,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, withState, getContext, withHandlers } from 'recompose';
|
||||
import fileSaver from 'file-saver';
|
||||
import * as workpadService from '../../lib/workpad_service';
|
||||
import { notify } from '../../lib/notify';
|
||||
import { canUserWrite } from '../../state/selectors/app';
|
||||
import { getWorkpad } from '../../state/selectors/workpad';
|
||||
import { getId } from '../../lib/get_id';
|
||||
import { setCanUserWrite } from '../../state/actions/transient';
|
||||
import { downloadWorkpad } from '../../lib/download_workpad';
|
||||
import { WorkpadLoader as Component } from './workpad_loader';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -46,7 +46,7 @@ export const WorkpadLoader = compose(
|
|||
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
|
||||
// https://github.com/elastic/kibana/issues/20277
|
||||
if (err.response.status === 403) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
props.setCanUserWrite(false);
|
||||
}
|
||||
}
|
||||
|
@ -67,15 +67,7 @@ export const WorkpadLoader = compose(
|
|||
},
|
||||
|
||||
// Workpad import/export methods
|
||||
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` });
|
||||
}
|
||||
},
|
||||
downloadWorkpad: () => workpadId => downloadWorkpad(workpadId),
|
||||
|
||||
// Clone workpad given an id
|
||||
cloneWorkpad: props => async workpadId => {
|
||||
|
@ -89,7 +81,7 @@ export const WorkpadLoader = compose(
|
|||
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
|
||||
// https://github.com/elastic/kibana/issues/20277
|
||||
if (err.response.status === 403) {
|
||||
if (err.response && err.response.status === 403) {
|
||||
props.setCanUserWrite(false);
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +114,7 @@ export const WorkpadLoader = compose(
|
|||
errors.push(result.id);
|
||||
// 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
|
||||
if (result.err.response.status === 403) {
|
||||
if (result.err.response && result.err.response.status === 403) {
|
||||
props.setCanUserWrite(false);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -142,7 +142,7 @@ export class WorkpadLoader extends React.PureComponent {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content="Download">
|
||||
<EuiButtonIcon
|
||||
iconType="sortDown"
|
||||
iconType="exportAction"
|
||||
onClick={() => this.props.downloadWorkpad(workpad.id)}
|
||||
aria-label="Download Workpad"
|
||||
/>
|
||||
|
@ -288,7 +288,7 @@ export class WorkpadLoader extends React.PureComponent {
|
|||
);
|
||||
|
||||
const downloadButton = (
|
||||
<EuiButton color="secondary" onClick={this.downloadWorkpads} iconType="sortDown">
|
||||
<EuiButton color="secondary" onClick={this.downloadWorkpads} iconType="exportAction">
|
||||
{`Download (${selectedWorkpads.length})`}
|
||||
</EuiButton>
|
||||
);
|
||||
|
|
18
x-pack/plugins/canvas/public/lib/download_workpad.js
Normal file
18
x-pack/plugins/canvas/public/lib/download_workpad.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* 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` });
|
||||
}
|
||||
};
|
|
@ -55,26 +55,26 @@ export const esPersistMiddleware = ({ getState }) => {
|
|||
if (workpadChanged(curState, newState) || assetsChanged(curState, newState)) {
|
||||
const persistedWorkpad = getWorkpadPersisted(getState());
|
||||
return update(persistedWorkpad.id, persistedWorkpad).catch(err => {
|
||||
if (err.response.status === 400) {
|
||||
return notify.error(err.response, {
|
||||
title: `Couldn't save your changes to Elasticsearch`,
|
||||
});
|
||||
}
|
||||
|
||||
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.`,
|
||||
{
|
||||
const statusCode = err.response && err.response.status;
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return notify.error(err.response, {
|
||||
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`,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
@import '../components/autocomplete/autocomplete';
|
||||
@import '../components/border_connection/border_connection';
|
||||
@import '../components/border_resize_handle/border_resize_handle';
|
||||
@import '../components/clipboard/clipboard';
|
||||
@import '../components/color_dot/color_dot';
|
||||
@import '../components/color_palette/color_palette';
|
||||
@import '../components/color_picker_mini/color_picker_mini';
|
||||
|
@ -52,6 +53,7 @@
|
|||
@import '../components/toolbar/toolbar';
|
||||
@import '../components/toolbar/tray/tray';
|
||||
@import '../components/workpad/workpad';
|
||||
@import '../components/workpad_export/workpad_export';
|
||||
@import '../components/workpad_loader/workpad_loader';
|
||||
@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone';
|
||||
@import '../components/workpad_page/workpad_page';
|
||||
|
|
|
@ -41,7 +41,7 @@ export function workpad(server) {
|
|||
const savedObjectsClient = req.getSavedObjectsClient();
|
||||
|
||||
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();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { JoinEditor } from './view';
|
||||
import { getSelectedLayer, getSelectedLayerJoinDescriptors } from '../../../selectors/map_selectors';
|
||||
import { setJoinsForLayer } from '../../../actions/store_actions';
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
|
@ -16,12 +17,10 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
function mapStateToProps({}, props) {
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
joins: props.layer.getJoins().map(join => {
|
||||
return join.toDescriptor();
|
||||
}),
|
||||
layer: props.layer,
|
||||
joins: getSelectedLayerJoinDescriptors(state),
|
||||
layer: getSelectedLayer(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export class LayerPanel extends React.Component {
|
|||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<JoinEditor layer={this.props.selectedLayer}/>
|
||||
<JoinEditor/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -135,15 +135,6 @@ export const getDataFilters = createSelector(
|
|||
|
||||
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(
|
||||
getLayerListRaw,
|
||||
getDataSources,
|
||||
|
@ -152,4 +143,19 @@ export const getLayerList = createSelector(
|
|||
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()));
|
||||
|
|
|
@ -21,7 +21,7 @@ export const ALL_SOURCES = [
|
|||
EMSFileSource,
|
||||
EMSTMSSource,
|
||||
KibanaRegionmapSource,
|
||||
KibanaTilemapSource,
|
||||
XYZTMSSource,
|
||||
WMSSource,
|
||||
KibanaTilemapSource
|
||||
];
|
||||
|
|
|
@ -20,7 +20,7 @@ import { emsServiceSettings } from '../../../kibana_services';
|
|||
export class EMSFileSource extends VectorSource {
|
||||
|
||||
static type = 'EMS_FILE';
|
||||
static typeDisplayName = 'Elastic Maps Service region boundaries';
|
||||
static typeDisplayName = 'Elastic Maps Service vector shapes';
|
||||
|
||||
static createDescriptor(id) {
|
||||
return {
|
||||
|
@ -60,7 +60,9 @@ export class EMSFileSource extends VectorSource {
|
|||
<strong>{EMSFileSource.typeDisplayName}</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<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>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -108,10 +110,6 @@ export class EMSFileSource extends VectorSource {
|
|||
|
||||
}
|
||||
|
||||
async isTimeAware() {
|
||||
return false;
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import _ from 'lodash';
|
|||
export class EMSTMSSource extends TMSSource {
|
||||
|
||||
static type = 'EMS_TMS';
|
||||
static typeDisplayName = 'Elastic Maps Service Tile Service';
|
||||
static typeDisplayName = 'Elastic Maps Service tiles';
|
||||
|
||||
static createDescriptor(serviceId) {
|
||||
return {
|
||||
|
@ -58,7 +58,9 @@ export class EMSTMSSource extends TMSSource {
|
|||
<strong>{EMSTMSSource.typeDisplayName}</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<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>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -91,8 +91,7 @@ export class ESGeohashGridSource extends VectorSource {
|
|||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">
|
||||
Group documents into grid cells and display metrics for each cell.
|
||||
Great for displaying large datasets.
|
||||
Group geospatial data in grids with metrics for each gridded cell
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
|
|
|
@ -50,7 +50,7 @@ export class ESSearchSource extends VectorSource {
|
|||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">
|
||||
Display documents from an elasticsearch index.
|
||||
Geospatial data from an Elasticsearch index
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>);
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
export class KibanaRegionmapSource extends VectorSource {
|
||||
|
||||
static type = 'REGIONMAP_FILE';
|
||||
static typeDisplayName = 'Custom region boundaries';
|
||||
static typeDisplayName = 'Custom vector shapes';
|
||||
|
||||
constructor(descriptor, { ymlFileLayers }) {
|
||||
super(descriptor);
|
||||
|
@ -62,7 +62,7 @@ export class KibanaRegionmapSource extends VectorSource {
|
|||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s" color="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>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
export class KibanaTilemapSource extends TMSSource {
|
||||
|
||||
static type = 'KIBANA_TILEMAP';
|
||||
static typeDisplayName = 'TMS from kibana.yml';
|
||||
static typeDisplayName = 'Custom Tile Map Service';
|
||||
|
||||
static createDescriptor(url) {
|
||||
return {
|
||||
|
@ -41,7 +41,9 @@ export class KibanaTilemapSource extends TMSSource {
|
|||
<strong>{KibanaTilemapSource.typeDisplayName}</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<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>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -57,17 +57,14 @@ export class VectorSource extends ASource {
|
|||
}
|
||||
|
||||
isFilterByMapBounds() {
|
||||
console.warn('Should implement VectorSource#isFilterByMapBounds');
|
||||
return false;
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
console.warn('Should implement VectorSource#getNumberFields');
|
||||
return [];
|
||||
}
|
||||
|
||||
async getStringFields() {
|
||||
console.warn('Should implement VectorSource@getStringFields');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -93,7 +90,7 @@ export class VectorSource extends ASource {
|
|||
}
|
||||
|
||||
async isTimeAware() {
|
||||
throw new Error('Should implement');
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export class WMSSource extends TMSSource {
|
|||
|
||||
static type = 'WMS';
|
||||
|
||||
static typeDisplayName = 'WMS';
|
||||
static typeDisplayName = 'Web Map Service';
|
||||
|
||||
static createDescriptor({ serviceUrl, layers, styles }) {
|
||||
return {
|
||||
|
@ -46,7 +46,9 @@ export class WMSSource extends TMSSource {
|
|||
<strong>{WMSSource.typeDisplayName}</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<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>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,7 @@ export class XYZTMSSource extends TMSSource {
|
|||
|
||||
static type = 'EMS_XYZ';
|
||||
|
||||
static typeDisplayName = 'TMS XYZ';
|
||||
static typeDisplayName = 'Tile Map Service from URL';
|
||||
|
||||
static createDescriptor(urlTemplate) {
|
||||
return {
|
||||
|
@ -44,7 +44,9 @@ export class XYZTMSSource extends TMSSource {
|
|||
<strong>{XYZTMSSource.typeDisplayName}</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<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>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -223,7 +223,7 @@ export class VectorLayer extends ALayer {
|
|||
join: join
|
||||
};
|
||||
}
|
||||
startLoading(sourceDataId, requestToken, { timeFilters: dataFilters.timeFilters });
|
||||
startLoading(sourceDataId, requestToken, dataFilters);
|
||||
const leftSourceName = await this.getSourceName();
|
||||
const {
|
||||
rawData,
|
||||
|
|
|
@ -17,5 +17,5 @@ export const enrichResponse = async (response, callWithRequest) => {
|
|||
// silently swallow enricher response errors
|
||||
}
|
||||
}
|
||||
return response;
|
||||
return enrichedResponse;
|
||||
};
|
||||
|
|
|
@ -75,6 +75,10 @@
|
|||
min-width: 150px;
|
||||
}
|
||||
|
||||
.mlAnomalyCategoryExamples__header {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.mlAnomalyCategoryExamples__link {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -29,9 +29,11 @@ import { AnomalyDetails } from './anomaly_details';
|
|||
|
||||
import { mlTableService } from '../../services/table_service';
|
||||
import { RuleEditorFlyout } from '../../components/rule_editor';
|
||||
import { ml } from '../../services/ml_api_service';
|
||||
import {
|
||||
INFLUENCERS_LIMIT,
|
||||
ANOMALIES_TABLE_TABS
|
||||
ANOMALIES_TABLE_TABS,
|
||||
MAX_CHARS
|
||||
} from './anomalies_table_constants';
|
||||
|
||||
class AnomaliesTable extends Component {
|
||||
|
@ -71,18 +73,36 @@ class AnomaliesTable extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
toggleRow = (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
|
||||
toggleRow = async (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
|
||||
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMap[item.rowId]) {
|
||||
delete itemIdToExpandedRowMap[item.rowId];
|
||||
} else {
|
||||
const examples = (item.entityName === 'mlcategory') ?
|
||||
_.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] = (
|
||||
<AnomalyDetails
|
||||
tabIndex={tab}
|
||||
anomaly={item}
|
||||
examples={examples}
|
||||
definition={definition}
|
||||
isAggregatedData={this.isShowingAggregatedData()}
|
||||
filter={this.props.filter}
|
||||
influencersLimit={INFLUENCERS_LIMIT}
|
||||
|
|
|
@ -13,3 +13,5 @@ export const ANOMALIES_TABLE_TABS = {
|
|||
DETAILS: 0,
|
||||
CATEGORY_EXAMPLES: 1
|
||||
};
|
||||
|
||||
export const MAX_CHARS = 500;
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
|
@ -35,6 +36,7 @@ import {
|
|||
} from '../../../common/util/anomaly_utils';
|
||||
import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact';
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
import { MAX_CHARS } from './anomalies_table_constants';
|
||||
|
||||
const TIME_FIELD_NAME = 'timestamp';
|
||||
|
||||
|
@ -218,6 +220,8 @@ export class AnomalyDetails extends Component {
|
|||
}
|
||||
|
||||
renderCategoryExamples() {
|
||||
const { examples, definition } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
|
@ -225,9 +229,54 @@ export class AnomalyDetails extends Component {
|
|||
gutterSize="m"
|
||||
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>
|
||||
<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>
|
||||
<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 (
|
||||
<EuiFlexItem key={`example${i}`}>
|
||||
{(i === 0 && definition !== undefined) &&
|
||||
<EuiText size="s">
|
||||
<h4>Examples</h4>
|
||||
</EuiText>}
|
||||
<span className="mlAnomalyCategoryExamples__item">{example}</span>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
@ -384,6 +433,7 @@ export class AnomalyDetails extends Component {
|
|||
AnomalyDetails.propTypes = {
|
||||
anomaly: PropTypes.object.isRequired,
|
||||
examples: PropTypes.array,
|
||||
definition: PropTypes.object,
|
||||
isAggregatedData: PropTypes.bool,
|
||||
filter: PropTypes.func,
|
||||
influencersLimit: PropTypes.number,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { AnomalyDetails } from './anomaly_details';
|
||||
|
||||
const props = {
|
||||
|
@ -86,4 +86,75 @@ describe('AnomalyDetails', () => {
|
|||
);
|
||||
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
Loading…
Reference in a new issue