From 581f1bd6e9329b66db5bc3cb3348475595db5aa2 Mon Sep 17 00:00:00 2001
From: Vadim Dalecky
Date: Mon, 24 Feb 2020 16:26:34 +0100
Subject: [PATCH 01/21] Expressions debug mode (#57841)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 🎸 add ability to execute expression in debug mode
* feat: 🎸 store resolved arguments in debug information
* feat: 🎸 track function execution time and set "success" flag
* feat: 🎸 store debug information for functions that throw
* feat: 🎸 use performance.now, safe "fn" reference, fix typo
---
src/plugins/expressions/common/ast/types.ts | 53 ++++
.../common/execution/execution.test.ts | 253 +++++++++++++++++-
.../expressions/common/execution/execution.ts | 67 ++++-
.../expressions/common/executor/executor.ts | 26 +-
4 files changed, 379 insertions(+), 20 deletions(-)
diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts
index 82a7578dd4b89..0b505f117a580 100644
--- a/src/plugins/expressions/common/ast/types.ts
+++ b/src/plugins/expressions/common/ast/types.ts
@@ -17,6 +17,9 @@
* under the License.
*/
+import { ExpressionValue, ExpressionValueError } from '../expression_types';
+import { ExpressionFunction } from '../../public';
+
export type ExpressionAstNode =
| ExpressionAstExpression
| ExpressionAstFunction
@@ -31,6 +34,56 @@ export interface ExpressionAstFunction {
type: 'function';
function: string;
arguments: Record;
+
+ /**
+ * Debug information added to each function when expression is executed in *debug mode*.
+ */
+ debug?: ExpressionAstFunctionDebug;
+}
+
+export interface ExpressionAstFunctionDebug {
+ /**
+ * True if function successfully returned output, false if function threw.
+ */
+ success: boolean;
+
+ /**
+ * Reference to the expression function this AST node represents.
+ */
+ fn: ExpressionFunction;
+
+ /**
+ * Input that expression function received as its first argument.
+ */
+ input: ExpressionValue;
+
+ /**
+ * Map of resolved arguments expression function received as its second argument.
+ */
+ args: Record;
+
+ /**
+ * Result returned by the expression function. Including an error result
+ * if it was returned by the function (not thrown).
+ */
+ output?: ExpressionValue;
+
+ /**
+ * Error that function threw normalized to `ExpressionValueError`.
+ */
+ error?: ExpressionValueError;
+
+ /**
+ * Raw error that was thrown by the function, if any.
+ */
+ rawError?: any | Error;
+
+ /**
+ * Time in milliseconds it took to execute the function. Duration can be
+ * `undefined` if error happened during argument resolution, because function
+ * timing starts after the arguments have been resolved.
+ */
+ duration: number | undefined;
}
export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression;
diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts
index b6c1721e33eef..f6ff9efca848b 100644
--- a/src/plugins/expressions/common/execution/execution.test.ts
+++ b/src/plugins/expressions/common/execution/execution.test.ts
@@ -18,20 +18,28 @@
*/
import { Execution } from './execution';
-import { parseExpression } from '../ast';
+import { parseExpression, ExpressionAstExpression } from '../ast';
import { createUnitTestExecutor } from '../test_helpers';
import { ExpressionFunctionDefinition } from '../../public';
import { ExecutionContract } from './execution_contract';
+beforeAll(() => {
+ if (typeof performance === 'undefined') {
+ (global as any).performance = { now: Date.now };
+ }
+});
+
const createExecution = (
expression: string = 'foo bar=123',
- context: Record = {}
+ context: Record = {},
+ debug: boolean = false
) => {
const executor = createUnitTestExecutor();
const execution = new Execution({
executor,
ast: parseExpression(expression),
context,
+ debug,
});
return execution;
};
@@ -406,4 +414,245 @@ describe('Execution', () => {
});
});
});
+
+ describe('debug mode', () => {
+ test('can execute expression in debug mode', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ const result = await execution.result;
+
+ expect(result).toEqual({
+ type: 'num',
+ value: 5,
+ });
+ });
+
+ test('can execute expression with sub-expression in debug mode', async () => {
+ const execution = createExecution(
+ 'add val={var_set name=foo value=5 | var name=foo} | add val=10',
+ {},
+ true
+ );
+ execution.start(0);
+ const result = await execution.result;
+
+ expect(result).toEqual({
+ type: 'num',
+ value: 15,
+ });
+ });
+
+ describe('when functions succeed', () => {
+ test('sets "success" flag on all functions to true', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ for (const node of execution.state.get().ast.chain) {
+ expect(node.debug?.success).toBe(true);
+ }
+ });
+
+ test('stores "fn" reference to the function', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ for (const node of execution.state.get().ast.chain) {
+ expect(node.debug?.fn.name).toBe('add');
+ }
+ });
+
+ test('saves duration it took to execute each function', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ for (const node of execution.state.get().ast.chain) {
+ expect(typeof node.debug?.duration).toBe('number');
+ expect(node.debug?.duration).toBeLessThan(100);
+ expect(node.debug?.duration).toBeGreaterThanOrEqual(0);
+ }
+ });
+
+ test('sets duration to 10 milliseconds when function executes 10 milliseconds', async () => {
+ const execution = createExecution('sleep 10', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ const node = execution.state.get().ast.chain[0];
+ expect(typeof node.debug?.duration).toBe('number');
+ expect(node.debug?.duration).toBeLessThan(50);
+ expect(node.debug?.duration).toBeGreaterThanOrEqual(5);
+ });
+
+ test('adds .debug field in expression AST on each executed function', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ for (const node of execution.state.get().ast.chain) {
+ expect(typeof node.debug).toBe('object');
+ expect(!!node.debug).toBe(true);
+ }
+ });
+
+ test('stores input of each function', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ const { chain } = execution.state.get().ast;
+
+ expect(chain[0].debug!.input).toBe(-1);
+ expect(chain[1].debug!.input).toEqual({
+ type: 'num',
+ value: 0,
+ });
+ expect(chain[2].debug!.input).toEqual({
+ type: 'num',
+ value: 2,
+ });
+ });
+
+ test('stores output of each function', async () => {
+ const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
+ execution.start(-1);
+ await execution.result;
+
+ const { chain } = execution.state.get().ast;
+
+ expect(chain[0].debug!.output).toEqual({
+ type: 'num',
+ value: 0,
+ });
+ expect(chain[1].debug!.output).toEqual({
+ type: 'num',
+ value: 2,
+ });
+ expect(chain[2].debug!.output).toEqual({
+ type: 'num',
+ value: 5,
+ });
+ });
+
+ test('stores resolved arguments of a function', async () => {
+ const execution = createExecution(
+ 'add val={var_set name=foo value=5 | var name=foo} | add val=10',
+ {},
+ true
+ );
+ execution.start(-1);
+ await execution.result;
+
+ const { chain } = execution.state.get().ast;
+
+ expect(chain[0].debug!.args).toEqual({
+ val: 5,
+ });
+
+ expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual(
+ {
+ name: 'foo',
+ value: 5,
+ }
+ );
+ });
+
+ test('store debug information about sub-expressions', async () => {
+ const execution = createExecution(
+ 'add val={var_set name=foo value=5 | var name=foo} | add val=10',
+ {},
+ true
+ );
+ execution.start(0);
+ await execution.result;
+
+ const { chain } = execution.state.get().ast.chain[0].arguments
+ .val[0] as ExpressionAstExpression;
+
+ expect(typeof chain[0].debug).toBe('object');
+ expect(typeof chain[1].debug).toBe('object');
+ expect(!!chain[0].debug).toBe(true);
+ expect(!!chain[1].debug).toBe(true);
+
+ expect(chain[0].debug!.input).toBe(0);
+ expect(chain[0].debug!.output).toBe(0);
+ expect(chain[1].debug!.input).toBe(0);
+ expect(chain[1].debug!.output).toBe(5);
+ });
+ });
+
+ describe('when expression throws', () => {
+ const executor = createUnitTestExecutor();
+ executor.registerFunction({
+ name: 'throws',
+ args: {},
+ help: '',
+ fn: () => {
+ throw new Error('foo');
+ },
+ });
+
+ test('stores debug information up until the function that throws', async () => {
+ const execution = new Execution({
+ executor,
+ ast: parseExpression('add val=1 | throws | add val=3'),
+ debug: true,
+ });
+ execution.start(0);
+ await execution.result;
+
+ const node1 = execution.state.get().ast.chain[0];
+ const node2 = execution.state.get().ast.chain[1];
+ const node3 = execution.state.get().ast.chain[2];
+
+ expect(typeof node1.debug).toBe('object');
+ expect(typeof node2.debug).toBe('object');
+ expect(typeof node3.debug).toBe('undefined');
+ });
+
+ test('stores error thrown in debug information', async () => {
+ const execution = new Execution({
+ executor,
+ ast: parseExpression('add val=1 | throws | add val=3'),
+ debug: true,
+ });
+ execution.start(0);
+ await execution.result;
+
+ const node2 = execution.state.get().ast.chain[1];
+
+ expect(node2.debug?.error).toMatchObject({
+ type: 'error',
+ error: {
+ message: '[throws] > foo',
+ },
+ });
+ expect(node2.debug?.rawError).toBeInstanceOf(Error);
+ });
+
+ test('sets .debug object to expected shape', async () => {
+ const execution = new Execution({
+ executor,
+ ast: parseExpression('add val=1 | throws | add val=3'),
+ debug: true,
+ });
+ execution.start(0);
+ await execution.result;
+
+ const node2 = execution.state.get().ast.chain[1];
+
+ expect(node2.debug).toMatchObject({
+ success: false,
+ fn: expect.any(Object),
+ input: expect.any(Object),
+ args: expect.any(Object),
+ error: expect.any(Object),
+ rawError: expect.any(Error),
+ duration: expect.any(Number),
+ });
+ });
+ });
+ });
});
diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts
index 2a272e187cffc..7e7df822724ae 100644
--- a/src/plugins/expressions/common/execution/execution.ts
+++ b/src/plugins/expressions/common/execution/execution.ts
@@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
import { Defer } from '../../../kibana_utils/common';
import { RequestAdapter, DataAdapter } from '../../../inspector/common';
-import { isExpressionValueError } from '../expression_types/specs/error';
+import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error';
import {
ExpressionAstExpression,
ExpressionAstFunction,
@@ -32,7 +32,7 @@ import {
parseExpression,
} from '../ast';
import { ExecutionContext, DefaultInspectorAdapters } from './types';
-import { getType } from '../expression_types';
+import { getType, ExpressionValue } from '../expression_types';
import { ArgumentType, ExpressionFunction } from '../expression_functions';
import { getByAlias } from '../util/get_by_alias';
import { ExecutionContract } from './execution_contract';
@@ -44,6 +44,13 @@ export interface ExecutionParams<
ast?: ExpressionAstExpression;
expression?: string;
context?: ExtraContext;
+
+ /**
+ * Whether to execute expression in *debug mode*. In *debug mode* inputs and
+ * outputs as well as all resolved arguments and time it took to execute each
+ * function are saved and are available for introspection.
+ */
+ debug?: boolean;
}
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
@@ -190,23 +197,55 @@ export class Execution<
}
const { function: fnName, arguments: fnArgs } = link;
- const fnDef = getByAlias(this.state.get().functions, fnName);
+ const fn = getByAlias(this.state.get().functions, fnName);
- if (!fnDef) {
+ if (!fn) {
return createError({ message: `Function ${fnName} could not be found.` });
}
+ let args: Record = {};
+ let timeStart: number | undefined;
+
try {
- // Resolve arguments before passing to function
- // resolveArgs returns an object because the arguments themselves might
- // actually have a 'then' function which would be treated as a promise
- const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs);
- const output = await this.invokeFunction(fnDef, input, resolvedArgs);
+ // `resolveArgs` returns an object because the arguments themselves might
+ // actually have a `then` function which would be treated as a `Promise`.
+ const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs);
+ args = resolvedArgs;
+ timeStart = this.params.debug ? performance.now() : 0;
+ const output = await this.invokeFunction(fn, input, resolvedArgs);
+
+ if (this.params.debug) {
+ const timeEnd: number = performance.now();
+ (link as ExpressionAstFunction).debug = {
+ success: true,
+ fn,
+ input,
+ args: resolvedArgs,
+ output,
+ duration: timeEnd - timeStart,
+ };
+ }
+
if (getType(output) === 'error') return output;
input = output;
- } catch (e) {
- e.message = `[${fnName}] > ${e.message}`;
- return createError(e);
+ } catch (rawError) {
+ const timeEnd: number = this.params.debug ? performance.now() : 0;
+ rawError.message = `[${fnName}] > ${rawError.message}`;
+ const error = createError(rawError) as ExpressionValueError;
+
+ if (this.params.debug) {
+ (link as ExpressionAstFunction).debug = {
+ success: false,
+ fn,
+ input,
+ args,
+ error,
+ rawError,
+ duration: timeStart ? timeEnd - timeStart : undefined,
+ };
+ }
+
+ return error;
}
}
@@ -327,7 +366,9 @@ export class Execution<
const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => {
return asts.map((item: ExpressionAstExpression) => {
return async (subInput = input) => {
- const output = await this.params.executor.interpret(item, subInput);
+ const output = await this.params.executor.interpret(item, subInput, {
+ debug: this.params.debug,
+ });
if (isExpressionValueError(output)) throw output.error;
const casted = this.cast(output, argDefs[argName as any].types);
return casted;
diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts
index af3662d13de4e..2ecbc5f75a9e8 100644
--- a/src/plugins/expressions/common/executor/executor.ts
+++ b/src/plugins/expressions/common/executor/executor.ts
@@ -31,6 +31,15 @@ import { ExpressionAstExpression, ExpressionAstNode } from '../ast';
import { typeSpecs } from '../expression_types/specs';
import { functionSpecs } from '../expression_functions/specs';
+export interface ExpressionExecOptions {
+ /**
+ * Whether to execute expression in *debug mode*. In *debug mode* inputs and
+ * outputs as well as all resolved arguments and time it took to execute each
+ * function are saved and are available for introspection.
+ */
+ debug?: boolean;
+}
+
export class TypesRegistry implements IRegistry {
constructor(private readonly executor: Executor) {}
@@ -145,10 +154,14 @@ export class Executor = Record(ast: ExpressionAstNode, input: T): Promise {
+ public async interpret(
+ ast: ExpressionAstNode,
+ input: T,
+ options?: ExpressionExecOptions
+ ): Promise {
switch (getType(ast)) {
case 'expression':
- return await this.interpretExpression(ast as ExpressionAstExpression, input);
+ return await this.interpretExpression(ast as ExpressionAstExpression, input, options);
case 'string':
case 'number':
case 'null':
@@ -161,9 +174,10 @@ export class Executor = Record(
ast: string | ExpressionAstExpression,
- input: T
+ input: T,
+ options?: ExpressionExecOptions
): Promise {
- const execution = this.createExecution(ast);
+ const execution = this.createExecution(ast, undefined, options);
execution.start(input);
return await execution.result;
}
@@ -192,7 +206,8 @@ export class Executor = Record(
ast: string | ExpressionAstExpression,
- context: ExtraContext = {} as ExtraContext
+ context: ExtraContext = {} as ExtraContext,
+ { debug }: ExpressionExecOptions = {} as ExpressionExecOptions
): Execution {
const params: ExecutionParams = {
executor: this,
@@ -200,6 +215,7 @@ export class Executor = Record
Date: Mon, 24 Feb 2020 16:40:20 +0100
Subject: [PATCH 02/21] no sparse array by default. (#58212)
Co-authored-by: Elastic Machine
---
.../src/types/array_type.test.ts | 37 ++++++++++++++++---
.../kbn-config-schema/src/types/array_type.ts | 4 +-
2 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/packages/kbn-config-schema/src/types/array_type.test.ts b/packages/kbn-config-schema/src/types/array_type.test.ts
index 73661ef849cf4..66b72096a593d 100644
--- a/packages/kbn-config-schema/src/types/array_type.test.ts
+++ b/packages/kbn-config-schema/src/types/array_type.test.ts
@@ -85,14 +85,29 @@ test('fails if mixed types of content in array', () => {
);
});
-test('returns empty array if input is empty but type has default value', () => {
- const type = schema.arrayOf(schema.string({ defaultValue: 'test' }));
+test('fails if sparse content in array', () => {
+ const type = schema.arrayOf(schema.string());
expect(type.validate([])).toEqual([]);
+ expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot(
+ `"[0]: sparse array are not allowed"`
+ );
});
-test('returns empty array if input is empty even if type is required', () => {
- const type = schema.arrayOf(schema.string());
+test('fails if sparse content in array if optional', () => {
+ const type = schema.arrayOf(schema.maybe(schema.string()));
+ expect(type.validate([])).toEqual([]);
+ expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot(
+ `"[0]: sparse array are not allowed"`
+ );
+});
+
+test('fails if sparse content in array if nullable', () => {
+ const type = schema.arrayOf(schema.nullable(schema.string()));
expect(type.validate([])).toEqual([]);
+ expect(type.validate([null])).toEqual([null]);
+ expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot(
+ `"[0]: sparse array are not allowed"`
+ );
});
test('fails for null values if optional', () => {
@@ -102,9 +117,19 @@ test('fails for null values if optional', () => {
);
});
+test('returns empty array if input is empty but type has default value', () => {
+ const type = schema.arrayOf(schema.string({ defaultValue: 'test' }));
+ expect(type.validate([])).toEqual([]);
+});
+
+test('returns empty array if input is empty even if type is required', () => {
+ const type = schema.arrayOf(schema.string());
+ expect(type.validate([])).toEqual([]);
+});
+
test('handles default values for undefined values', () => {
- const type = schema.arrayOf(schema.string({ defaultValue: 'foo' }));
- expect(type.validate([undefined])).toEqual(['foo']);
+ const type = schema.arrayOf(schema.string(), { defaultValue: ['foo'] });
+ expect(type.validate(undefined)).toEqual(['foo']);
});
test('array within array', () => {
diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts
index ad74f375588ad..a0353e8348ddd 100644
--- a/packages/kbn-config-schema/src/types/array_type.ts
+++ b/packages/kbn-config-schema/src/types/array_type.ts
@@ -31,7 +31,7 @@ export class ArrayType extends Type {
let schema = internals
.array()
.items(type.getSchema().optional())
- .sparse();
+ .sparse(false);
if (options.minSize !== undefined) {
schema = schema.min(options.minSize);
@@ -49,6 +49,8 @@ export class ArrayType extends Type {
case 'any.required':
case 'array.base':
return `expected value of type [array] but got [${typeDetect(value)}]`;
+ case 'array.sparse':
+ return `sparse array are not allowed`;
case 'array.parse':
return `could not parse array value from [${value}]`;
case 'array.min':
From 6b735c9ca016a65cc0c2e29fb144a1046020f1d3 Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Mon, 24 Feb 2020 11:17:29 -0500
Subject: [PATCH 03/21] [ML] New Platform server shim: update usage collector
to use core savedObjects (#58058)
* use NP savedObjects and createInternalRepository for usage collection
* fix route doc typo
* update MlTelemetrySavedObject type
* remove fileDataVisualizer routes dependency on legacy es plugin
* update mlTelemetry tests
* remove deprecated use of getSavedObjectsClient
---
x-pack/legacy/plugins/ml/index.ts | 3 +-
.../ml/server/lib/ml_telemetry/index.ts | 1 -
.../ml_telemetry/make_ml_usage_collector.ts | 15 ++-
.../lib/ml_telemetry/ml_telemetry.test.ts | 102 +++++-------------
.../server/lib/ml_telemetry/ml_telemetry.ts | 39 +++----
.../plugins/ml/server/new_platform/plugin.ts | 21 ++--
.../ml/server/routes/file_data_visualizer.ts | 2 +-
.../ml/server/routes/job_audit_messages.ts | 2 +-
8 files changed, 55 insertions(+), 130 deletions(-)
diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts
index 0ef5e14e44f71..09f1b9ccedce4 100755
--- a/x-pack/legacy/plugins/ml/index.ts
+++ b/x-pack/legacy/plugins/ml/index.ts
@@ -81,7 +81,8 @@ export const ml = (kibana: any) => {
injectUiAppVars: server.injectUiAppVars,
http: mlHttpService,
savedObjects: server.savedObjects,
- elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, // NP
+ coreSavedObjects: kbnServer.newPlatform.start.core.savedObjects,
+ elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch,
};
const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins;
const plugins = {
diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts
index 5da4f6b62bcec..dffd95f50e0d9 100644
--- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts
+++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts
@@ -6,7 +6,6 @@
export {
createMlTelemetry,
- getSavedObjectsClient,
incrementFileDataVisualizerIndexCreationCount,
storeMlTelemetry,
MlTelemetry,
diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts
index 293480b2aa5dc..a120450bbb2b0 100644
--- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts
+++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts
@@ -5,19 +5,17 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { SavedObjectsServiceStart } from 'src/core/server';
import {
createMlTelemetry,
- getSavedObjectsClient,
ML_TELEMETRY_DOC_ID,
MlTelemetry,
MlTelemetrySavedObject,
} from './ml_telemetry';
-import { UsageInitialization } from '../../new_platform/plugin';
-
export function makeMlUsageCollector(
usageCollection: UsageCollectionSetup | undefined,
- { elasticsearchPlugin, savedObjects }: UsageInitialization
+ savedObjects: SavedObjectsServiceStart
): void {
if (!usageCollection) {
return;
@@ -28,11 +26,10 @@ export function makeMlUsageCollector(
isReady: () => true,
fetch: async (): Promise => {
try {
- const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects);
- const mlTelemetrySavedObject = (await savedObjectsClient.get(
- 'ml-telemetry',
- ML_TELEMETRY_DOC_ID
- )) as MlTelemetrySavedObject;
+ const mlTelemetrySavedObject: MlTelemetrySavedObject = await savedObjects
+ .createInternalRepository()
+ .get('ml-telemetry', ML_TELEMETRY_DOC_ID);
+
return mlTelemetrySavedObject.attributes;
} catch (err) {
return createMlTelemetry();
diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts
index fcf3763626b6f..9d14ffb31be63 100644
--- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts
+++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts
@@ -6,7 +6,6 @@
import {
createMlTelemetry,
- getSavedObjectsClient,
incrementFileDataVisualizerIndexCreationCount,
ML_TELEMETRY_DOC_ID,
MlTelemetry,
@@ -26,22 +25,11 @@ describe('ml_telemetry', () => {
});
describe('storeMlTelemetry', () => {
- let elasticsearchPlugin: any;
- let savedObjects: any;
let mlTelemetry: MlTelemetry;
- let savedObjectsClientInstance: any;
+ let internalRepository: any;
beforeEach(() => {
- savedObjectsClientInstance = { create: jest.fn() };
- const callWithInternalUser = jest.fn();
- const internalRepository = jest.fn();
- elasticsearchPlugin = {
- getCluster: jest.fn(() => ({ callWithInternalUser })),
- };
- savedObjects = {
- SavedObjectsClient: jest.fn(() => savedObjectsClientInstance),
- getSavedObjectsRepository: jest.fn(() => internalRepository),
- };
+ internalRepository = { create: jest.fn(), get: jest.fn() };
mlTelemetry = {
file_data_visualizer: {
index_creation_count: 1,
@@ -49,59 +37,28 @@ describe('ml_telemetry', () => {
};
});
- it('should call savedObjectsClient create with the given MlTelemetry object', () => {
- storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry);
- expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe(mlTelemetry);
+ it('should call internalRepository create with the given MlTelemetry object', () => {
+ storeMlTelemetry(internalRepository, mlTelemetry);
+ expect(internalRepository.create.mock.calls[0][1]).toBe(mlTelemetry);
});
- it('should call savedObjectsClient create with the ml-telemetry document type and ID', () => {
- storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry);
- expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry');
- expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID);
+ it('should call internalRepository create with the ml-telemetry document type and ID', () => {
+ storeMlTelemetry(internalRepository, mlTelemetry);
+ expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry');
+ expect(internalRepository.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID);
});
- it('should call savedObjectsClient create with overwrite: true', () => {
- storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry);
- expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe(true);
- });
- });
-
- describe('getSavedObjectsClient', () => {
- let elasticsearchPlugin: any;
- let savedObjects: any;
- let savedObjectsClientInstance: any;
- let callWithInternalUser: any;
- let internalRepository: any;
-
- beforeEach(() => {
- savedObjectsClientInstance = { create: jest.fn() };
- callWithInternalUser = jest.fn();
- internalRepository = jest.fn();
- elasticsearchPlugin = {
- getCluster: jest.fn(() => ({ callWithInternalUser })),
- };
- savedObjects = {
- SavedObjectsClient: jest.fn(() => savedObjectsClientInstance),
- getSavedObjectsRepository: jest.fn(() => internalRepository),
- };
- });
-
- it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => {
- const result = getSavedObjectsClient(elasticsearchPlugin, savedObjects);
-
- expect(result).toBe(savedObjectsClientInstance);
- expect(savedObjects.SavedObjectsClient).toHaveBeenCalledWith(internalRepository);
+ it('should call internalRepository create with overwrite: true', () => {
+ storeMlTelemetry(internalRepository, mlTelemetry);
+ expect(internalRepository.create.mock.calls[0][2].overwrite).toBe(true);
});
});
describe('incrementFileDataVisualizerIndexCreationCount', () => {
- let elasticsearchPlugin: any;
let savedObjects: any;
- let savedObjectsClientInstance: any;
- let callWithInternalUser: any;
let internalRepository: any;
- function createSavedObjectsClientInstance(
+ function createInternalRepositoryInstance(
telemetryEnabled?: boolean,
indexCreationCount?: number
) {
@@ -136,51 +93,42 @@ describe('ml_telemetry', () => {
}
function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void {
- savedObjectsClientInstance = createSavedObjectsClientInstance(
- telemetryEnabled,
- indexCreationCount
- );
- callWithInternalUser = jest.fn();
- internalRepository = jest.fn();
+ internalRepository = createInternalRepositoryInstance(telemetryEnabled, indexCreationCount);
savedObjects = {
- SavedObjectsClient: jest.fn(() => savedObjectsClientInstance),
- getSavedObjectsRepository: jest.fn(() => internalRepository),
- };
- elasticsearchPlugin = {
- getCluster: jest.fn(() => ({ callWithInternalUser })),
+ createInternalRepository: jest.fn(() => internalRepository),
};
}
it('should not increment if telemetry status cannot be determined', async () => {
mockInit();
- await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects);
+ await incrementFileDataVisualizerIndexCreationCount(savedObjects);
- expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0);
+ expect(internalRepository.create.mock.calls).toHaveLength(0);
});
it('should not increment if telemetry status is disabled', async () => {
mockInit(false);
- await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects);
+ await incrementFileDataVisualizerIndexCreationCount(savedObjects);
- expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0);
+ expect(internalRepository.create.mock.calls).toHaveLength(0);
});
it('should initialize index_creation_count with 1', async () => {
mockInit(true);
- await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects);
+ await incrementFileDataVisualizerIndexCreationCount(savedObjects);
- expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry');
- expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({
+ expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry');
+ expect(internalRepository.create.mock.calls[0][1]).toEqual({
file_data_visualizer: { index_creation_count: 1 },
});
});
it('should increment index_creation_count to 2', async () => {
mockInit(true, 1);
- await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects);
+ await incrementFileDataVisualizerIndexCreationCount(savedObjects);
- expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry');
- expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({
+ expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry');
+ expect(internalRepository.create.mock.calls[0][1]).toEqual({
file_data_visualizer: { index_creation_count: 2 },
});
});
diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts
index 1bac3f1780644..d76b1ee94e21e 100644
--- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts
+++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
-import { SavedObjectsLegacyService } from 'src/legacy/server/kbn_server';
-import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory';
+import {
+ SavedObjectAttributes,
+ SavedObjectsServiceStart,
+ ISavedObjectsRepository,
+} from 'src/core/server';
-export interface MlTelemetry {
+export interface MlTelemetry extends SavedObjectAttributes {
file_data_visualizer: {
index_creation_count: number;
};
@@ -29,35 +31,22 @@ export function createMlTelemetry(count: number = 0): MlTelemetry {
}
// savedObjects
export function storeMlTelemetry(
- elasticsearchPlugin: ElasticsearchPlugin,
- savedObjects: SavedObjectsLegacyService,
+ internalRepository: ISavedObjectsRepository,
mlTelemetry: MlTelemetry
): void {
- const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects);
- savedObjectsClient.create('ml-telemetry', mlTelemetry, {
+ internalRepository.create('ml-telemetry', mlTelemetry, {
id: ML_TELEMETRY_DOC_ID,
overwrite: true,
});
}
-// needs savedObjects and elasticsearchPlugin
-export function getSavedObjectsClient(
- elasticsearchPlugin: ElasticsearchPlugin,
- savedObjects: SavedObjectsLegacyService
-): any {
- const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects;
- const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin);
- const internalRepository = getSavedObjectsRepository(callWithInternalUser);
- return new SavedObjectsClient(internalRepository);
-}
export async function incrementFileDataVisualizerIndexCreationCount(
- elasticsearchPlugin: ElasticsearchPlugin,
- savedObjects: SavedObjectsLegacyService
+ savedObjects: SavedObjectsServiceStart
): Promise {
- const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects);
-
+ const internalRepository = await savedObjects.createInternalRepository();
try {
- const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry');
+ const { attributes } = await internalRepository.get('telemetry', 'telemetry');
+
if (attributes.enabled === false) {
return;
}
@@ -70,7 +59,7 @@ export async function incrementFileDataVisualizerIndexCreationCount(
let indicesCount = 1;
try {
- const { attributes } = (await savedObjectsClient.get(
+ const { attributes } = (await internalRepository.get(
'ml-telemetry',
ML_TELEMETRY_DOC_ID
)) as MlTelemetrySavedObject;
@@ -80,5 +69,5 @@ export async function incrementFileDataVisualizerIndexCreationCount(
}
const mlTelemetry = createMlTelemetry(indicesCount);
- storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry);
+ storeMlTelemetry(internalRepository, mlTelemetry);
}
diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts
index 10961182be841..43c276ac63a13 100644
--- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts
+++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts
@@ -14,6 +14,7 @@ import {
CoreSetup,
IRouter,
IScopedClusterClient,
+ SavedObjectsServiceStart,
} from 'src/core/server';
import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
@@ -28,12 +29,10 @@ import { LICENSE_TYPE } from '../../common/constants/license';
import { annotationRoutes } from '../routes/annotations';
import { jobRoutes } from '../routes/anomaly_detectors';
import { dataFeedRoutes } from '../routes/datafeeds';
-// @ts-ignore: could not find declaration file for module
import { indicesRoutes } from '../routes/indices';
import { jobValidationRoutes } from '../routes/job_validation';
import { makeMlUsageCollector } from '../lib/ml_telemetry';
import { notificationRoutes } from '../routes/notification_settings';
-// @ts-ignore: could not find declaration file for module
import { systemRoutes } from '../routes/system';
import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics';
import { dataRecognizer } from '../routes/modules';
@@ -45,7 +44,6 @@ import { filtersRoutes } from '../routes/filters';
import { resultsServiceRoutes } from '../routes/results_service';
import { jobServiceRoutes } from '../routes/job_service';
import { jobAuditMessagesRoutes } from '../routes/job_audit_messages';
-// @ts-ignore: could not find declaration file for module
import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer';
import { initMlServerLog, LogInitialization } from '../client/log';
import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server';
@@ -67,6 +65,7 @@ export interface MlCoreSetup {
injectUiAppVars: (id: string, callback: () => {}) => any;
http: MlHttpServiceSetup;
savedObjects: SavedObjectsLegacyService;
+ coreSavedObjects: SavedObjectsServiceStart;
elasticsearch: ElasticsearchServiceSetup;
}
export interface MlInitializerContext extends PluginInitializerContext {
@@ -93,15 +92,11 @@ export interface RouteInitialization {
route(route: ServerRoute | ServerRoute[]): void;
router: IRouter;
xpackMainPlugin: MlXpackMainPlugin;
- savedObjects?: SavedObjectsLegacyService;
+ savedObjects?: SavedObjectsServiceStart;
spacesPlugin: any;
securityPlugin: any;
cloud?: CloudSetup;
}
-export interface UsageInitialization {
- elasticsearchPlugin: ElasticsearchPlugin;
- savedObjects: SavedObjectsLegacyService;
-}
declare module 'kibana/server' {
interface RequestHandlerContext {
@@ -123,7 +118,7 @@ export class Plugin {
public setup(core: MlCoreSetup, plugins: PluginsSetup) {
const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain;
- const { http } = core;
+ const { http, coreSavedObjects } = core;
const pluginId = this.pluginId;
mirrorPluginStatus(xpackMainPlugin, plugins.ml);
@@ -208,14 +203,10 @@ export class Plugin {
const extendedRouteInitializationDeps: RouteInitialization = {
...routeInitializationDeps,
config: this.config,
- savedObjects: core.savedObjects,
+ savedObjects: coreSavedObjects,
spacesPlugin: plugins.spaces,
cloud: plugins.cloud,
};
- const usageInitializationDeps: UsageInitialization = {
- elasticsearchPlugin: plugins.elasticsearch,
- savedObjects: core.savedObjects,
- };
const logInitializationDeps: LogInitialization = {
log: this.log,
@@ -240,7 +231,7 @@ export class Plugin {
fileDataVisualizerRoutes(extendedRouteInitializationDeps);
initMlServerLog(logInitializationDeps);
- makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps);
+ makeMlUsageCollector(plugins.usageCollection, coreSavedObjects);
}
public stop() {}
diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts
index 95f2a9fe7298f..d5a992c933293 100644
--- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts
+++ b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts
@@ -138,7 +138,7 @@ export function fileDataVisualizerRoutes({
// follow-up import calls to just add additional data will include the `id` of the created
// index, we'll ignore those and don't increment the counter.
if (id === undefined) {
- await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects!);
+ await incrementFileDataVisualizerIndexCreationCount(savedObjects!);
}
const result = await importData(
diff --git a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts
index 7298312990005..76986b935b993 100644
--- a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts
+++ b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts
@@ -50,7 +50,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial
/**
* @apiGroup JobAuditMessages
*
- * @api {get} /api/ml/results/anomalies_table_data Get all audit messages
+ * @api {get} /api/ml/job_audit_messages/messages Get all audit messages
* @apiName GetAllJobAuditMessages
* @apiDescription Returns all audit messages
*/
From c6f5fdd061d93ad0d67335a658449be92e24640c Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Mon, 24 Feb 2020 17:42:34 +0100
Subject: [PATCH 04/21] Advanced settings UI change to centralize save state
(#53693)
---
.../advanced_settings/public/_index.scss | 2 +-
.../public/management_app/_index.scss | 3 +
.../management_app/advanced_settings.scss | 40 +-
.../management_app/advanced_settings.tsx | 16 +-
.../management_app/components/_index.scss | 1 +
.../field/__snapshots__/field.test.tsx.snap | 7069 ++++++++---------
.../components/field/field.test.tsx | 267 +-
.../management_app/components/field/field.tsx | 545 +-
.../management_app/components/field/index.ts | 2 +-
.../form/__snapshots__/form.test.tsx.snap | 1228 ++-
.../management_app/components/form/_form.scss | 13 +
.../components/form/_index.scss | 1 +
.../components/form/form.test.tsx | 114 +-
.../management_app/components/form/form.tsx | 280 +-
.../public/management_app/types.ts | 13 +
.../telemetry_management_section.tsx | 54 +-
test/functional/page_objects/settings_page.ts | 6 +-
.../translations/translations/ja-JP.json | 11 -
.../translations/translations/zh-CN.json | 10 -
19 files changed, 5086 insertions(+), 4589 deletions(-)
create mode 100644 src/plugins/advanced_settings/public/management_app/_index.scss
create mode 100644 src/plugins/advanced_settings/public/management_app/components/_index.scss
create mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_form.scss
create mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_index.scss
diff --git a/src/plugins/advanced_settings/public/_index.scss b/src/plugins/advanced_settings/public/_index.scss
index f3fe78bf6a9c0..d13c37bff32d0 100644
--- a/src/plugins/advanced_settings/public/_index.scss
+++ b/src/plugins/advanced_settings/public/_index.scss
@@ -17,4 +17,4 @@
* under the License.
*/
- @import './management_app/advanced_settings';
+@import './management_app/index';
diff --git a/src/plugins/advanced_settings/public/management_app/_index.scss b/src/plugins/advanced_settings/public/management_app/_index.scss
new file mode 100644
index 0000000000000..aa1980692f7b7
--- /dev/null
+++ b/src/plugins/advanced_settings/public/management_app/_index.scss
@@ -0,0 +1,3 @@
+@import './advanced_settings';
+
+@import './components/index';
diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss
index 79b6feccb6b7d..016edb2817da8 100644
--- a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss
+++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss
@@ -17,21 +17,27 @@
* under the License.
*/
-.mgtAdvancedSettings__field {
+ .mgtAdvancedSettings__field {
+ * {
margin-top: $euiSize;
}
- &Wrapper {
- width: 640px;
- @include internetExplorerOnly() {
- min-height: 1px;
- }
+ padding-left: $euiSizeS;
+ margin-left: -$euiSizeS;
+ &--unsaved {
+ // Simulates a left side border without shifting content
+ box-shadow: -$euiSizeXS 0px $euiColorSecondary;
}
-
- &Actions {
- padding-top: $euiSizeM;
+ &--invalid {
+ // Simulates a left side border without shifting content
+ box-shadow: -$euiSizeXS 0px $euiColorDanger;
+ }
+ @include internetExplorerOnly() {
+ min-height: 1px;
+ }
+ &Row {
+ padding-left: $euiSizeS;
}
@include internetExplorerOnly {
@@ -40,3 +46,19 @@
}
}
}
+
+.mgtAdvancedSettingsForm__unsavedCount {
+ @include euiBreakpoint('xs', 's') {
+ display: none;
+ }
+}
+
+.mgtAdvancedSettingsForm__unsavedCountMessage{
+ // Simulates a left side border without shifting content
+ box-shadow: -$euiSizeXS 0px $euiColorSecondary;
+ padding-left: $euiSizeS;
+}
+
+.mgtAdvancedSettingsForm__button {
+ width: 100%;
+}
diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx
index 5057d072e3e41..39312c9340ff9 100644
--- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx
+++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx
@@ -38,7 +38,7 @@ import { ComponentRegistry } from '../';
import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib';
-import { FieldSetting, IQuery } from './types';
+import { FieldSetting, IQuery, SettingsChanges } from './types';
interface AdvancedSettingsProps {
enableSaving: boolean;
@@ -177,6 +177,13 @@ export class AdvancedSettingsComponent extends Component<
});
};
+ saveConfig = async (changes: SettingsChanges) => {
+ const arr = Object.entries(changes).map(([key, value]) =>
+ this.props.uiSettings.set(key, value)
+ );
+ return Promise.all(arr);
+ };
+
render() {
const { filteredSettings, query, footerQueryMatched } = this.state;
const componentRegistry = this.props.componentRegistry;
@@ -205,18 +212,19 @@ export class AdvancedSettingsComponent extends Component<
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Array test setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for array setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- default_value
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- Array test setting
-
-
- }
- >
-
+ default_value
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="array:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="array:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for array setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Array test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for array setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Array test setting
-
-
- }
- >
-
+
+
+`;
+
+exports[`Field for array setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for array setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ default_value
+ ,
+ }
+ }
/>
-
-
-
- default_value
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Array test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="array:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Array test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="array:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for boolean setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Boolean test setting
-
-
- }
- >
-
-
- }
- onChange={[Function]}
- onKeyDown={[Function]}
+
-
-
-
-
-
+ }
+ onChange={[Function]}
+ />
+
+
`;
exports[`Field for boolean setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- true
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- Boolean test setting
-
-
- }
- >
-
+ true
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="boolean:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+
+ }
+>
+
-
- }
- onChange={[Function]}
- onKeyDown={[Function]}
+
+
+ }
+ label="boolean:test:setting"
+ labelType="label"
+ >
+
-
-
-
-
-
+ }
+ onChange={[Function]}
+ />
+
+
`;
exports[`Field for boolean setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Boolean test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
- }
- onChange={[Function]}
- onKeyDown={[Function]}
+
-
-
-
-
-
+ }
+ onChange={[Function]}
+ />
+
+
`;
exports[`Field for boolean setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Boolean test setting
-
-
+
}
- >
-
-
+ onChange={[Function]}
+ />
+
+
+`;
+
+exports[`Field for boolean setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
-
-
-
-
-
+ }
+ onChange={[Function]}
+ />
+
+
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for boolean setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ true
+ ,
+ }
+ }
/>
-
-
-
- true
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Boolean test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="boolean:test:setting"
- labelType="label"
- >
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Boolean test setting
+
+
+ }
+>
+
+
+
- }
- onChange={[Function]}
- onKeyDown={[Function]}
+
+
+
+
+ }
+ label="boolean:test:setting"
+ labelType="label"
+ >
+
-
-
-
-
-
+ }
+ onChange={[Function]}
+ />
+
+
`;
exports[`Field for image setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Image test setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for image setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- null
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- Image test setting
-
-
- }
- >
-
+ null
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="image:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="image:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for image setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Image test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for image setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Image test setting
-
-
- }
- >
-
+
+
+`;
+
+exports[`Field for image setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for image setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ null
+ ,
+ }
+ }
/>
-
-
-
- null
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Image test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="image:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Image test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ label="image:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for json setting should render as read only if saving is disabled 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ {}
+ ,
+ }
+ }
/>
-
-
-
- {}
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Json test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+
+ }
+>
+
+
+
+
`;
exports[`Field for json setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ {}
+ ,
+ }
+ }
/>
-
-
-
- {}
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Json test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+
+ }
+>
+
+
+
+ }
+ label="json:test:setting"
+ labelType="label"
+ >
+
-
-
-
+
-
-
-
-
-
-
-
-
+ fullWidth={true}
+ height="auto"
+ isReadOnly={true}
+ maxLines={30}
+ minLines={6}
+ mode="json"
+ onChange={[Function]}
+ setOptions={
+ Object {
+ "showLineNumbers": false,
+ "tabSize": 2,
+ }
+ }
+ showGutter={false}
+ theme="textmate"
+ value="{\\"hello\\": \\"world\\"}"
+ width="100%"
+ />
+
+
+
`;
exports[`Field for json setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Json test setting
-
- }
- type="asterisk"
- />
-
- }
+
+
+
`;
exports[`Field for json setting should render default value if there is no user value set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ {}
+ ,
+ }
+ }
/>
-
-
-
- {}
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Json test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="json:test:setting"
+ labelType="label"
+ >
+
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="json:test:setting"
- labelType="label"
+
+
+
+
+`;
+
+exports[`Field for json setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for json setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ {}
+ ,
+ }
+ }
/>
-
-
-
- {}
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Json test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Json test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="json:test:setting"
+ labelType="label"
+ >
+
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="json:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
-
-
+
+
+
+
`;
exports[`Field for markdown setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Markdown test setting
-
-
- }
+
+
+
`;
exports[`Field for markdown setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ null
+ ,
+ }
+ }
/>
-
-
-
- null
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Markdown test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+
+ }
+>
+
+
+
+ }
+ label="markdown:test:setting"
+ labelType="label"
+ >
+
-
-
-
+
-
-
-
-
-
-
-
-
+ fullWidth={true}
+ height="auto"
+ isReadOnly={true}
+ maxLines={30}
+ minLines={6}
+ mode="markdown"
+ onChange={[Function]}
+ setOptions={
+ Object {
+ "showLineNumbers": false,
+ "tabSize": 2,
+ }
+ }
+ showGutter={false}
+ theme="textmate"
+ value="**bold**"
+ width="100%"
+ />
+
+
+
`;
exports[`Field for markdown setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Markdown test setting
-
- }
- type="asterisk"
- />
-
- }
+
+
+
`;
exports[`Field for markdown setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Markdown test setting
-
-
- }
+
+
+
+
+
+`;
+
+exports[`Field for markdown setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
-
+
+
+
-
-
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for markdown setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ null
+ ,
+ }
+ }
/>
-
-
-
- null
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Markdown test setting
-
-
- }
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Markdown test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="markdown:test:setting"
+ labelType="label"
+ >
+
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="markdown:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
-
-
+
+
+
+
`;
exports[`Field for number setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Number test setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for number setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- 5
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- Number test setting
-
-
- }
- >
-
+ 5
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="number:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="number:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for number setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Number test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
+`;
+
+exports[`Field for number setting should render default value if there is no user value set 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+
+ }
+>
+
+
+
+
`;
-exports[`Field for number setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Number test setting
-
-
- }
- >
-
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for number setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ 5
+ ,
+ }
+ }
/>
-
-
-
- 5
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Number test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="number:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Number test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="number:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for select setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Select test setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for select setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- Orange
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- Select test setting
-
-
- }
- >
-
+ Orange
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="select:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="select:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for select setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- Select test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for select setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- Select test setting
-
-
- }
- >
-
+
+
+`;
+
+exports[`Field for select setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for select setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ Orange
+ ,
+ }
+ }
/>
-
-
-
- Orange
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- Select test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="select:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ Select test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="select:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for string setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- String test setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for string setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- null
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- String test setting
-
-
- }
- >
-
+ null
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="string:test:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="string:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for string setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- String test setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for string setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- String test setting
-
-
- }
- >
-
+
+
+`;
+
+exports[`Field for string setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for string setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ null
+ ,
+ }
+ }
/>
-
-
-
- null
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- String test setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="string:test:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="string:test:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- String test validation setting
-
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
-
-
-
- foo-default
- ,
- }
- }
- />
-
-
-
-
- }
- title={
-
- String test validation setting
-
-
- }
- >
-
+ foo-default
+ ,
+ }
+ }
/>
-
- }
- isInvalid={false}
- label="string:test-validation:setting"
- labelType="label"
+
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+
+ }
+>
+
-
-
-
-
-
-
+
+ }
+ label="string:test-validation:setting"
+ labelType="label"
+ >
+
+
+
`;
exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+ }
+ type="asterisk"
+ />
+
+ }
>
-
-
-
-
- }
- title={
-
- String test validation setting
-
- }
- type="asterisk"
- />
-
- }
- >
-
-
-
-
-
-
-
+
+
+
`;
exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = `
-
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+
+ }
>
-
-
-
-
- }
- title={
-
- String test validation setting
-
-
- }
- >
-
+
+
+`;
+
+exports[`Field for stringWithValidation setting should render unsaved value if there are unsaved changes 1`] = `
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+ }
+ type="asterisk"
+ />
+
+ }
+>
+
+
+
+
-
-
-
-
-
-
+ Setting is currently not saved.
+
+
+
+
`;
exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = `
-
-
-
-
+ description={
+
+
+
+
+
-
+ foo-default
+ ,
+ }
+ }
/>
-
-
-
- foo-default
- ,
- }
- }
- />
-
-
-
- }
- title={
-
- String test validation setting
-
-
- }
- >
-
-
-
-
-
-
-
-
- }
- isInvalid={false}
- label="string:test-validation:setting"
- labelType="label"
- >
-
-
-
-
-
-
+
+
+
+ }
+ fullWidth={true}
+ title={
+
+ String test validation setting
+
+
+ }
+>
+
+
+
+
+
+
+
+
+ }
+ label="string:test-validation:setting"
+ labelType="label"
+ >
+
+
+
`;
diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx
index 81df22ccf6e43..8e41fed685898 100644
--- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx
+++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx
@@ -20,21 +20,14 @@
import React from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
-import { mount } from 'enzyme';
+import { mount, ReactWrapper } from 'enzyme';
import { FieldSetting } from '../../types';
import { UiSettingsType, StringValidation } from '../../../../../../core/public';
import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
-import { Field } from './field';
-
-jest.mock('ui/notify', () => ({
- toastNotifications: {
- addDanger: () => {},
- add: jest.fn(),
- },
-}));
+import { Field, getEditableValue } from './field';
jest.mock('brace/theme/textmate', () => 'brace/theme/textmate');
jest.mock('brace/mode/markdown', () => 'brace/mode/markdown');
@@ -45,6 +38,18 @@ const defaults = {
category: ['category'],
};
+const exampleValues = {
+ array: ['example_value'],
+ boolean: false,
+ image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=',
+ json: { foo: 'bar2' },
+ markdown: 'Hello World',
+ number: 1,
+ select: 'banana',
+ string: 'hello world',
+ stringWithValidation: 'foo',
+};
+
const settings: Record = {
array: {
name: 'array:test:setting',
@@ -161,7 +166,7 @@ const settings: Record = {
description: 'Description for String test validation setting',
type: 'string',
validation: {
- regex: new RegExp('/^foo'),
+ regex: new RegExp('^foo'),
message: 'must start with "foo"',
},
value: undefined,
@@ -182,11 +187,22 @@ const userValues = {
string: 'foo',
stringWithValidation: 'fooUserValue',
};
+
const invalidUserValues = {
stringWithValidation: 'invalidUserValue',
};
-const save = jest.fn(() => Promise.resolve(true));
-const clear = jest.fn(() => Promise.resolve(true));
+
+const handleChange = jest.fn();
+const clearChange = jest.fn();
+
+const getFieldSettingValue = (wrapper: ReactWrapper, name: string, type: string) => {
+ const field = findTestSubject(wrapper, `advancedSetting-editField-${name}`);
+ if (type === 'boolean') {
+ return field.props()['aria-checked'];
+ } else {
+ return field.props().value;
+ }
+};
describe('Field', () => {
Object.keys(settings).forEach(type => {
@@ -197,8 +213,7 @@ describe('Field', () => {
const component = shallowWithI18nProvider(
{
value: userValues[type],
isOverridden: true,
}}
- save={save}
- clear={clear}
+ handleChange={handleChange}
enableSaving={true}
toasts={notificationServiceMock.createStartContract().toasts}
dockLinks={docLinksServiceMock.createStartContract().links}
@@ -232,14 +246,12 @@ describe('Field', () => {
const component = shallowWithI18nProvider(
);
-
expect(component).toMatchSnapshot();
});
@@ -251,8 +263,7 @@ describe('Field', () => {
// @ts-ignore
value: userValues[type],
}}
- save={save}
- clear={clear}
+ handleChange={handleChange}
enableSaving={true}
toasts={notificationServiceMock.createStartContract().toasts}
dockLinks={docLinksServiceMock.createStartContract().links}
@@ -269,48 +280,44 @@ describe('Field', () => {
...setting,
isCustom: true,
}}
- save={save}
- clear={clear}
+ handleChange={handleChange}
enableSaving={true}
toasts={notificationServiceMock.createStartContract().toasts}
dockLinks={docLinksServiceMock.createStartContract().links}
/>
);
-
expect(component).toMatchSnapshot();
});
- });
-
- if (type === 'select') {
- it('should use options for rendering values', () => {
- const component = mountWithI18nProvider(
+ it('should render unsaved value if there are unsaved changes', async () => {
+ const component = shallowWithI18nProvider(
);
- const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`);
- // @ts-ignore
- const labels = select.find('option').map(option => option.prop('value'));
- expect(labels).toEqual(['apple', 'orange', 'banana']);
+ expect(component).toMatchSnapshot();
});
+ });
- it('should use optionLabels for rendering labels', () => {
+ if (type === 'select') {
+ it('should use options for rendering values and optionsLabels for rendering labels', () => {
const component = mountWithI18nProvider(
{
);
const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`);
// @ts-ignore
+ const values = select.find('option').map(option => option.prop('value'));
+ expect(values).toEqual(['apple', 'orange', 'banana']);
+ // @ts-ignore
const labels = select.find('option').map(option => option.text());
expect(labels).toEqual(['Apple', 'Orange', 'banana']);
});
@@ -328,8 +338,8 @@ describe('Field', () => {
{
const userValue = userValues[type];
(component.instance() as Field).getImageAsBase64 = ({}: Blob) => Promise.resolve('');
- it('should be able to change value from no value and cancel', async () => {
- await (component.instance() as Field).onImageChange([userValue]);
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(
- (component.instance() as Field).state.unsavedValue ===
- (component.instance() as Field).state.savedValue
- ).toBe(true);
- });
-
- it('should be able to change value and save', async () => {
- await (component.instance() as Field).onImageChange([userValue]);
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(save).toBeCalled();
- component.setState({ savedValue: userValue });
+ it('should be able to change value and cancel', async () => {
+ (component.instance() as Field).onImageChange([userValue]);
+ expect(handleChange).toBeCalled();
await wrapper.setProps({
+ unsavedChanges: {
+ value: userValue,
+ changeImage: true,
+ },
setting: {
...(component.instance() as Field).props.setting,
value: userValue,
},
});
-
await (component.instance() as Field).cancelChangeImage();
+ expect(clearChange).toBeCalledWith(setting.name);
wrapper.update();
});
- it('should be able to change value from existing value and save', async () => {
+ it('should be able to change value from existing value', async () => {
+ await wrapper.setProps({
+ unsavedChanges: {},
+ });
const updated = wrapper.update();
findTestSubject(updated, `advancedSetting-changeImage-${setting.name}`).simulate('click');
-
const newUserValue = `${userValue}=`;
await (component.instance() as Field).onImageChange([newUserValue]);
- const updated2 = wrapper.update();
- findTestSubject(updated2, `advancedSetting-saveEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(save).toBeCalled();
- component.setState({ savedValue: newUserValue });
- await wrapper.setProps({
- setting: {
- ...(component.instance() as Field).props.setting,
- value: newUserValue,
- },
- });
- wrapper.update();
+ expect(handleChange).toBeCalled();
});
it('should be able to reset to default value', async () => {
const updated = wrapper.update();
findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click');
- expect(clear).toBeCalled();
+ expect(handleChange).toBeCalledWith(setting.name, {
+ value: getEditableValue(setting.type, setting.defVal),
+ changeImage: true,
+ });
});
});
} else if (type === 'markdown' || type === 'json') {
describe(`for changing ${type} setting`, () => {
const { wrapper, component } = setup();
const userValue = userValues[type];
- const fieldUserValue = userValue;
-
- it('should be able to change value and cancel', async () => {
- (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType);
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(
- (component.instance() as Field).state.unsavedValue ===
- (component.instance() as Field).state.savedValue
- ).toBe(true);
- });
- it('should be able to change value and save', async () => {
- (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType);
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(save).toBeCalled();
- component.setState({ savedValue: fieldUserValue });
+ it('should be able to change value', async () => {
+ (component.instance() as Field).onCodeEditorChange(userValue as UiSettingsType);
+ expect(handleChange).toBeCalledWith(setting.name, { value: userValue });
await wrapper.setProps({
setting: {
...(component.instance() as Field).props.setting,
@@ -445,19 +417,21 @@ describe('Field', () => {
wrapper.update();
});
+ it('should be able to reset to default value', async () => {
+ const updated = wrapper.update();
+ findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click');
+ expect(handleChange).toBeCalledWith(setting.name, {
+ value: getEditableValue(setting.type, setting.defVal),
+ });
+ });
+
if (type === 'json') {
it('should be able to clear value and have empty object populate', async () => {
- (component.instance() as Field).onCodeEditorChange('' as UiSettingsType);
+ await (component.instance() as Field).onCodeEditorChange('' as UiSettingsType);
wrapper.update();
- expect((component.instance() as Field).state.unsavedValue).toEqual('{}');
+ expect(handleChange).toBeCalledWith(setting.name, { value: setting.defVal });
});
}
-
- it('should be able to reset to default value', async () => {
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click');
- expect(clear).toBeCalled();
- });
});
} else {
describe(`for changing ${type} setting`, () => {
@@ -470,76 +444,45 @@ describe('Field', () => {
// @ts-ignore
const invalidUserValue = invalidUserValues[type];
it('should display an error when validation fails', async () => {
- (component.instance() as Field).onFieldChange(invalidUserValue);
+ await (component.instance() as Field).onFieldChange(invalidUserValue);
+ const expectedUnsavedChanges = {
+ value: invalidUserValue,
+ error: (setting.validation as StringValidation).message,
+ isInvalid: true,
+ };
+ expect(handleChange).toBeCalledWith(setting.name, expectedUnsavedChanges);
+ wrapper.setProps({ unsavedChanges: expectedUnsavedChanges });
const updated = wrapper.update();
const errorMessage = updated.find('.euiFormErrorText').text();
- expect(errorMessage).toEqual((setting.validation as StringValidation).message);
+ expect(errorMessage).toEqual(expectedUnsavedChanges.error);
});
}
- it('should be able to change value and cancel', async () => {
- (component.instance() as Field).onFieldChange(fieldUserValue);
+ it('should be able to change value', async () => {
+ await (component.instance() as Field).onFieldChange(fieldUserValue);
const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(
- (component.instance() as Field).state.unsavedValue ===
- (component.instance() as Field).state.savedValue
- ).toBe(true);
+ expect(handleChange).toBeCalledWith(setting.name, { value: fieldUserValue });
+ updated.setProps({ unsavedChanges: { value: fieldUserValue } });
+ const currentValue = getFieldSettingValue(updated, setting.name, type);
+ expect(currentValue).toEqual(fieldUserValue);
});
- it('should be able to change value and save', async () => {
- (component.instance() as Field).onFieldChange(fieldUserValue);
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate(
- 'click'
- );
- expect(save).toBeCalled();
- component.setState({ savedValue: fieldUserValue });
+ it('should be able to reset to default value', async () => {
await wrapper.setProps({
- setting: {
- ...(component.instance() as Field).props.setting,
- value: userValue,
- },
+ unsavedChanges: {},
+ setting: { ...setting, value: fieldUserValue },
});
- wrapper.update();
- });
-
- it('should be able to reset to default value', async () => {
const updated = wrapper.update();
findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click');
- expect(clear).toBeCalled();
+ const expectedEditableValue = getEditableValue(setting.type, setting.defVal);
+ expect(handleChange).toBeCalledWith(setting.name, {
+ value: expectedEditableValue,
+ });
+ updated.setProps({ unsavedChanges: { value: expectedEditableValue } });
+ const currentValue = getFieldSettingValue(updated, setting.name, type);
+ expect(currentValue).toEqual(expectedEditableValue);
});
});
}
});
-
- it('should show a reload toast when saving setting requiring a page reload', async () => {
- const setting = {
- ...settings.string,
- requiresPageReload: true,
- };
- const toasts = notificationServiceMock.createStartContract().toasts;
- const wrapper = mountWithI18nProvider(
-
- );
- (wrapper.instance() as Field).onFieldChange({ target: { value: 'a new value' } });
- const updated = wrapper.update();
- findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate('click');
- expect(save).toHaveBeenCalled();
- await save();
- expect(toasts.add).toHaveBeenCalledWith(
- expect.objectContaining({
- title: expect.stringContaining('Please reload the page'),
- })
- );
- });
});
diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
index 7158e3d5e7b3e..d9c3752d1c0a5 100644
--- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
+++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx
@@ -18,17 +18,16 @@
*/
import React, { PureComponent, Fragment } from 'react';
-import ReactDOM from 'react-dom';
+import classNames from 'classnames';
import 'brace/theme/textmate';
import 'brace/mode/markdown';
import {
EuiBadge,
- EuiButton,
- EuiButtonEmpty,
EuiCode,
EuiCodeBlock,
+ EuiScreenReaderOnly,
// @ts-ignore
EuiCodeEditor,
EuiDescribedFormGroup,
@@ -36,23 +35,20 @@ import {
EuiFieldText,
// @ts-ignore
EuiFilePicker,
- EuiFlexGroup,
- EuiFlexItem,
EuiFormRow,
EuiIconTip,
EuiImage,
EuiLink,
EuiSpacer,
- EuiToolTip,
EuiText,
EuiSelect,
EuiSwitch,
EuiSwitchEvent,
- keyCodes,
+ EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { FieldSetting } from '../../types';
+import { FieldSetting, FieldState } from '../../types';
import { isDefaultValue } from '../../lib';
import {
UiSettingsType,
@@ -64,71 +60,37 @@ import {
interface FieldProps {
setting: FieldSetting;
- save: (name: string, value: string) => Promise;
- clear: (name: string) => Promise;
+ handleChange: (name: string, value: FieldState) => void;
enableSaving: boolean;
dockLinks: DocLinksStart['links'];
toasts: ToastsStart;
+ clearChange?: (name: string) => void;
+ unsavedChanges?: FieldState;
+ loading?: boolean;
}
-interface FieldState {
- unsavedValue: any;
- savedValue: any;
- loading: boolean;
- isInvalid: boolean;
- error: string | null;
- changeImage: boolean;
- isJsonArray: boolean;
-}
-
-export class Field extends PureComponent {
- private changeImageForm: EuiFilePicker | undefined;
- constructor(props: FieldProps) {
- super(props);
- const { type, value, defVal } = this.props.setting;
- const editableValue = this.getEditableValue(type, value, defVal);
-
- this.state = {
- isInvalid: false,
- error: null,
- loading: false,
- changeImage: false,
- savedValue: editableValue,
- unsavedValue: editableValue,
- isJsonArray: type === 'json' ? Array.isArray(JSON.parse(String(defVal) || '{}')) : false,
- };
- }
-
- UNSAFE_componentWillReceiveProps(nextProps: FieldProps) {
- const { unsavedValue } = this.state;
- const { type, value, defVal } = nextProps.setting;
- const editableValue = this.getEditableValue(type, value, defVal);
-
- this.setState({
- savedValue: editableValue,
- unsavedValue: value === null || value === undefined ? editableValue : unsavedValue,
- });
+export const getEditableValue = (
+ type: UiSettingsType,
+ value: FieldSetting['value'],
+ defVal?: FieldSetting['defVal']
+) => {
+ const val = value === null || value === undefined ? defVal : value;
+ switch (type) {
+ case 'array':
+ return (val as string[]).join(', ');
+ case 'boolean':
+ return !!val;
+ case 'number':
+ return Number(val);
+ case 'image':
+ return val;
+ default:
+ return val || '';
}
+};
- getEditableValue(
- type: UiSettingsType,
- value: FieldSetting['value'],
- defVal: FieldSetting['defVal']
- ) {
- const val = value === null || value === undefined ? defVal : value;
- switch (type) {
- case 'array':
- return (val as string[]).join(', ');
- case 'boolean':
- return !!val;
- case 'number':
- return Number(val);
- case 'image':
- return val;
- default:
- return val || '';
- }
- }
+export class Field extends PureComponent {
+ private changeImageForm: EuiFilePicker | undefined = React.createRef();
getDisplayedDefaultValue(
type: UiSettingsType,
@@ -150,47 +112,60 @@ export class Field extends PureComponent {
}
}
- setLoading(loading: boolean) {
- this.setState({
- loading,
- });
- }
+ handleChange = (unsavedChanges: FieldState) => {
+ this.props.handleChange(this.props.setting.name, unsavedChanges);
+ };
- clearError() {
- this.setState({
- isInvalid: false,
- error: null,
- });
+ resetField = () => {
+ const { type, defVal } = this.props.setting;
+ if (type === 'image') {
+ this.cancelChangeImage();
+ return this.handleChange({
+ value: getEditableValue(type, defVal),
+ changeImage: true,
+ });
+ }
+ return this.handleChange({ value: getEditableValue(type, defVal) });
+ };
+
+ componentDidUpdate(prevProps: FieldProps) {
+ if (
+ prevProps.setting.type === 'image' &&
+ prevProps.unsavedChanges?.value &&
+ !this.props.unsavedChanges?.value
+ ) {
+ this.cancelChangeImage();
+ }
}
onCodeEditorChange = (value: UiSettingsType) => {
- const { type } = this.props.setting;
- const { isJsonArray } = this.state;
+ const { defVal, type } = this.props.setting;
let newUnsavedValue;
- let isInvalid = false;
- let error = null;
+ let errorParams = {};
switch (type) {
case 'json':
+ const isJsonArray = Array.isArray(JSON.parse((defVal as string) || '{}'));
newUnsavedValue = value.trim() || (isJsonArray ? '[]' : '{}');
try {
JSON.parse(newUnsavedValue);
} catch (e) {
- isInvalid = true;
- error = i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', {
- defaultMessage: 'Invalid JSON syntax',
- });
+ errorParams = {
+ error: i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', {
+ defaultMessage: 'Invalid JSON syntax',
+ }),
+ isInvalid: true,
+ };
}
break;
default:
newUnsavedValue = value;
}
- this.setState({
- error,
- isInvalid,
- unsavedValue: newUnsavedValue,
+ this.handleChange({
+ value: newUnsavedValue,
+ ...errorParams,
});
};
@@ -201,58 +176,44 @@ export class Field extends PureComponent {
onFieldChangeEvent = (e: React.ChangeEvent) =>
this.onFieldChange(e.target.value);
- onFieldChange = (value: any) => {
- const { type, validation } = this.props.setting;
- const { unsavedValue } = this.state;
-
+ onFieldChange = (targetValue: any) => {
+ const { type, validation, value, defVal } = this.props.setting;
let newUnsavedValue;
switch (type) {
case 'boolean':
- newUnsavedValue = !unsavedValue;
+ const { unsavedChanges } = this.props;
+ const currentValue = unsavedChanges
+ ? unsavedChanges.value
+ : getEditableValue(type, value, defVal);
+ newUnsavedValue = !currentValue;
break;
case 'number':
- newUnsavedValue = Number(value);
+ newUnsavedValue = Number(targetValue);
break;
default:
- newUnsavedValue = value;
+ newUnsavedValue = targetValue;
}
- let isInvalid = false;
- let error = null;
+ let errorParams = {};
- if (validation && (validation as StringValidationRegex).regex) {
+ if ((validation as StringValidationRegex)?.regex) {
if (!(validation as StringValidationRegex).regex!.test(newUnsavedValue.toString())) {
- error = (validation as StringValidationRegex).message;
- isInvalid = true;
+ errorParams = {
+ error: (validation as StringValidationRegex).message,
+ isInvalid: true,
+ };
}
}
- this.setState({
- unsavedValue: newUnsavedValue,
- isInvalid,
- error,
+ this.handleChange({
+ value: newUnsavedValue,
+ ...errorParams,
});
};
- onFieldKeyDown = ({ keyCode }: { keyCode: number }) => {
- if (keyCode === keyCodes.ENTER) {
- this.saveEdit();
- }
- if (keyCode === keyCodes.ESCAPE) {
- this.cancelEdit();
- }
- };
-
- onFieldEscape = ({ keyCode }: { keyCode: number }) => {
- if (keyCode === keyCodes.ESCAPE) {
- this.cancelEdit();
- }
- };
-
onImageChange = async (files: any[]) => {
if (!files.length) {
- this.clearError();
this.setState({
unsavedValue: null,
});
@@ -266,19 +227,24 @@ export class Field extends PureComponent {
if (file instanceof File) {
base64Image = (await this.getImageAsBase64(file)) as string;
}
- const isInvalid = !!(maxSize && maxSize.length && base64Image.length > maxSize.length);
- this.setState({
- isInvalid,
- error: isInvalid
- ? i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', {
- defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}',
- values: {
- maxSizeDescription: maxSize.description,
- },
- })
- : null,
+
+ let errorParams = {};
+ const isInvalid = !!(maxSize?.length && base64Image.length > maxSize.length);
+ if (isInvalid) {
+ errorParams = {
+ isInvalid,
+ error: i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', {
+ defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}',
+ values: {
+ maxSizeDescription: maxSize.description,
+ },
+ }),
+ };
+ }
+ this.handleChange({
changeImage: true,
- unsavedValue: base64Image,
+ value: base64Image,
+ ...errorParams,
});
} catch (err) {
this.props.toasts.addDanger(
@@ -305,152 +271,62 @@ export class Field extends PureComponent {
}
changeImage = () => {
- this.setState({
+ this.handleChange({
+ value: null,
changeImage: true,
});
};
cancelChangeImage = () => {
- const { savedValue } = this.state;
-
- if (this.changeImageForm) {
- this.changeImageForm.fileInput.value = null;
- this.changeImageForm.handleChange();
- }
-
- this.setState({
- changeImage: false,
- unsavedValue: savedValue,
- });
- };
-
- cancelEdit = () => {
- const { savedValue } = this.state;
- this.clearError();
- this.setState({
- unsavedValue: savedValue,
- });
- };
-
- showPageReloadToast = () => {
- if (this.props.setting.requiresPageReload) {
- this.props.toasts.add({
- title: i18n.translate('advancedSettings.field.requiresPageReloadToastDescription', {
- defaultMessage: 'Please reload the page for the "{settingName}" setting to take effect.',
- values: {
- settingName: this.props.setting.displayName || this.props.setting.name,
- },
- }),
- text: element => {
- const content = (
- <>
-
-
- window.location.reload()}>
- {i18n.translate('advancedSettings.field.requiresPageReloadToastButtonLabel', {
- defaultMessage: 'Reload page',
- })}
-
-
-
- >
- );
- ReactDOM.render(content, element);
- return () => ReactDOM.unmountComponentAtNode(element);
- },
- color: 'success',
- });
- }
- };
-
- saveEdit = async () => {
- const { name, defVal, type } = this.props.setting;
- const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state;
-
- if (savedValue === unsavedValue) {
- return;
- }
-
- let valueToSave = unsavedValue;
- let isSameValue = false;
-
- switch (type) {
- case 'array':
- valueToSave = valueToSave.split(',').map((val: string) => val.trim());
- isSameValue = valueToSave.join(',') === (defVal as string[]).join(',');
- break;
- case 'json':
- valueToSave = valueToSave.trim();
- valueToSave = valueToSave || (isJsonArray ? '[]' : '{}');
- default:
- isSameValue = valueToSave === defVal;
- }
-
- this.setLoading(true);
- try {
- if (isSameValue) {
- await this.props.clear(name);
- } else {
- await this.props.save(name, valueToSave);
- }
-
- this.showPageReloadToast();
-
- if (changeImage) {
- this.cancelChangeImage();
- }
- } catch (e) {
- this.props.toasts.addDanger(
- i18n.translate('advancedSettings.field.saveFieldErrorMessage', {
- defaultMessage: 'Unable to save {name}',
- values: { name },
- })
- );
+ if (this.changeImageForm.current) {
+ this.changeImageForm.current.fileInput.value = null;
+ this.changeImageForm.current.handleChange({});
}
- this.setLoading(false);
- };
-
- resetField = async () => {
- const { name } = this.props.setting;
- this.setLoading(true);
- try {
- await this.props.clear(name);
- this.showPageReloadToast();
- this.cancelChangeImage();
- this.clearError();
- } catch (e) {
- this.props.toasts.addDanger(
- i18n.translate('advancedSettings.field.resetFieldErrorMessage', {
- defaultMessage: 'Unable to reset {name}',
- values: { name },
- })
- );
+ if (this.props.clearChange) {
+ this.props.clearChange(this.props.setting.name);
}
- this.setLoading(false);
};
- renderField(setting: FieldSetting) {
- const { enableSaving } = this.props;
- const { loading, changeImage, unsavedValue } = this.state;
- const { name, value, type, options, optionLabels = {}, isOverridden, ariaName } = setting;
+ renderField(id: string, setting: FieldSetting) {
+ const { enableSaving, unsavedChanges, loading } = this.props;
+ const {
+ name,
+ value,
+ type,
+ options,
+ optionLabels = {},
+ isOverridden,
+ defVal,
+ ariaName,
+ } = setting;
+ const a11yProps: { [key: string]: string } = unsavedChanges
+ ? {
+ 'aria-label': ariaName,
+ 'aria-describedby': id,
+ }
+ : {
+ 'aria-label': ariaName,
+ };
+ const currentValue = unsavedChanges
+ ? unsavedChanges.value
+ : getEditableValue(type, value, defVal);
switch (type) {
case 'boolean':
return (
) : (
)
}
- checked={!!unsavedValue}
+ checked={!!currentValue}
onChange={this.onFieldChangeSwitch}
disabled={loading || isOverridden || !enableSaving}
- onKeyDown={this.onFieldKeyDown}
data-test-subj={`advancedSetting-editField-${name}`}
- aria-label={ariaName}
+ {...a11yProps}
/>
);
case 'markdown':
@@ -458,10 +334,10 @@ export class Field extends PureComponent {
return (
{
$blockScrolling: Infinity,
}}
showGutter={false}
+ fullWidth
/>
);
case 'image':
+ const changeImage = unsavedChanges?.changeImage;
if (!isDefaultValue(setting) && !changeImage) {
- return (
-
- );
+ return ;
} else {
return (
{
- this.changeImageForm = input;
- }}
- onKeyDown={this.onFieldEscape}
+ ref={this.changeImageForm}
+ fullWidth
data-test-subj={`advancedSetting-editField-${name}`}
/>
);
@@ -501,8 +375,8 @@ export class Field extends PureComponent {
case 'select':
return (
{
return {
text: optionLabels.hasOwnProperty(option) ? optionLabels[option] : option,
@@ -512,31 +386,31 @@ export class Field extends PureComponent {
onChange={this.onFieldChangeEvent}
isLoading={loading}
disabled={loading || isOverridden || !enableSaving}
- onKeyDown={this.onFieldKeyDown}
+ fullWidth
data-test-subj={`advancedSetting-editField-${name}`}
/>
);
case 'number':
return (
);
default:
return (
);
@@ -699,8 +573,12 @@ export class Field extends PureComponent {
}
renderResetToDefaultLink(setting: FieldSetting) {
- const { ariaName, name } = setting;
- if (isDefaultValue(setting)) {
+ const { defVal, ariaName, name } = setting;
+ if (
+ defVal === this.props.unsavedChanges?.value ||
+ isDefaultValue(setting) ||
+ this.props.loading
+ ) {
return;
}
return (
@@ -726,7 +604,7 @@ export class Field extends PureComponent {
}
renderChangeImageLink(setting: FieldSetting) {
- const { changeImage } = this.state;
+ const changeImage = this.props.unsavedChanges?.changeImage;
const { type, value, ariaName, name } = setting;
if (type !== 'image' || !value || changeImage) {
return;
@@ -752,84 +630,49 @@ export class Field extends PureComponent {
);
}
- renderActions(setting: FieldSetting) {
- const { ariaName, name } = setting;
- const { loading, isInvalid, changeImage, savedValue, unsavedValue } = this.state;
- const isDisabled = loading || setting.isOverridden;
-
- if (savedValue === unsavedValue && !changeImage) {
- return;
- }
-
- return (
-
-
-
-
-
-
-
-
- (changeImage ? this.cancelChangeImage() : this.cancelEdit())}
- disabled={isDisabled}
- data-test-subj={`advancedSetting-cancelEditField-${name}`}
- >
-
-
-
-
-
- );
- }
-
render() {
- const { setting } = this.props;
- const { error, isInvalid } = this.state;
+ const { setting, unsavedChanges } = this.props;
+ const error = unsavedChanges?.error;
+ const isInvalid = unsavedChanges?.isInvalid;
+
+ const className = classNames('mgtAdvancedSettings__field', {
+ 'mgtAdvancedSettings__field--unsaved': unsavedChanges,
+ 'mgtAdvancedSettings__field--invalid': isInvalid,
+ });
+ const id = setting.name;
return (
-
-
-
-
- {this.renderField(setting)}
-
-
-
- {this.renderActions(setting)}
-
+
+
+ <>
+ {this.renderField(id, setting)}
+ {unsavedChanges && (
+
+
+ {unsavedChanges.error
+ ? unsavedChanges.error
+ : i18n.translate('advancedSettings.field.settingIsUnsaved', {
+ defaultMessage: 'Setting is currently not saved.',
+ })}
+
+
+ )}
+ >
+
+
);
}
}
diff --git a/src/plugins/advanced_settings/public/management_app/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts
index 5c86519116fe9..d1b9b34515532 100644
--- a/src/plugins/advanced_settings/public/management_app/components/field/index.ts
+++ b/src/plugins/advanced_settings/public/management_app/components/field/index.ts
@@ -17,4 +17,4 @@
* under the License.
*/
-export { Field } from './field';
+export { Field, getEditableValue } from './field';
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
index 8c471f5f5be9c..bce9cb67537db 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
+++ b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
@@ -1,449 +1,849 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Form should not render no settings message when instructed not to 1`] = ` `;
+exports[`Form should not render no settings message when instructed not to 1`] = `
+
+
+
+
+
+
+
+
+ General
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X-pack
+
+
+
+
+
+
+
+
+ ,
+ "settingsCount": 9,
+ }
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+`;
exports[`Form should render no settings message when there are no settings 1`] = `
-
-
+
+
+
+
-
- ,
- }
- }
+
+
+ General
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X-pack
+
+
+
+
+
+
+
+
+ ,
+ "settingsCount": 9,
+ }
+ }
+ />
+
+
+
+
+
+
+
+
+
+
`;
exports[`Form should render normally 1`] = `
-
-
-
-
-
+
+
+
+
-
- General
-
-
-
-
-
-
+
+ General
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
- Dashboard
-
-
-
-
-
-
+
+ Dashboard
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- X-pack
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
- ,
- "settingsCount": 9,
+
+
+ X-pack
+
+
+
+
+
+
+
+
+ ,
+ "settingsCount": 9,
+ }
}
- }
- />
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+ toasts={Object {}}
+ />
+
+
+
+
`;
exports[`Form should render read-only when saving is disabled 1`] = `
-
-
-
-
-
+
+
+
+
-
- General
-
-
-
-
-
-
+
+ General
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
- Dashboard
-
-
-
-
-
-
+
+ Dashboard
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- X-pack
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
- ,
- "settingsCount": 9,
+
+
+ X-pack
+
+
+
+
+
+
+
+
+ ,
+ "settingsCount": 9,
+ }
}
- }
- />
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+ toasts={Object {}}
+ />
+
+
+
+
`;
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
new file mode 100644
index 0000000000000..02ebb90221d90
--- /dev/null
+++ b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
@@ -0,0 +1,13 @@
+@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/components/nav_drawer/variables';
+
+.mgtAdvancedSettingsForm__bottomBar {
+ margin-left: $euiNavDrawerWidthCollapsed;
+ z-index: 9; // Puts it inuder the nav drawer when expanded
+ &--pushForNav {
+ margin-left: $euiNavDrawerWidthExpanded;
+ }
+ @include euiBreakpoint('xs', 's') {
+ margin-left: 0;
+ }
+}
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss
new file mode 100644
index 0000000000000..2ef4ef1d20ce9
--- /dev/null
+++ b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss
@@ -0,0 +1 @@
+@import './form';
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx
index 468cfbfc70820..0e942665b23a9 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx
+++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx
@@ -18,9 +18,14 @@
*/
import React from 'react';
-import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
+import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
import { UiSettingsType } from '../../../../../../core/public';
+// @ts-ignore
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+import { notificationServiceMock } from '../../../../../../core/public/mocks';
+import { SettingsChanges } from '../../types';
import { Form } from './form';
jest.mock('../field', () => ({
@@ -29,6 +34,25 @@ jest.mock('../field', () => ({
},
}));
+beforeAll(() => {
+ const localStorage: Record = {
+ 'core.chrome.isLocked': true,
+ };
+
+ Object.defineProperty(window, 'localStorage', {
+ value: {
+ getItem: (key: string) => {
+ return localStorage[key] || null;
+ },
+ },
+ writable: true,
+ });
+});
+
+afterAll(() => {
+ delete (window as any).localStorage;
+});
+
const defaults = {
requiresPageReload: false,
readOnly: false,
@@ -43,50 +67,52 @@ const defaults = {
const settings = {
dashboard: [
{
+ ...defaults,
name: 'dashboard:test:setting',
ariaName: 'dashboard test setting',
displayName: 'Dashboard test setting',
category: ['dashboard'],
- ...defaults,
+ requiresPageReload: true,
},
],
general: [
{
+ ...defaults,
name: 'general:test:date',
ariaName: 'general test date',
displayName: 'Test date',
description: 'bar',
category: ['general'],
- ...defaults,
},
{
+ ...defaults,
name: 'setting:test',
ariaName: 'setting test',
displayName: 'Test setting',
description: 'foo',
category: ['general'],
- ...defaults,
},
],
'x-pack': [
{
+ ...defaults,
name: 'xpack:test:setting',
ariaName: 'xpack test setting',
displayName: 'X-Pack test setting',
category: ['x-pack'],
description: 'bar',
- ...defaults,
},
],
};
+
const categories = ['general', 'dashboard', 'hiddenCategory', 'x-pack'];
const categoryCounts = {
general: 2,
dashboard: 1,
'x-pack': 10,
};
-const save = (key: string, value: any) => Promise.resolve(true);
-const clear = (key: string) => Promise.resolve(true);
+const save = jest.fn((changes: SettingsChanges) => Promise.resolve([true]));
+
const clearQuery = () => {};
describe('Form', () => {
@@ -94,10 +120,10 @@ describe('Form', () => {
const component = shallowWithI18nProvider(
+ );
+ (wrapper.instance() as Form).setState({
+ unsavedChanges: {
+ 'dashboard:test:setting': {
+ value: 'changedValue',
+ },
+ },
+ });
+ const updated = wrapper.update();
+ expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(true);
+ await findTestSubject(updated, `advancedSetting-cancelButton`).simulate('click');
+ updated.update();
+ expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(false);
+ });
+
+ it('should show a reload toast when saving setting requiring a page reload', async () => {
+ const toasts = notificationServiceMock.createStartContract().toasts;
+ const wrapper = mountWithI18nProvider(
+
+ );
+ (wrapper.instance() as Form).setState({
+ unsavedChanges: {
+ 'dashboard:test:setting': {
+ value: 'changedValue',
+ },
+ },
+ });
+ const updated = wrapper.update();
+
+ findTestSubject(updated, `advancedSetting-saveButton`).simulate('click');
+ expect(save).toHaveBeenCalled();
+ await save({ 'dashboard:test:setting': 'changedValue' });
+ expect(toasts.add).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: expect.stringContaining(
+ 'One or more settings require you to reload the page to take effect.'
+ ),
+ })
+ );
+ });
});
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx
index 91d587866836e..ef433dd990d33 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx
+++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx
@@ -18,7 +18,7 @@
*/
import React, { PureComponent, Fragment } from 'react';
-
+import classNames from 'classnames';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -27,30 +27,188 @@ import {
EuiPanel,
EuiSpacer,
EuiText,
+ EuiTextColor,
+ EuiBottomBar,
+ EuiButton,
+ EuiToolTip,
+ EuiButtonEmpty,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { isEmpty } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { toMountPoint } from '../../../../../kibana_react/public';
import { DocLinksStart, ToastsStart } from '../../../../../../core/public';
import { getCategoryName } from '../../lib';
-import { Field } from '../field';
-import { FieldSetting } from '../../types';
+import { Field, getEditableValue } from '../field';
+import { FieldSetting, SettingsChanges, FieldState } from '../../types';
type Category = string;
+const NAV_IS_LOCKED_KEY = 'core.chrome.isLocked';
interface FormProps {
settings: Record;
+ visibleSettings: Record;
categories: Category[];
categoryCounts: Record;
clearQuery: () => void;
- save: (key: string, value: any) => Promise;
- clear: (key: string) => Promise;
+ save: (changes: SettingsChanges) => Promise;
showNoResultsMessage: boolean;
enableSaving: boolean;
dockLinks: DocLinksStart['links'];
toasts: ToastsStart;
}
+interface FormState {
+ unsavedChanges: {
+ [key: string]: FieldState;
+ };
+ loading: boolean;
+}
+
export class Form extends PureComponent {
+ state: FormState = {
+ unsavedChanges: {},
+ loading: false,
+ };
+
+ setLoading(loading: boolean) {
+ this.setState({
+ loading,
+ });
+ }
+
+ getSettingByKey = (key: string): FieldSetting | undefined => {
+ return Object.values(this.props.settings)
+ .flat()
+ .find(el => el.name === key);
+ };
+
+ getCountOfUnsavedChanges = (): number => {
+ return Object.keys(this.state.unsavedChanges).length;
+ };
+
+ getCountOfHiddenUnsavedChanges = (): number => {
+ const shownSettings = Object.values(this.props.visibleSettings)
+ .flat()
+ .map(setting => setting.name);
+ return Object.keys(this.state.unsavedChanges).filter(key => !shownSettings.includes(key))
+ .length;
+ };
+
+ areChangesInvalid = (): boolean => {
+ const { unsavedChanges } = this.state;
+ return Object.values(unsavedChanges).some(({ isInvalid }) => isInvalid);
+ };
+
+ handleChange = (key: string, change: FieldState) => {
+ const setting = this.getSettingByKey(key);
+ if (!setting) {
+ return;
+ }
+ const { type, defVal, value } = setting;
+ const savedValue = getEditableValue(type, value, defVal);
+ if (change.value === savedValue) {
+ return this.clearChange(key);
+ }
+ this.setState({
+ unsavedChanges: {
+ ...this.state.unsavedChanges,
+ [key]: change,
+ },
+ });
+ };
+
+ clearChange = (key: string) => {
+ if (!this.state.unsavedChanges[key]) {
+ return;
+ }
+ const unsavedChanges = { ...this.state.unsavedChanges };
+ delete unsavedChanges[key];
+
+ this.setState({
+ unsavedChanges,
+ });
+ };
+
+ clearAllUnsaved = () => {
+ this.setState({ unsavedChanges: {} });
+ };
+
+ saveAll = async () => {
+ this.setLoading(true);
+ const { unsavedChanges } = this.state;
+
+ if (isEmpty(unsavedChanges)) {
+ return;
+ }
+ const configToSave: SettingsChanges = {};
+ let requiresReload = false;
+
+ Object.entries(unsavedChanges).forEach(([name, { value }]) => {
+ const setting = this.getSettingByKey(name);
+ if (!setting) {
+ return;
+ }
+ const { defVal, type, requiresPageReload } = setting;
+ let valueToSave = value;
+ let equalsToDefault = false;
+ switch (type) {
+ case 'array':
+ valueToSave = valueToSave.split(',').map((val: string) => val.trim());
+ equalsToDefault = valueToSave.join(',') === (defVal as string[]).join(',');
+ break;
+ case 'json':
+ const isArray = Array.isArray(JSON.parse((defVal as string) || '{}'));
+ valueToSave = valueToSave.trim();
+ valueToSave = valueToSave || (isArray ? '[]' : '{}');
+ default:
+ equalsToDefault = valueToSave === defVal;
+ }
+ if (requiresPageReload) {
+ requiresReload = true;
+ }
+ configToSave[name] = equalsToDefault ? null : valueToSave;
+ });
+
+ try {
+ await this.props.save(configToSave);
+ this.clearAllUnsaved();
+ if (requiresReload) {
+ this.renderPageReloadToast();
+ }
+ } catch (e) {
+ this.props.toasts.addDanger(
+ i18n.translate('advancedSettings.form.saveErrorMessage', {
+ defaultMessage: 'Unable to save',
+ })
+ );
+ }
+ this.setLoading(false);
+ };
+
+ renderPageReloadToast = () => {
+ this.props.toasts.add({
+ title: i18n.translate('advancedSettings.form.requiresPageReloadToastDescription', {
+ defaultMessage: 'One or more settings require you to reload the page to take effect.',
+ }),
+ text: toMountPoint(
+ <>
+
+
+ window.location.reload()}>
+ {i18n.translate('advancedSettings.form.requiresPageReloadToastButtonLabel', {
+ defaultMessage: 'Reload page',
+ })}
+
+
+
+ >
+ ),
+ color: 'success',
+ });
+ };
+
renderClearQueryLink(totalSettings: number, currentSettings: number) {
const { clearQuery } = this.props;
@@ -102,8 +260,9 @@ export class Form extends PureComponent {
{
return null;
}
+ renderCountOfUnsaved = () => {
+ const unsavedCount = this.getCountOfUnsavedChanges();
+ const hiddenUnsavedCount = this.getCountOfHiddenUnsavedChanges();
+ return (
+
+
+
+ );
+ };
+
+ renderBottomBar = () => {
+ const areChangesInvalid = this.areChangesInvalid();
+ const bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', {
+ 'mgtAdvancedSettingsForm__bottomBar--pushForNav':
+ localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true',
+ });
+ return (
+
+
+
+ {this.renderCountOfUnsaved()}
+
+
+
+
+
+ {i18n.translate('advancedSettings.form.cancelButtonLabel', {
+ defaultMessage: 'Cancel changes',
+ })}
+
+
+
+
+
+ {i18n.translate('advancedSettings.form.saveButtonLabel', {
+ defaultMessage: 'Save changes',
+ })}
+
+
+
+
+
+
+
+ );
+ };
+
render() {
- const { settings, categories, categoryCounts, clearQuery } = this.props;
+ const { unsavedChanges } = this.state;
+ const { visibleSettings, categories, categoryCounts, clearQuery } = this.props;
const currentCategories: Category[] = [];
categories.forEach(category => {
- if (settings[category] && settings[category].length) {
+ if (visibleSettings[category] && visibleSettings[category].length) {
currentCategories.push(category);
}
});
return (
- {currentCategories.length
- ? currentCategories.map(category => {
- return this.renderCategory(category, settings[category], categoryCounts[category]);
- })
- : this.maybeRenderNoSettings(clearQuery)}
+
+ {currentCategories.length
+ ? currentCategories.map(category => {
+ return this.renderCategory(
+ category,
+ visibleSettings[category],
+ categoryCounts[category]
+ );
+ })
+ : this.maybeRenderNoSettings(clearQuery)}
+
+ {!isEmpty(unsavedChanges) && this.renderBottomBar()}
);
}
diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts
index 05bb5e754563d..d44a05ce36f5d 100644
--- a/src/plugins/advanced_settings/public/management_app/types.ts
+++ b/src/plugins/advanced_settings/public/management_app/types.ts
@@ -47,6 +47,19 @@ export interface FieldSetting {
}
// until eui searchbar and query are typed
+
+export interface SettingsChanges {
+ [key: string]: any;
+}
+
+export interface FieldState {
+ value?: any;
+ changeImage?: boolean;
+ loading?: boolean;
+ isInvalid?: boolean;
+ error?: string | null;
+}
+
export interface IQuery {
ast: any; // incomplete
text: string;
diff --git a/src/plugins/telemetry/public/components/telemetry_management_section.tsx b/src/plugins/telemetry/public/components/telemetry_management_section.tsx
index 20c8873b13272..bf14c33a48048 100644
--- a/src/plugins/telemetry/public/components/telemetry_management_section.tsx
+++ b/src/plugins/telemetry/public/components/telemetry_management_section.tsx
@@ -33,8 +33,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { PRIVACY_STATEMENT_URL } from '../../common/constants';
import { OptInExampleFlyout } from './opt_in_example_flyout';
-// @ts-ignore
import { Field } from '../../../advanced_settings/public';
+import { ToastsStart } from '../../../../core/public/';
import { TelemetryService } from '../services/telemetry_service';
const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data'];
@@ -44,12 +44,14 @@ interface Props {
showAppliesSettingMessage: boolean;
enableSaving: boolean;
query?: any;
+ toasts: ToastsStart;
}
interface State {
processing: boolean;
showExample: boolean;
queryMatches: boolean | null;
+ enabled: boolean;
}
export class TelemetryManagementSection extends Component {
@@ -57,6 +59,7 @@ export class TelemetryManagementSection extends Component {
processing: false,
showExample: false,
queryMatches: null,
+ enabled: this.props.telemetryService.getIsOptedIn() || false,
};
UNSAFE_componentWillReceiveProps(nextProps: Props) {
@@ -79,7 +82,7 @@ export class TelemetryManagementSection extends Component {
render() {
const { telemetryService } = this.props;
- const { showExample, queryMatches } = this.state;
+ const { showExample, queryMatches, enabled, processing } = this.state;
if (!telemetryService.getCanChangeOptInStatus()) {
return null;
@@ -119,7 +122,7 @@ export class TelemetryManagementSection extends Component {
displayName: i18n.translate('telemetry.provideUsageStatisticsTitle', {
defaultMessage: 'Provide usage statistics',
}),
- value: telemetryService.getIsOptedIn(),
+ value: enabled,
description: this.renderDescription(),
defVal: true,
ariaName: i18n.translate('telemetry.provideUsageStatisticsAriaName', {
@@ -127,10 +130,10 @@ export class TelemetryManagementSection extends Component {
}),
} as any
}
+ loading={processing}
dockLinks={null as any}
toasts={null as any}
- save={this.toggleOptIn}
- clear={this.toggleOptIn}
+ handleChange={this.toggleOptIn}
enableSaving={this.props.enableSaving}
/>
@@ -151,13 +154,13 @@ export class TelemetryManagementSection extends Component {
),
@@ -200,20 +203,35 @@ export class TelemetryManagementSection extends Component {
);
toggleOptIn = async (): Promise => {
- const { telemetryService } = this.props;
- const newOptInValue = !telemetryService.getIsOptedIn();
+ const { telemetryService, toasts } = this.props;
+ const newOptInValue = !this.state.enabled;
return new Promise((resolve, reject) => {
- this.setState({ processing: true }, async () => {
- try {
- await telemetryService.setOptIn(newOptInValue);
- this.setState({ processing: false });
- resolve(true);
- } catch (err) {
- this.setState({ processing: false });
- reject(err);
+ this.setState(
+ {
+ processing: true,
+ enabled: newOptInValue,
+ },
+ async () => {
+ try {
+ await telemetryService.setOptIn(newOptInValue);
+ this.setState({ processing: false });
+ toasts.addSuccess(
+ newOptInValue
+ ? i18n.translate('telemetry.optInSuccessOn', {
+ defaultMessage: 'Usage data collection turned on.',
+ })
+ : i18n.translate('telemetry.optInSuccessOff', {
+ defaultMessage: 'Usage data collection turned off.',
+ })
+ );
+ resolve(true);
+ } catch (err) {
+ this.setState({ processing: false });
+ reject(err);
+ }
}
- });
+ );
});
};
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index d7e5064cf7280..ff340c6b0abcd 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -94,7 +94,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
`[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]`
);
await PageObjects.header.waitUntilLoadingHasFinished();
- await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`);
+ await testSubjects.click(`advancedSetting-saveButton`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
@@ -102,14 +102,14 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
await input.clearValue();
await input.type(propertyValue);
- await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`);
+ await testSubjects.click(`advancedSetting-saveButton`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async toggleAdvancedSettingCheckbox(propertyName: string) {
testSubjects.click(`advancedSetting-editField-${propertyName}`);
await PageObjects.header.waitUntilLoadingHasFinished();
- await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`);
+ await testSubjects.click(`advancedSetting-saveButton`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 4b06645cdfe04..78bb39dd22dea 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -1618,8 +1618,6 @@
"advancedSettings.categoryNames.timelionLabel": "Timelion",
"advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション",
"advancedSettings.categorySearchLabel": "カテゴリー",
- "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル",
- "advancedSettings.field.cancelEditingButtonLabel": "キャンセル",
"advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更",
"advancedSettings.field.changeImageLinkText": "画像を変更",
"advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文",
@@ -1632,17 +1630,10 @@
"advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です",
"advancedSettings.field.offLabel": "オフ",
"advancedSettings.field.onLabel": "オン",
- "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み",
- "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。",
- "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした",
"advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット",
"advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット",
- "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存",
- "advancedSettings.field.saveButtonLabel": "保存",
- "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした",
"advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)",
"advancedSettings.form.clearSearchResultText": "(検索結果を消去)",
- "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}",
"advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}",
"advancedSettings.pageTitle": "設定",
"advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません",
@@ -2474,8 +2465,6 @@
"statusPage.statusApp.statusTitle": "プラグインステータス",
"statusPage.statusTable.columns.idHeader": "ID",
"statusPage.statusTable.columns.statusHeader": "ステータス",
- "telemetry.callout.appliesSettingTitle": "この設定は {allOfKibanaText} に適用されます",
- "telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて",
"telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。",
"telemetry.callout.clusterStatisticsTitle": "クラスター統計",
"telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ecf4dfbb33be6..fc9dacf0e50f7 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1618,8 +1618,6 @@
"advancedSettings.categoryNames.timelionLabel": "Timelion",
"advancedSettings.categoryNames.visualizationsLabel": "可视化",
"advancedSettings.categorySearchLabel": "类别",
- "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}",
- "advancedSettings.field.cancelEditingButtonLabel": "取消",
"advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}",
"advancedSettings.field.changeImageLinkText": "更改图片",
"advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效",
@@ -1632,14 +1630,8 @@
"advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}",
"advancedSettings.field.offLabel": "关闭",
"advancedSettings.field.onLabel": "开启",
- "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面",
- "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。",
- "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}",
"advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值",
"advancedSettings.field.resetToDefaultLinkText": "重置为默认值",
- "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}",
- "advancedSettings.field.saveButtonLabel": "保存",
- "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}",
"advancedSettings.form.clearNoSearchResultText": "(清除搜索)",
"advancedSettings.form.clearSearchResultText": "(清除搜索)",
"advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}",
@@ -2474,8 +2466,6 @@
"statusPage.statusApp.statusTitle": "插件状态",
"statusPage.statusTable.columns.idHeader": "ID",
"statusPage.statusTable.columns.statusHeader": "状态",
- "telemetry.callout.appliesSettingTitle": "此设置适用于{allOfKibanaText}",
- "telemetry.callout.appliesSettingTitle.allOfKibanaText": "所有 Kibana。",
"telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。",
"telemetry.callout.clusterStatisticsTitle": "集群统计信息",
"telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障或者有网络错误。检查 Kibana,然后重新加载页面并重试。",
From 256e4ab67c7fccae9aae38aac22f3788466f8b2f Mon Sep 17 00:00:00 2001
From: Brandon Kobel
Date: Mon, 24 Feb 2020 09:40:37 -0800
Subject: [PATCH 05/21] Adding xpack.encryptedSavedObjects.encryptionKey to
docker allow-list (#58291)
Co-authored-by: Elastic Machine
---
.../os_packages/docker_generator/resources/bin/kibana-docker | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index 34ba25f92beb6..d4d2e86e1e96b 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -142,6 +142,7 @@ kibana_vars=(
xpack.code.security.enableGitCertCheck
xpack.code.security.gitHostWhitelist
xpack.code.security.gitProtocolWhitelist
+ xpack.encryptedSavedObjects.encryptionKey
xpack.graph.enabled
xpack.graph.canEditDrillDownUrls
xpack.graph.savePolicy
From b88b99140bc0d63036c0789d1ddc8dc9597e2b5e Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Mon, 24 Feb 2020 18:44:24 +0100
Subject: [PATCH 06/21] [ML] Fix transforms license check. (#58343)
Fixes an error where the transforms page would load blank with an expired license. Fixes the issue by adding a type guard. With an expired license, the page now renders again correctly the error message.
---
.../lib/authorization/components/common.ts | 24 +++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts
index 5aec2ac041db3..27556e0d673a8 100644
--- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts
+++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts
@@ -21,19 +21,33 @@ export interface Privileges {
missingPrivileges: MissingPrivileges;
}
+function isPrivileges(arg: any): arg is Privileges {
+ return (
+ typeof arg === 'object' &&
+ arg !== null &&
+ arg.hasOwnProperty('hasAllPrivileges') &&
+ typeof arg.hasAllPrivileges === 'boolean' &&
+ arg.hasOwnProperty('missingPrivileges') &&
+ typeof arg.missingPrivileges === 'object' &&
+ arg.missingPrivileges !== null
+ );
+}
+
export interface MissingPrivileges {
[key: string]: string[] | undefined;
}
export const toArray = (value: string | string[]): string[] =>
Array.isArray(value) ? value : [value];
-export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privilege) => {
+export const hasPrivilegeFactory = (privileges: Privileges | undefined | null) => (
+ privilege: Privilege
+) => {
const [section, requiredPrivilege] = privilege;
- if (!privileges.missingPrivileges[section]) {
+ if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) {
// if the section does not exist in our missingPrivileges, everything is OK
return true;
}
- if (privileges.missingPrivileges[section]!.length === 0) {
+ if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) {
return true;
}
if (requiredPrivilege === '*') {
@@ -42,7 +56,9 @@ export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privi
}
// If we require _some_ privilege, we make sure that the one
// we require is *not* in the missingPrivilege array
- return !privileges.missingPrivileges[section]!.includes(requiredPrivilege);
+ return (
+ isPrivileges(privileges) && !privileges.missingPrivileges[section]!.includes(requiredPrivilege)
+ );
};
// create the text for button's tooltips if the user
From 12f35d5788f5250803434b6d8f25d9df82ac0940 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Mon, 24 Feb 2020 10:23:44 -0800
Subject: [PATCH 07/21] Add ingest manager header component (#58300)
Co-authored-by: Elastic Machine
---
x-pack/legacy/plugins/ingest_manager/index.ts | 2 +
.../ingest_manager/components/header.tsx | 62 ++++
.../ingest_manager/components/index.ts | 1 +
.../ingest_manager/layouts/default.tsx | 29 +-
.../ingest_manager/layouts/index.tsx | 1 +
.../ingest_manager/layouts/with_header.tsx | 29 ++
.../sections/agent_config/list_page/index.tsx | 291 +++++++++---------
...illustration_kibana_getting_started@2x.png | Bin 0 -> 131132 bytes
.../ingest_manager/sections/epm/index.tsx | 69 ++++-
.../ingest_manager/sections/fleet/index.tsx | 46 ++-
.../sections/overview/index.tsx | 32 +-
11 files changed, 397 insertions(+), 165 deletions(-)
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png
diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts
index c20cc7225d780..7ed5599b234a3 100644
--- a/x-pack/legacy/plugins/ingest_manager/index.ts
+++ b/x-pack/legacy/plugins/ingest_manager/index.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { resolve } from 'path';
import {
savedObjectMappings,
OUTPUT_SAVED_OBJECT_TYPE,
@@ -18,6 +19,7 @@ import {
export function ingestManager(kibana: any) {
return new kibana.Plugin({
id: 'ingestManager',
+ publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'),
uiExports: {
savedObjectSchemas: {
[AGENT_CONFIG_SAVED_OBJECT_TYPE]: {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx
new file mode 100644
index 0000000000000..0936b5dcfed10
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 React from 'react';
+import styled from 'styled-components';
+import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
+import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
+
+const Container = styled.div`
+ border-bottom: ${props => props.theme.eui.euiBorderThin};
+ background-color: ${props => props.theme.eui.euiPageBackgroundColor};
+`;
+
+const Wrapper = styled.div`
+ max-width: 1200px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-top: ${props => props.theme.eui.paddingSizes.xl};
+`;
+
+const Tabs = styled(EuiTabs)`
+ top: 1px;
+ &:before {
+ height: 0px;
+ }
+`;
+
+export interface HeaderProps {
+ leftColumn?: JSX.Element;
+ rightColumn?: JSX.Element;
+ tabs?: EuiTabProps[];
+}
+
+export const Header: React.FC = ({ leftColumn, rightColumn, tabs }) => (
+
+
+
+ {leftColumn ? {leftColumn} : null}
+ {rightColumn ? {rightColumn} : null}
+
+
+ {tabs ? (
+
+
+ {tabs.map(props => (
+
+ {props.name}
+
+ ))}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts
index 5133d82588494..b6bb29462c569 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts
@@ -4,3 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { Loading } from './loading';
+export { Header, HeaderProps } from './header';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
index eaf49fed3d933..f99d1bfe50026 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
@@ -4,17 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
-import {
- EuiPage,
- EuiPageBody,
- EuiTabs,
- EuiTab,
- EuiFlexGroup,
- EuiFlexItem,
- EuiIcon,
-} from '@elastic/eui';
+import styled from 'styled-components';
+import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import euiStyled from '../../../../../../legacy/common/eui_styled_components';
import { Section } from '../sections';
import { useLink, useConfig } from '../hooks';
import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants';
@@ -24,7 +16,12 @@ interface Props {
children?: React.ReactNode;
}
-const Nav = euiStyled.nav`
+const Container = styled.div`
+ min-height: calc(100vh - ${props => props.theme.eui.euiHeaderChildSize});
+ background: ${props => props.theme.eui.euiColorEmptyShade};
+`;
+
+const Nav = styled.nav`
background: ${props => props.theme.eui.euiColorEmptyShade};
border-bottom: ${props => props.theme.eui.euiBorderThin};
padding: ${props =>
@@ -32,13 +29,13 @@ const Nav = euiStyled.nav`
.euiTabs {
padding-left: 3px;
margin-left: -3px;
- };
+ }
`;
export const DefaultLayout: React.FunctionComponent = ({ section, children }) => {
const { epm, fleet } = useConfig();
return (
-
+
@@ -82,9 +79,7 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre
-
- {children}
-
-
+ {children}
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx
index 858951bd0d38f..a9ef7f1656260 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx
@@ -4,3 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { DefaultLayout } from './default';
+export { WithHeaderLayout } from './with_header';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx
new file mode 100644
index 0000000000000..d59c99316c8b8
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 React, { Fragment } from 'react';
+import styled from 'styled-components';
+import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
+import { Header, HeaderProps } from '../components';
+
+const Page = styled(EuiPage)`
+ background: ${props => props.theme.eui.euiColorEmptyShade};
+`;
+
+interface Props extends HeaderProps {
+ children?: React.ReactNode;
+}
+
+export const WithHeaderLayout: React.FC = ({ children, ...rest }) => (
+
+
+
+
+
+ {children}
+
+
+
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index ca9fb195166f6..ef5a38d486901 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
@@ -5,9 +5,6 @@
*/
import React, { useState } from 'react';
import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
EuiSpacer,
EuiText,
EuiFlexGroup,
@@ -24,11 +21,43 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AgentConfig } from '../../../types';
import { DEFAULT_AGENT_CONFIG_ID, AGENT_CONFIG_DETAILS_PATH } from '../../../constants';
+import { WithHeaderLayout } from '../../../layouts';
// import { SearchBar } from '../../../components';
import { useGetAgentConfigs, usePagination, useLink } from '../../../hooks';
import { AgentConfigDeleteProvider } from '../components';
import { CreateAgentConfigFlyout } from './components';
+const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ {children}
+
+);
+
export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
// Create agent config flyout state
const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState(
@@ -123,71 +152,46 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
);
return (
-
-
- {isCreateAgentConfigFlyoutOpen ? (
- {
- setIsCreateAgentConfigFlyoutOpen(false);
- sendRequest();
- }}
- />
- ) : null}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {selectedAgentConfigs.length ? (
-
-
- {deleteAgentConfigsPrompt => (
- {
- deleteAgentConfigsPrompt(
- selectedAgentConfigs.map(agentConfig => agentConfig.id),
- () => {
- sendRequest();
- setSelectedAgentConfigs([]);
- }
- );
+
+ {isCreateAgentConfigFlyoutOpen ? (
+ {
+ setIsCreateAgentConfigFlyoutOpen(false);
+ sendRequest();
+ }}
+ />
+ ) : null}
+
+ {selectedAgentConfigs.length ? (
+
+
+ {deleteAgentConfigsPrompt => (
+ {
+ deleteAgentConfigsPrompt(
+ selectedAgentConfigs.map(agentConfig => agentConfig.id),
+ () => {
+ sendRequest();
+ setSelectedAgentConfigs([]);
+ }
+ );
+ }}
+ >
+
-
-
- )}
-
-
- ) : null}
-
- {/*
+
+ )}
+
+
+ ) : null}
+
+ {/* {
setPagination({
@@ -198,83 +202,82 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
}}
fieldPrefix={AGENT_CONFIG_SAVED_OBJECT_TYPE}
/> */}
-
-
- sendRequest()}>
-
-
-
-
- setIsCreateAgentConfigFlyoutOpen(true)}
- >
-
-
-
-
+
+
+ sendRequest()}>
+
+
+
+
+ setIsCreateAgentConfigFlyoutOpen(true)}
+ >
+
+
+
+
-
-
- ) : !search.trim() && agentConfigData?.total === 0 ? (
- emptyPrompt
- ) : (
- setSearch('')}>
-
-
- ),
- }}
- />
- )
- }
- items={agentConfigData ? agentConfigData.items : []}
- itemId="id"
- columns={columns}
- isSelectable={true}
- selection={{
- selectable: (agentConfig: AgentConfig) => agentConfig.id !== DEFAULT_AGENT_CONFIG_ID,
- onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => {
- setSelectedAgentConfigs(newSelectedAgentConfigs);
- },
- }}
- pagination={{
- pageIndex: pagination.currentPage - 1,
- pageSize: pagination.pageSize,
- totalItemCount: agentConfigData ? agentConfigData.total : 0,
- }}
- onChange={({ page }: { page: { index: number; size: number } }) => {
- const newPagination = {
- ...pagination,
- currentPage: page.index + 1,
- pageSize: page.size,
- };
- setPagination(newPagination);
- sendRequest(); // todo: fix this to send pagination options
- }}
- />
-
-
+
+
+ ) : !search.trim() && agentConfigData?.total === 0 ? (
+ emptyPrompt
+ ) : (
+ setSearch('')}>
+
+
+ ),
+ }}
+ />
+ )
+ }
+ items={agentConfigData ? agentConfigData.items : []}
+ itemId="id"
+ columns={columns}
+ isSelectable={true}
+ selection={{
+ selectable: (agentConfig: AgentConfig) => agentConfig.id !== DEFAULT_AGENT_CONFIG_ID,
+ onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => {
+ setSelectedAgentConfigs(newSelectedAgentConfigs);
+ },
+ }}
+ pagination={{
+ pageIndex: pagination.currentPage - 1,
+ pageSize: pagination.pageSize,
+ totalItemCount: agentConfigData ? agentConfigData.total : 0,
+ }}
+ onChange={({ page }: { page: { index: number; size: number } }) => {
+ const newPagination = {
+ ...pagination,
+ currentPage: page.index + 1,
+ pageSize: page.size,
+ };
+ setPagination(newPagination);
+ sendRequest(); // todo: fix this to send pagination options
+ }}
+ />
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..cad64be0b6e36e79012970814c7018776bee5f73
GIT binary patch
literal 131132
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
z