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
/plugins
/optimize
/dlls
/built_assets
/src/fixtures/vislib/mock_data
/src/ui/public/angular-bootstrap
/src/ui/public/flot-charts

3
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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.

View file

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

View file

@ -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

View file

@ -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;

View file

@ -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}/**',

View file

@ -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

View file

@ -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' } },
],
},
},
},

View file

@ -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',

View file

@ -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`);
}
}

View file

@ -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',

View file

@ -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',
}),

View file

@ -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),
];
}

View file

@ -20,7 +20,7 @@
export function createProxyBundlesRoute({ host, port }) {
return [
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',
manifestExt: '.manifest.dll.json',
styleExt: '.style.dll.css',
outputPath: fromRoot('./dlls'),
outputPath: fromRoot('built_assets/dlls'),
publicPath: PUBLIC_PATH_PLACEHOLDER
};
}

View file

@ -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');

View file

@ -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
*/

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 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;
}

View file

@ -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=... */"
`);
});

View file

@ -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;

View file

@ -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));

View 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",

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 () => {
const callCluster = sinon.spy(async (path: string, { index }: any) => {
return {

View file

@ -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) {

View file

@ -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"
>

View file

@ -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

View file

@ -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),

View file

@ -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);

View file

@ -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: {

View file

@ -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'
]
}

View file

@ -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} />
}
];

View file

@ -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],
},
]
}

View file

@ -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} />
}
];

View file

@ -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&apos;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>
)
});

View file

@ -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}
/>
);

View file

@ -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",
},
]

View file

@ -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}

View file

@ -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" />
}
];

View file

@ -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>
`;

View file

@ -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

View file

@ -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' } }
]
}
}
}

View file

@ -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;

View file

@ -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'`);
});
});
});

View file

@ -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`
);
});
});

View file

@ -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.`
);
});
});
});

View file

@ -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'`);
});
});
});

View file

@ -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'`);
});
});
});

View file

@ -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'`);
}
);
});

View file

@ -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`);
});
});
});

View file

@ -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`
);
});
});
});

View file

@ -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'`);
});
});
});

View file

@ -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}'`);
}
})();
}

View file

@ -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`
);
}

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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;
});

View file

@ -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 = '';

View file

@ -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 {

View file

@ -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();
}

View file

@ -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'.");
});
});
});

View file

@ -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'.`);
}

View file

@ -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,

View file

@ -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));
}
});

View file

@ -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>
);

View file

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

View file

@ -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;
});
});

View file

@ -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>
);

View file

@ -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);

View file

@ -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>
);

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 { 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 {

View file

@ -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>
);

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)) {
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`,
});
});
}
};

View file

@ -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';

View file

@ -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();

View file

@ -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),
};
}

View file

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

View file

@ -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()));

View file

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

View file

@ -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;
}

View file

@ -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>
);

View file

@ -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>

View file

@ -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>);

View file

@ -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>

View file

@ -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>
);

View file

@ -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;
}
}

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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,

View file

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

View file

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

View file

@ -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}

View file

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

View file

@ -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>&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 (
<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,

View file

@ -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