> => ({
+ code: '123456',
+ remainingAttempts: 5,
+ verify: jest.fn().mockReturnValue(true),
+ }),
+};
diff --git a/src/plugins/interactive_setup/server/verification_code.test.ts b/src/plugins/interactive_setup/server/verification_code.test.ts
new file mode 100644
index 0000000000000..7387f285a2f62
--- /dev/null
+++ b/src/plugins/interactive_setup/server/verification_code.test.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { loggingSystemMock } from 'src/core/server/mocks';
+
+import { VERIFICATION_CODE_LENGTH } from '../common';
+import { VerificationCode } from './verification_code';
+
+const loggerMock = loggingSystemMock.createLogger();
+
+describe('VerificationCode', () => {
+ it('should generate a 6 digit code', () => {
+ for (let i = 0; i < 10; i++) {
+ const { code } = new VerificationCode(loggerMock);
+ expect(code).toHaveLength(VERIFICATION_CODE_LENGTH);
+ expect(code).toEqual(expect.stringMatching(/^[0-9]+$/));
+ }
+ });
+
+ it('should verify code correctly', () => {
+ const verificationCode = new VerificationCode(loggerMock);
+
+ expect(verificationCode.verify(undefined)).toBe(false);
+ expect(verificationCode.verify('')).toBe(false);
+ expect(verificationCode.verify('invalid')).toBe(false);
+ expect(verificationCode.verify(verificationCode.code)).toBe(true);
+ });
+
+ it('should track number of failed attempts', () => {
+ const verificationCode = new VerificationCode(loggerMock);
+
+ verificationCode.verify('invalid');
+ verificationCode.verify('invalid');
+ verificationCode.verify('invalid');
+ expect(verificationCode['failedAttempts']).toBe(3); // eslint-disable-line dot-notation
+ });
+
+ it('should reset number of failed attempts if valid code is entered', () => {
+ const verificationCode = new VerificationCode(loggerMock);
+
+ verificationCode.verify('invalid');
+ verificationCode.verify('invalid');
+ verificationCode.verify('invalid');
+ expect(verificationCode.verify(verificationCode.code)).toBe(true);
+ expect(verificationCode['failedAttempts']).toBe(0); // eslint-disable-line dot-notation
+ });
+
+ it('should permanently fail once maximum number of failed attempts has been reached', () => {
+ const verificationCode = new VerificationCode(loggerMock);
+
+ // eslint-disable-next-line dot-notation
+ for (let i = 0; i < verificationCode['maxFailedAttempts']; i++) {
+ verificationCode.verify('invalid');
+ }
+ expect(verificationCode.verify(verificationCode.code)).toBe(false);
+ });
+
+ it('should ignore empty calls in number of failed attempts', () => {
+ const verificationCode = new VerificationCode(loggerMock);
+
+ verificationCode.verify(undefined);
+ verificationCode.verify(undefined);
+ verificationCode.verify(undefined);
+ expect(verificationCode['failedAttempts']).toBe(0); // eslint-disable-line dot-notation
+ });
+});
diff --git a/src/plugins/interactive_setup/server/verification_code.ts b/src/plugins/interactive_setup/server/verification_code.ts
new file mode 100644
index 0000000000000..849ece5f4e0b0
--- /dev/null
+++ b/src/plugins/interactive_setup/server/verification_code.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import chalk from 'chalk';
+import crypto from 'crypto';
+
+import type { Logger } from 'src/core/server';
+
+import { VERIFICATION_CODE_LENGTH } from '../common';
+
+export class VerificationCode {
+ public readonly code = VerificationCode.generate(VERIFICATION_CODE_LENGTH);
+ private failedAttempts = 0;
+ private readonly maxFailedAttempts = 5;
+
+ constructor(private readonly logger: Logger) {}
+
+ public get remainingAttempts() {
+ return this.maxFailedAttempts - this.failedAttempts;
+ }
+
+ public verify(code: string | undefined) {
+ if (this.failedAttempts >= this.maxFailedAttempts) {
+ this.logger.error(
+ 'Maximum number of attempts exceeded. Restart Kibana to generate a new code and retry.'
+ );
+ return false;
+ }
+
+ const highlightedCode = chalk.black.bgCyanBright(
+ ` ${this.code.substr(0, 3)} ${this.code.substr(3)} `
+ );
+
+ if (code === undefined) {
+ // eslint-disable-next-line no-console
+ console.log(`
+
+Your verification code is: ${highlightedCode}
+
+`);
+ return false;
+ }
+
+ if (code !== this.code) {
+ this.failedAttempts++;
+ this.logger.error(
+ `Invalid verification code '${code}' provided. ${this.remainingAttempts} attempts left.`
+ );
+ // eslint-disable-next-line no-console
+ console.log(`
+
+Your verification code is: ${highlightedCode}
+
+`);
+ return false;
+ }
+
+ this.logger.debug(`Code '${code}' verified successfully`);
+
+ this.failedAttempts = 0;
+ return true;
+ }
+
+ /**
+ * Returns a cryptographically secure and random 6-digit code.
+ *
+ * Implementation notes: `secureRandomNumber` returns a random number like `0.05505769583xxxx`. To
+ * turn that into a 6 digit code we multiply it by `10^6` and result is `055057`.
+ */
+ private static generate(length: number) {
+ return Math.floor(secureRandomNumber() * Math.pow(10, length))
+ .toString()
+ .padStart(length, '0');
+ }
+}
+
+/**
+ * Cryptographically secure equivalent of `Math.random()`.
+ */
+function secureRandomNumber() {
+ return crypto.randomBytes(4).readUInt32LE() / 0x100000000;
+}
diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts
index 1f999b59ddb61..74e849948d418 100644
--- a/src/plugins/share/public/index.ts
+++ b/src/plugins/share/public/index.ts
@@ -31,6 +31,7 @@ export {
UrlGeneratorsService,
} from './url_generators';
+export { RedirectOptions } from './url_service';
export { useLocatorUrl } from '../common/url_service/locators/use_locator_url';
import { SharePlugin } from './plugin';
diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts
index cc45e0d3126af..a5d895c7cbcf0 100644
--- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts
+++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts
@@ -15,7 +15,15 @@ import type { UrlService } from '../../../common/url_service';
import { render } from './render';
import { parseSearchParams } from './util/parse_search_params';
-export interface RedirectOptions {
+/**
+ * @public
+ * Serializable locator parameters that can be used by the redirect service to navigate to a location
+ * in Kibana.
+ *
+ * When passed to the public {@link SharePluginSetup['navigate']} function, locator params will also be
+ * migrated.
+ */
+export interface RedirectOptions {
/** Locator ID. */
id: string;
@@ -23,7 +31,7 @@ export interface RedirectOptions {
version: string;
/** Locator params. */
- params: unknown & SerializableRecord;
+ params: P;
}
export interface RedirectManagerDependencies {
diff --git a/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx
index c3735bdc0d79a..837ec5ff60dc5 100644
--- a/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx
+++ b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx
@@ -16,7 +16,7 @@ import { Datatable } from '../../../../expressions/public';
import { getHeatmapColors } from '../../../../charts/public';
import { VisParams, MetricVisMetric } from '../types';
import { getFormatService } from '../services';
-import { SchemaConfig } from '../../../../visualizations/public';
+import { ExpressionValueVisDimension } from '../../../../visualizations/public';
import { Range } from '../../../../expressions/public';
import './metric_vis.scss';
@@ -98,6 +98,16 @@ class MetricVisComponent extends Component {
return fieldFormatter.convert(value, format);
};
+ private getColumn(
+ accessor: ExpressionValueVisDimension['accessor'],
+ columns: Datatable['columns'] = []
+ ) {
+ if (typeof accessor === 'number') {
+ return columns[accessor];
+ }
+ return columns.filter(({ id }) => accessor.id === id)[0];
+ }
+
private processTableGroups(table: Datatable) {
const config = this.props.visParams.metric;
const dimensions = this.props.visParams.dimensions;
@@ -112,13 +122,12 @@ class MetricVisComponent extends Component {
let bucketFormatter: IFieldFormat;
if (dimensions.bucket) {
- bucketColumnId = table.columns[dimensions.bucket.accessor].id;
+ bucketColumnId = this.getColumn(dimensions.bucket.accessor, table.columns).id;
bucketFormatter = getFormatService().deserialize(dimensions.bucket.format);
}
- dimensions.metrics.forEach((metric: SchemaConfig) => {
- const columnIndex = metric.accessor;
- const column = table?.columns[columnIndex];
+ dimensions.metrics.forEach((metric: ExpressionValueVisDimension) => {
+ const column = this.getColumn(metric.accessor, table?.columns);
const formatter = getFormatService().deserialize(metric.format);
table.rows.forEach((row, rowIndex) => {
let title = column.name;
diff --git a/src/plugins/vis_types/metric/public/metric_vis_fn.ts b/src/plugins/vis_types/metric/public/metric_vis_fn.ts
index 9a144defed4e7..210552732bc0a 100644
--- a/src/plugins/vis_types/metric/public/metric_vis_fn.ts
+++ b/src/plugins/vis_types/metric/public/metric_vis_fn.ts
@@ -15,9 +15,10 @@ import {
Render,
Style,
} from '../../../expressions/public';
-import { visType, DimensionsVisParam, VisParams } from './types';
+import { visType, VisParams } from './types';
import { prepareLogTable, Dimension } from '../../../visualizations/public';
import { ColorSchemas, vislibColorMaps, ColorMode } from '../../../charts/public';
+import { ExpressionValueVisDimension } from '../../../visualizations/public';
export type Input = Datatable;
@@ -32,8 +33,8 @@ interface Arguments {
subText: string;
colorRange: Range[];
font: Style;
- metric: any[]; // these aren't typed yet
- bucket: any; // these aren't typed yet
+ metric: ExpressionValueVisDimension[];
+ bucket: ExpressionValueVisDimension;
}
export interface MetricVisRenderValue {
@@ -150,14 +151,6 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({
},
},
fn(input, args, handlers) {
- const dimensions: DimensionsVisParam = {
- metrics: args.metric,
- };
-
- if (args.bucket) {
- dimensions.bucket = args.bucket;
- }
-
if (args.percentageMode && (!args.colorRange || args.colorRange.length === 0)) {
throw new Error('colorRange must be provided when using percentageMode');
}
@@ -184,6 +177,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({
const logTable = prepareLogTable(input, argsTable);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
+
return {
type: 'render',
as: 'metric_vis',
@@ -209,7 +203,10 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({
fontSize,
},
},
- dimensions,
+ dimensions: {
+ metrics: args.metric,
+ ...(args.bucket ? { bucket: args.bucket } : {}),
+ },
},
},
};
diff --git a/src/plugins/vis_types/metric/public/types.ts b/src/plugins/vis_types/metric/public/types.ts
index 1baaa25959f31..8e86c0217bba6 100644
--- a/src/plugins/vis_types/metric/public/types.ts
+++ b/src/plugins/vis_types/metric/public/types.ts
@@ -7,14 +7,14 @@
*/
import { Range } from '../../../expressions/public';
-import { SchemaConfig } from '../../../visualizations/public';
+import { ExpressionValueVisDimension } from '../../../visualizations/public';
import { ColorMode, Labels, Style, ColorSchemas } from '../../../charts/public';
export const visType = 'metric';
export interface DimensionsVisParam {
- metrics: SchemaConfig[];
- bucket?: SchemaConfig;
+ metrics: ExpressionValueVisDimension[];
+ bucket?: ExpressionValueVisDimension;
}
export interface MetricVisParam {
diff --git a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap
index fed6fb54288f2..9e4c3071db8d6 100644
--- a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap
+++ b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap
@@ -1,6 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled 1`] = `
+exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with DatatableColumn vis_dimension.accessor at metric 1`] = `
+Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "aggs": Array [],
+ "index": Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "id": Array [
+ "123",
+ ],
+ },
+ "function": "indexPatternLoad",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ],
+ "metricsAtAllLevels": Array [
+ false,
+ ],
+ "partialRows": Array [
+ false,
+ ],
+ },
+ "function": "esaggs",
+ "type": "function",
+ },
+ Object {
+ "arguments": Object {
+ "bucket": Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "accessor": Array [
+ 0,
+ ],
+ "format": Array [
+ "terms",
+ ],
+ "formatParams": Array [
+ "{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\"}",
+ ],
+ },
+ "function": "visdimension",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ],
+ "maxFontSize": Array [
+ 15,
+ ],
+ "metric": Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "accessor": Array [
+ 1,
+ ],
+ "format": Array [
+ "number",
+ ],
+ },
+ "function": "visdimension",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ],
+ "minFontSize": Array [
+ 5,
+ ],
+ "orientation": Array [
+ "single",
+ ],
+ "palette": Array [
+ "default",
+ ],
+ "scale": Array [
+ "linear",
+ ],
+ "showLabel": Array [
+ true,
+ ],
+ },
+ "function": "tagcloud",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+}
+`;
+
+exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with number vis_dimension.accessor at metric 1`] = `
Object {
"chain": Array [
Object {
diff --git a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts
index c70448ab113cb..6de1d4fb3e75d 100644
--- a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts
+++ b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts
@@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
-import { Vis } from 'src/plugins/visualizations/public';
+import { Vis, VisToExpressionAstParams } from '../../../visualizations/public';
import { toExpressionAst } from './to_ast';
import { TagCloudVisParams } from './types';
-const mockSchemas = {
+const mockedSchemas = {
metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }],
segment: [
{
@@ -31,14 +31,14 @@ const mockSchemas = {
};
jest.mock('../../../visualizations/public', () => ({
- getVisSchemas: () => mockSchemas,
+ getVisSchemas: () => mockedSchemas,
}));
describe('tagcloud vis toExpressionAst function', () => {
let vis: Vis;
beforeEach(() => {
- vis = {
+ vis = ({
isHierarchical: () => false,
type: {},
params: {
@@ -51,15 +51,15 @@ describe('tagcloud vis toExpressionAst function', () => {
aggs: [],
},
},
- } as any;
+ } as unknown) as Vis;
});
it('should match snapshot without params', () => {
- const actual = toExpressionAst(vis, {} as any);
+ const actual = toExpressionAst(vis, {} as VisToExpressionAstParams);
expect(actual).toMatchSnapshot();
});
- it('should match snapshot params fulfilled', () => {
+ it('should match snapshot params fulfilled with number vis_dimension.accessor at metric', () => {
vis.params = {
scale: 'linear',
orientation: 'single',
@@ -70,9 +70,48 @@ describe('tagcloud vis toExpressionAst function', () => {
type: 'palette',
name: 'default',
},
- metric: { accessor: 0, format: { id: 'number' } },
+ metric: {
+ type: 'vis_dimension',
+ accessor: 0,
+ format: {
+ id: 'number',
+ params: {
+ id: 'number',
+ },
+ },
+ },
+ };
+ const actual = toExpressionAst(vis, {} as VisToExpressionAstParams);
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should match snapshot params fulfilled with DatatableColumn vis_dimension.accessor at metric', () => {
+ vis.params = {
+ scale: 'linear',
+ orientation: 'single',
+ minFontSize: 5,
+ maxFontSize: 15,
+ showLabel: true,
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ metric: {
+ type: 'vis_dimension',
+ accessor: {
+ id: 'count',
+ name: 'count',
+ meta: { type: 'number' },
+ },
+ format: {
+ id: 'number',
+ params: {
+ id: 'number',
+ },
+ },
+ },
};
- const actual = toExpressionAst(vis, {} as any);
+ const actual = toExpressionAst(vis, {} as VisToExpressionAstParams);
expect(actual).toMatchSnapshot();
});
});
diff --git a/src/plugins/vis_types/tagcloud/public/types.ts b/src/plugins/vis_types/tagcloud/public/types.ts
index 28a7c6506eb31..996555ae99f83 100644
--- a/src/plugins/vis_types/tagcloud/public/types.ts
+++ b/src/plugins/vis_types/tagcloud/public/types.ts
@@ -6,15 +6,7 @@
* Side Public License, v 1.
*/
import type { ChartsPluginSetup, PaletteOutput } from '../../../charts/public';
-import type { SerializedFieldFormat } from '../../../expressions/public';
-
-interface Dimension {
- accessor: number;
- format: {
- id?: string;
- params?: SerializedFieldFormat