diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93d49dc18d417a..be2b4533e22db6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -132,7 +132,7 @@ /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui # Maps -/x-pack/plugins/maps/ @elastic/kibana-gis +#CC# /x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 13c1d20552fa15..3c0e63fae0daa9 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -9,7 +9,7 @@ Alerts and actions are enabled by default in {kib}, but require you configure th . <>. . <>. -. <>. +. If you are using an *on-premises* Elastic Stack deployment, <>. You can configure the following settings in the `kibana.yml` file. diff --git a/packages/kbn-config/src/__mocks__/env.ts b/packages/kbn-config/src/__mocks__/env.ts index 3bb24debda7807..8b7475680ecf57 100644 --- a/packages/kbn-config/src/__mocks__/env.ts +++ b/packages/kbn-config/src/__mocks__/env.ts @@ -30,7 +30,6 @@ export function getEnvOptions(options: DeepPartial = {}): EnvOptions configs: options.configs || [], cliArgs: { dev: true, - open: false, quiet: false, silent: false, watch: false, diff --git a/packages/kbn-config/src/__snapshots__/env.test.ts.snap b/packages/kbn-config/src/__snapshots__/env.test.ts.snap index 09cd1acbd7177a..9236c83f9c921c 100644 --- a/packages/kbn-config/src/__snapshots__/env.test.ts.snap +++ b/packages/kbn-config/src/__snapshots__/env.test.ts.snap @@ -10,7 +10,6 @@ Env { "disableOptimizer": true, "dist": false, "envName": "development", - "open": false, "oss": false, "quiet": false, "repl": false, @@ -56,7 +55,6 @@ Env { "disableOptimizer": true, "dist": false, "envName": "production", - "open": false, "oss": false, "quiet": false, "repl": false, @@ -101,7 +99,6 @@ Env { "dev": true, "disableOptimizer": true, "dist": false, - "open": false, "oss": false, "quiet": false, "repl": false, @@ -146,7 +143,6 @@ Env { "dev": false, "disableOptimizer": true, "dist": false, - "open": false, "oss": false, "quiet": false, "repl": false, @@ -191,7 +187,6 @@ Env { "dev": false, "disableOptimizer": true, "dist": false, - "open": false, "oss": false, "quiet": false, "repl": false, @@ -236,7 +231,6 @@ Env { "dev": false, "disableOptimizer": true, "dist": false, - "open": false, "oss": false, "quiet": false, "repl": false, diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index bc6d2ae070acf1..d02c841e613379 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -38,7 +38,6 @@ export interface CliArgs { watch: boolean; repl: boolean; basePath: boolean; - open: boolean; oss: boolean; /** @deprecated use disableOptimizer to know if the @kbn/optimizer is disabled in development */ optimize?: boolean; diff --git a/packages/kbn-legacy-logging/yarn.lock b/packages/kbn-legacy-logging/yarn.lock deleted file mode 120000 index 3f82ebc9cdbae3..00000000000000 --- a/packages/kbn-legacy-logging/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/src/cli/cluster/cluster_manager.test.ts b/src/cli/cluster/cluster_manager.test.ts index a8e139533d397c..1d2986e742527d 100644 --- a/src/cli/cluster/cluster_manager.test.ts +++ b/src/cli/cluster/cluster_manager.test.ts @@ -38,7 +38,6 @@ import { Worker } from './worker'; const CLI_ARGS: SomeCliArgs = { disableOptimizer: true, - open: false, oss: false, quiet: false, repl: false, diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index f8ef388a82f280..7a14f617b5d5a4 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -18,10 +18,8 @@ */ import { resolve } from 'path'; -import { format as formatUrl } from 'url'; import Fs from 'fs'; -import opn from 'opn'; import { REPO_ROOT } from '@kbn/utils'; import { FSWatcher } from 'chokidar'; import * as Rx from 'rxjs'; @@ -41,7 +39,6 @@ export type SomeCliArgs = Pick< | 'silent' | 'repl' | 'disableOptimizer' - | 'open' | 'watch' | 'oss' | 'runExamples' @@ -50,7 +47,7 @@ export type SomeCliArgs = Pick< >; const firstAllTrue = (...sources: Array>) => - Rx.combineLatest(...sources).pipe( + Rx.combineLatest(sources).pipe( filter((values) => values.every((v) => v === true)), take(1), mapTo(undefined) @@ -144,17 +141,6 @@ export class ClusterManager { }); }); - if (opts.open) { - this.setupOpen( - formatUrl({ - protocol: config.get('server.ssl.enabled') ? 'https' : 'http', - hostname: config.get('server.host'), - port: config.get('server.port'), - pathname: this.basePathProxy ? this.basePathProxy.basePath : '', - }) - ); - } - if (opts.watch) { const pluginPaths = config.get('plugins.paths'); const scanDirs = [ @@ -206,14 +192,6 @@ export class ClusterManager { } } - setupOpen(openUrl: string) { - firstAllTrue(this.serverReady$, this.kbnOptimizerReady$) - .toPromise() - .then(() => { - opn(openUrl); - }); - } - setupWatching(extraPaths: string[], pluginInternalDirsIgnore: string[]) { // eslint-disable-next-line @typescript-eslint/no-var-requires const chokidar = require('chokidar'); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 158b7ac8948434..61f880d80633da 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -192,7 +192,6 @@ export default function (program) { if (DEV_MODE_SUPPORTED) { command .option('--dev', 'Run the server with development mode defaults') - .option('--open', 'Open a browser window to the base url after the server is started') .option('--ssl', 'Run the dev server using HTTPS') .option('--dist', 'Use production assets from kbn/optimizer') .option( @@ -222,7 +221,6 @@ export default function (program) { configs: [].concat(opts.config || []), cliArgs: { dev: !!opts.dev, - open: !!opts.open, envName: unknownOptions.env ? unknownOptions.env.name : undefined, quiet: !!opts.quiet, silent: !!opts.silent, diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 30bb1f71a7ec0f..3161420b94d22d 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -70,7 +70,6 @@ export function createRootWithSettings( configs: [], cliArgs: { dev: false, - open: false, quiet: false, silent: false, watch: false, diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index b80d6caf54f4f9..2a7087b5b2806c 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -30,7 +30,7 @@ interface Props { [key: string]: any; } -export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => { +export const CheckBoxField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const ComboBoxField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { // Errors for the comboBox value (the "array") const errorMessageField = field.getErrorsMessages(); @@ -87,7 +87,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const MultiSelectField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const MultiSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const NumericField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const NumericField = ({ field, euiFieldProps = {}, ...rest }: Props) => { error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const RadioGroupField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const RadioGroupField = ({ field, euiFieldProps = {}, ...rest }: Props) = error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const RangeField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const { onChange: onFieldChange } = field; @@ -50,7 +50,7 @@ export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => { error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const SelectField = ({ field, euiFieldProps, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -44,7 +44,7 @@ export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => { error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const SuperSelectField = ({ + field, + euiFieldProps = { options: [] }, + idAria, + ...rest +}: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -42,7 +47,7 @@ export const SuperSelectField = ({ field, euiFieldProps = { options: [] }, ...re error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const TextAreaField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const TextAreaField = ({ field, euiFieldProps = {}, ...rest }: Props) => error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const TextField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -39,7 +39,7 @@ export const TextField = ({ field, euiFieldProps = {}, ...rest }: Props) => { error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > { +export const ToggleField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); // Shim for sufficient overlap between EuiSwitchEvent and FieldHook[onChange] event @@ -46,7 +46,7 @@ export const ToggleField = ({ field, euiFieldProps = {}, ...rest }: Props) => { error={errorMessage} isInvalid={isInvalid} fullWidth - describedByIds={rest.idAria ? [rest.idAria] : undefined} + describedByIds={idAria ? [idAria] : undefined} {...rest} > ; export type MetricsItemsSchema = TypeOf; export type PanelSchema = TypeOf; export type VisPayload = TypeOf; + +interface PanelData { + id: string; + label: string; + data: Array<[number, number]>; +} + +// series data is not fully typed yet +interface SeriesData { + [key: string]: { + annotations: { + [key: string]: unknown[]; + }; + id: string; + series: PanelData[]; + error?: unknown; + }; +} + +export type TimeseriesVisData = SeriesData & { + type: PANEL_TYPES; + uiRestrictions: TimeseriesUIRestrictions; + /** + * series array is responsible only for "table" vis type + */ + series?: unknown[]; +}; diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts index e2911eb2d70e3b..1be428f1b93544 100644 --- a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts +++ b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts @@ -63,7 +63,7 @@ export const DEFAULT_UI_RESTRICTION: UIRestrictions = { * @constant * @public */ -export const limitOfSeries = { +export const limitOfSeries: Partial> = { [PANEL_TYPES.GAUGE]: 1, [PANEL_TYPES.METRIC]: 2, }; diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 9ec5ae1424ae3c..7f17a9c44298a3 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -251,7 +251,14 @@ export const panel = schema.object({ ), time_field: stringOptionalNullable, time_range_mode: stringOptionalNullable, - type: stringRequired, + type: schema.oneOf([ + schema.literal('table'), + schema.literal('gauge'), + schema.literal('markdown'), + schema.literal('top_n'), + schema.literal('timeseries'), + schema.literal('metric'), + ]), }); export const visPayloadSchema = schema.object({ @@ -267,7 +274,6 @@ export const visPayloadSchema = schema.object({ }) ), }), - savedObjectId: schema.maybe(schema.string()), timerange: schema.object({ timezone: stringRequired, min: stringRequired, diff --git a/src/plugins/vis_type_timeseries/public/application/components/no_data.js b/src/plugins/vis_type_timeseries/public/application/components/no_data.js deleted file mode 100644 index 1968f1858a46c0..00000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/no_data.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; - -export function NoDataComponent() { - return ( -
- - -
- ); -} diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx new file mode 100644 index 00000000000000..5b5c99b9708542 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useEffect } from 'react'; + +import { IUiSettingsClient } from 'src/core/public'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { PersistedState } from 'src/plugins/visualizations/public'; + +// @ts-expect-error +import { ErrorComponent } from './error'; +import { TimeseriesVisTypes } from './vis_types'; +import { TimeseriesVisParams } from '../../metrics_fn'; +import { TimeseriesVisData } from '../../../common/types'; + +interface TimeseriesVisualizationProps { + className?: string; + getConfig: IUiSettingsClient['get']; + handlers: IInterpreterRenderHandlers; + model: TimeseriesVisParams; + visData: TimeseriesVisData; + uiState: PersistedState; +} + +function TimeseriesVisualization({ + className = 'tvbVis', + visData, + model, + handlers, + uiState, + getConfig, +}: TimeseriesVisualizationProps) { + const onBrush = useCallback( + (gte: string, lte: string) => { + handlers.event({ + name: 'applyFilter', + data: { + timeFieldName: '*', + filters: [ + { + range: { + '*': { + gte, + lte, + }, + }, + }, + ], + }, + }); + }, + [handlers] + ); + + const handleUiState = useCallback( + (field: string, value: { column: string; order: string }) => { + uiState.set(field, value); + // reload visualization because data might need to be re-fetched + uiState.emit('reload'); + }, + [uiState] + ); + + useEffect(() => { + handlers.done(); + }); + + // Show the error panel + const error = visData[model.id]?.error; + if (error) { + return ( +
+ +
+ ); + } + + const VisComponent = TimeseriesVisTypes[model.type]; + + if (VisComponent) { + return ( + + ); + } + + return
; +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TimeseriesVisualization as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 57adecd9d598b2..083ccaf8e5073d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -23,10 +23,8 @@ import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; import { VisEditorVisualization } from './vis_editor_visualization'; -import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; -import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; @@ -49,7 +47,6 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler((data) => props.vis.API.events.applyFilter(data)); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); @@ -71,12 +68,6 @@ export class VisEditor extends Component { return this.props.config.get(...args); }; - handleUiState = (field, value) => { - this.props.vis.uiState.set(field, value); - // reload visualization because data might need to be re-fetched - this.props.vis.uiState.emit('reload'); - }; - updateVisState = debounce(() => { this.props.vis.params = this.state.model; this.props.embeddableHandler.reload(); @@ -101,16 +92,14 @@ export class VisEditor extends Component { dirty = false; } - if (this.props.isEditorMode) { - const extractedIndexPatterns = extractIndexPatterns(nextModel); - if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { - fetchFields(extractedIndexPatterns).then((visFields) => - this.setState({ - visFields, - extractedIndexPatterns, - }) - ); - } + const extractedIndexPatterns = extractIndexPatterns(nextModel); + if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { + fetchFields(extractedIndexPatterns).then((visFields) => + this.setState({ + visFields, + extractedIndexPatterns, + }) + ); } this.setState({ @@ -141,23 +130,6 @@ export class VisEditor extends Component { }; render() { - if (!this.props.isEditorMode) { - if (!this.props.visParams || !this.props.visData) { - return null; - } - return ( - - ); - } - const { model } = this.state; if (model) { @@ -211,23 +183,12 @@ export class VisEditor extends Component { } componentDidMount() { - this.props.renderComplete(); - - if (this.props.isEditorMode && this.props.eventEmitter) { - this.props.eventEmitter.on('updateEditor', this.updateModel); - } - } - - componentDidUpdate() { - this.props.renderComplete(); + this.props.eventEmitter.on('updateEditor', this.updateModel); } componentWillUnmount() { this.updateVisState.cancel(); - - if (this.props.isEditorMode && this.props.eventEmitter) { - this.props.eventEmitter.off('updateEditor', this.updateModel); - } + this.props.eventEmitter.off('updateEditor', this.updateModel); } } @@ -241,7 +202,6 @@ VisEditor.propTypes = { visFields: PropTypes.object, renderComplete: PropTypes.func, config: PropTypes.object, - isEditorMode: PropTypes.bool, savedObj: PropTypes.object, timeRange: PropTypes.object, appState: PropTypes.object, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js index 325e9c83727368..5a6d5f39d0c974 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js @@ -101,4 +101,8 @@ GaugeVisualization.propTypes = { getConfig: PropTypes.func, }; -export const gauge = visWithSplits(GaugeVisualization); +const gauge = visWithSplits(GaugeVisualization); + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { gauge as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts new file mode 100644 index 00000000000000..56e58b4da34580 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { IUiSettingsClient } from 'src/core/public'; +import { PersistedState } from 'src/plugins/visualizations/public'; + +import { TimeseriesVisParams } from '../../../metrics_fn'; +import { TimeseriesVisData } from '../../../../common/types'; + +/** + * Lazy load each visualization type, since the only one is presented on the screen at the same time. + * Disable typescript errors since the components are not typed yet. + */ + +// @ts-expect-error +const timeseries = lazy(() => import('./timeseries/vis')); +// @ts-expect-error +const metric = lazy(() => import('./metric/vis')); +// @ts-expect-error +const topN = lazy(() => import('./top_n/vis')); +// @ts-expect-error +const table = lazy(() => import('./table/vis')); +// @ts-expect-error +const gauge = lazy(() => import('./gauge/vis')); +// @ts-expect-error +const markdown = lazy(() => import('./markdown/vis')); + +export const TimeseriesVisTypes: Record> = { + timeseries, + metric, + top_n: topN, + table, + gauge, + markdown, +}; + +export interface TimeseriesVisProps { + model: TimeseriesVisParams; + onBrush: (gte: string, lte: string) => void; + onUiState: ( + field: string, + value: { + column: string; + order: string; + } + ) => void; + uiState: PersistedState; + visData: TimeseriesVisData; + dateFormat: string; + getConfig: IUiSettingsClient['get']; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js index a4e7738e923185..e68b9e5ed8467e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/markdown/vis.js @@ -30,7 +30,7 @@ import { isBackgroundInverted } from '../../../lib/set_is_reversed'; const getMarkdownId = (id) => `markdown-${id}`; -export function MarkdownVisualization(props) { +function MarkdownVisualization(props) { const { backgroundColor, model, visData, dateFormat } = props; const series = get(visData, `${model.id}.series`, []); const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig); @@ -106,3 +106,7 @@ MarkdownVisualization.propTypes = { dateFormat: PropTypes.string, getConfig: PropTypes.func, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { MarkdownVisualization as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index 5fe7afe47df9b6..7069a32fa2b50a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -95,4 +95,8 @@ MetricVisualization.propTypes = { getConfig: PropTypes.func, }; -export const metric = visWithSplits(MetricVisualization); +const metric = visWithSplits(MetricVisualization); + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { metric as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 92109e1a374268..a31be694cd172a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -46,7 +46,7 @@ function getColor(rules, colorKey, value) { return color; } -export class TableVis extends Component { +class TableVis extends Component { constructor(props) { super(props); @@ -260,3 +260,7 @@ TableVis.propTypes = { pageNumber: PropTypes.number, getConfig: PropTypes.func, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TableVis as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index f936710bf2b813..b752699fa1548f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -34,7 +34,7 @@ import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; import { getCoreStart } from '../../../../services'; -export class TimeseriesVisualization extends Component { +class TimeseriesVisualization extends Component { static propTypes = { model: PropTypes.object, onBrush: PropTypes.func, @@ -44,7 +44,8 @@ export class TimeseriesVisualization extends Component { }; xAxisFormatter = (interval) => (val) => { - const { scaledDataFormat, dateFormat } = this.props.visData; + const scaledDataFormat = this.props.getConfig('dateFormat:scaled'); + const { dateFormat } = this.props; if (!scaledDataFormat || !dateFormat) { return val; @@ -245,3 +246,7 @@ export class TimeseriesVisualization extends Component { ); } } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TimeseriesVisualization as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index 1c2ebb8264ef37..92311b3f33afb8 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -48,7 +48,7 @@ function sortSeries(visData, model) { }, []); } -export function TopNVisualization(props) { +function TopNVisualization(props) { const { backgroundColor, model, visData } = props; const series = sortSeries(visData, model).map((item) => { @@ -111,3 +111,7 @@ TopNVisualization.propTypes = { visData: PropTypes.object, getConfig: PropTypes.func, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TopNVisualization as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/visualization.js b/src/plugins/vis_type_timeseries/public/application/components/visualization.js deleted file mode 100644 index 8b8218653f97cc..00000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/visualization.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'lodash'; - -import { TimeseriesVisualization } from './vis_types/timeseries/vis'; -import { metric } from './vis_types/metric/vis'; -import { TopNVisualization as topN } from './vis_types/top_n/vis'; -import { TableVis as table } from './vis_types/table/vis'; -import { gauge } from './vis_types/gauge/vis'; -import { MarkdownVisualization as markdown } from './vis_types/markdown/vis'; -import { ErrorComponent } from './error'; -import { NoDataComponent } from './no_data'; - -const types = { - timeseries: TimeseriesVisualization, - metric, - top_n: topN, - table, - gauge, - markdown, -}; - -export function Visualization(props) { - const { visData, model } = props; - // Show the error panel - const error = _.get(visData, `${model.id}.error`); - if (error) { - return ( -
- -
- ); - } - - const path = visData.type === 'table' ? 'series' : `${model.id}.series`; - const noData = _.get(visData, path, []).length === 0; - if (noData) { - return ( -
- -
- ); - } - - const component = types[model.type]; - if (component) { - return React.createElement(component, { - dateFormat: props.dateFormat, - backgroundColor: props.backgroundColor, - model: props.model, - onBrush: props.onBrush, - onChange: props.onChange, - onUiState: props.onUiState, - uiState: props.uiState, - visData: visData.type === model.type ? visData : {}, - getConfig: props.getConfig, - }); - } - return
; -} - -Visualization.propTypes = { - backgroundColor: PropTypes.string, - className: PropTypes.string, - model: PropTypes.object, - onBrush: PropTypes.func, - onChange: PropTypes.func, - onUiState: PropTypes.func, - uiState: PropTypes.object, - visData: PropTypes.object, - dateFormat: PropTypes.string, - getConfig: PropTypes.func, -}; - -Visualization.defaultProps = { - className: 'tvbVis', -}; diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index f21b5f947bca79..548bf2623fc1af 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -70,7 +70,6 @@ export class EditorController { visParams={this.state.vis.params} timeRange={params.timeRange} renderComplete={() => {}} - isEditorMode={true} appState={params.appState} embeddableHandler={this.embeddableHandler} eventEmitter={this.eventEmitter} diff --git a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts b/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts deleted file mode 100644 index a9568b5be9d3fa..00000000000000 --- a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createBrushHandler } from './create_brush_handler'; -import { ExprVisAPIEvents } from '../../../../visualizations/public'; - -describe('brushHandler', () => { - let onBrush: ReturnType; - let applyFilter: ExprVisAPIEvents['applyFilter']; - - beforeEach(() => { - applyFilter = jest.fn(); - - onBrush = createBrushHandler(applyFilter); - }); - - test('returns brushHandler() should updates timefilter through vis.API.events.applyFilter', () => { - const gte = '2017-01-01T00:00:00Z'; - const lte = '2017-01-01T00:10:00Z'; - - onBrush(gte, lte); - - expect(applyFilter).toHaveBeenCalledWith({ - timeFieldName: '*', - filters: [ - { - range: { '*': { gte: '2017-01-01T00:00:00Z', lte: '2017-01-01T00:10:00Z' } }, - }, - ], - }); - }); -}); diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 8652d703f963e3..60acd35b224024 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -17,38 +17,36 @@ * under the License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaContext } from '../../data/public'; import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; -// @ts-ignore +import { PanelSchema, TimeseriesVisData } from '../common/types'; import { metricsRequestHandler } from './request_handler'; type Input = KibanaContext | null; -type Output = Promise>; +type Output = Promise>; interface Arguments { params: string; uiState: string; - savedObjectId: string | null; } -type VisParams = Required; +export type TimeseriesVisParams = PanelSchema; -interface RenderValue { - visType: 'metrics'; - visData: Input; - visConfig: VisParams; - uiState: any; +export interface TimeseriesRenderValue { + visData: TimeseriesVisData | {}; + visParams: TimeseriesVisParams; } -export const createMetricsFn = (): ExpressionFunctionDefinition< +export type TimeseriesExpressionFunctionDefinition = ExpressionFunctionDefinition< 'tsvb', Input, Arguments, Output -> => ({ +>; + +export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({ name: 'tsvb', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -66,37 +64,22 @@ export const createMetricsFn = (): ExpressionFunctionDefinition< default: '"{}"', help: '', }, - savedObjectId: { - types: ['null', 'string'], - default: null, - help: '', - }, }, async fn(input, args) { - const params = JSON.parse(args.params); - const uiStateParams = JSON.parse(args.uiState); - const savedObjectId = args.savedObjectId; - const { PersistedState } = await import('../../visualizations/public'); - const uiState = new PersistedState(uiStateParams); + const visParams: TimeseriesVisParams = JSON.parse(args.params); + const uiState = JSON.parse(args.uiState); const response = await metricsRequestHandler({ - timeRange: get(input, 'timeRange', null), - query: get(input, 'query', null), - filters: get(input, 'filters', null), - visParams: params, + input, + visParams, uiState, - savedObjectId, }); - response.visType = 'metrics'; - return { type: 'render', - as: 'visualization', + as: 'timeseries_vis', value: { - uiState, - visType: 'metrics', - visConfig: params, + visParams, visData: response, }, }; diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 01b6ea07683385..2b75f696206293 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -19,12 +19,9 @@ import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { metricsRequestHandler } from './request_handler'; import { EditorController } from './application'; -// @ts-ignore import { PANEL_TYPES } from '../common/panel_types'; -import { VisEditor } from './application/components/vis_editor_lazy'; +import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; import { getDataStart } from './services'; import { INDEXES_SEPARATOR } from '../common/constants'; @@ -73,7 +70,6 @@ export const metricsVisDefinition = { show_grid: 1, tooltip_mode: 'show_all', }, - component: VisEditor, }, editor: EditorController, options: { @@ -81,7 +77,7 @@ export const metricsVisDefinition = { showFilterBar: false, showIndexSelection: false, }, - requestHandler: metricsRequestHandler, + toExpressionAst, getSupportedTriggers: () => { return [VIS_EVENT_TO_TRIGGER.applyFilter]; }, @@ -102,5 +98,4 @@ export const metricsVisDefinition = { return []; }, - responseHandler: 'none', }; diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index d98e55bdb340cf..d36b3611680af7 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -36,6 +36,7 @@ import { } from './services'; import { DataPublicPluginStart } from '../../data/public'; import { ChartsPluginSetup } from '../../charts/public'; +import { getTimeseriesVisRenderer } from './timeseries_vis_renderer'; /** @internal */ export interface MetricsPluginSetupDependencies { @@ -62,9 +63,14 @@ export class MetricsPlugin implements Plugin, void> { { expressions, visualizations, charts }: MetricsPluginSetupDependencies ) { expressions.registerFunction(createMetricsFn); + expressions.registerRenderer( + getTimeseriesVisRenderer({ + uiSettings: core.uiSettings, + }) + ); setUISettings(core.uiSettings); setChartsSetup(charts); - visualizations.createReactVisualization(metricsVisDefinition); + visualizations.createBaseVisualization(metricsVisDefinition); } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { diff --git a/src/plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.ts similarity index 54% rename from src/plugins/vis_type_timeseries/public/request_handler.js rename to src/plugins/vis_type_timeseries/public/request_handler.ts index 12b7f3d417ef6a..aa45453515277e 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.js +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -17,57 +17,52 @@ * under the License. */ +import { KibanaContext } from '../../data/public'; + import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; -import { MAX_BUCKETS_SETTING } from '../common/constants'; +import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants'; +import { TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesVisData } from '../common/types'; + +interface MetricsRequestHandlerParams { + input: KibanaContext | null; + uiState: Record; + visParams: TimeseriesVisParams; +} export const metricsRequestHandler = async ({ + input, uiState, - timeRange, - filters, - query, visParams, - savedObjectId, -}) => { +}: MetricsRequestHandlerParams): Promise => { const config = getUISettings(); const timezone = getTimezone(config); - const uiStateObj = uiState.get(visParams.type, {}); + const uiStateObj = uiState[visParams.type] ?? {}; const dataSearch = getDataStart(); - const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(timeRange); - const scaledDataFormat = config.get('dateFormat:scaled'); - const dateFormat = config.get('dateFormat'); + const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(input?.timeRange!); if (visParams && visParams.id && !visParams.isModelInvalid) { - try { - const maxBuckets = config.get(MAX_BUCKETS_SETTING); + const maxBuckets = config.get(MAX_BUCKETS_SETTING); - validateInterval(parsedTimeRange, visParams, maxBuckets); + validateInterval(parsedTimeRange, visParams, maxBuckets); - const resp = await getCoreStart().http.post('/api/metrics/vis/data', { - body: JSON.stringify({ - timerange: { - timezone, - ...parsedTimeRange, - }, - query, - filters, - panels: [visParams], - state: uiStateObj, - savedObjectId: savedObjectId || 'unsaved', - sessionId: dataSearch.search.session.getSessionId(), - }), - }); + const resp = await getCoreStart().http.post(ROUTES.VIS_DATA, { + body: JSON.stringify({ + timerange: { + timezone, + ...parsedTimeRange, + }, + query: input?.query, + filters: input?.filters, + panels: [visParams], + state: uiStateObj, + sessionId: dataSearch.search.session.getSessionId(), + }), + }); - return { - dateFormat, - scaledDataFormat, - timezone, - ...resp, - }; - } catch (error) { - return Promise.reject(error); - } + return resp; } - return Promise.resolve({}); + return {}; }; diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx new file mode 100644 index 00000000000000..67ed487d293783 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { IUiSettingsClient } from 'kibana/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { TimeseriesRenderValue, TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesVisData } from '../common/types'; + +const TimeseriesVisualization = lazy( + () => import('./application/components/timeseries_visualization') +); + +const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVisParams) => { + if ('type' in visData) { + const data = visData.type === 'table' ? visData.series : visData?.[model.id]?.series; + return Boolean(data?.length); + } + + return false; +}; + +export const getTimeseriesVisRenderer: (deps: { + uiSettings: IUiSettingsClient; +}) => ExpressionRenderDefinition = ({ uiSettings }) => ({ + name: 'timeseries_vis', + reuseDomNode: true, + render: async (domNode, config, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const showNoResult = !checkIfDataExists(config.visData, config.visParams); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts b/src/plugins/vis_type_timeseries/public/to_ast.ts similarity index 59% rename from src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts rename to src/plugins/vis_type_timeseries/public/to_ast.ts index 38002c75529523..58de0269fd27fa 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts +++ b/src/plugins/vis_type_timeseries/public/to_ast.ts @@ -17,23 +17,17 @@ * under the License. */ -import { ExprVisAPIEvents } from '../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { TimeseriesExpressionFunctionDefinition, TimeseriesVisParams } from './metrics_fn'; -export const createBrushHandler = (applyFilter: ExprVisAPIEvents['applyFilter']) => ( - gte: string, - lte: string -) => { - return applyFilter({ - timeFieldName: '*', - filters: [ - { - range: { - '*': { - gte, - lte, - }, - }, - }, - ], +export const toExpressionAst = (vis: Vis) => { + const timeseries = buildExpressionFunction('tsvb', { + params: JSON.stringify(vis.params), + uiState: JSON.stringify(vis.uiState), }); + + const ast = buildExpression([timeseries]); + + return ast.toAst(); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index fcb66d2e12fd1c..aefbe0ea78d4b4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -20,46 +20,37 @@ import { FakeRequest, RequestHandlerContext } from 'kibana/server'; import _ from 'lodash'; import { first, map } from 'rxjs/operators'; + +import { Filter, Query } from 'src/plugins/data/common'; import { getPanelData } from './vis_data/get_panel_data'; import { Framework } from '../plugin'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; - -interface GetVisDataResponse { - [key: string]: GetVisDataPanel; -} - -interface GetVisDataPanel { - id: string; - series: GetVisDataSeries[]; -} - -interface GetVisDataSeries { - id: string; - label: string; - data: GetVisDataDataPoint[]; -} - -type GetVisDataDataPoint = [number, number]; +import { TimeseriesVisData } from '../../common/types'; export interface GetVisDataOptions { - timerange?: any; - panels?: any; - filters?: any; - state?: any; - query?: any; + timerange: { + min: number | string; + max: number | string; + timezone?: string; + }; + panels: unknown[]; + filters?: Filter[]; + state?: Record; + query?: Query | Query[]; + sessionId?: string; } export type GetVisData = ( requestContext: RequestHandlerContext, options: GetVisDataOptions, framework: Framework -) => Promise; +) => Promise; export function getVisData( requestContext: RequestHandlerContext, request: FakeRequest & { body: GetVisDataOptions }, framework: Framework -): Promise { +): Promise { // NOTE / TODO: This facade has been put in place to make migrating to the New Platform easier. It // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now @@ -81,10 +72,10 @@ export function getVisData( .toPromise(); }, }; - const promises = (reqFacade.payload as GetVisDataOptions).panels.map(getPanelData(reqFacade)); + const promises = reqFacade.payload.panels.map(getPanelData(reqFacade)); return Promise.all(promises).then((res) => { return res.reduce((acc, data) => { return _.assign(acc as any, data); }, {}); - }) as Promise; + }) as Promise; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts index 235235cf6a3be2..b7644f65827614 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.ts @@ -29,7 +29,8 @@ export const getActiveSeries = (panel: PanelSchema) => { } // Toogle visibility functionality for 'gauge', 'markdown' is not accessible - const shouldNotApplyFilter = [PANEL_TYPES.GAUGE, PANEL_TYPES.MARKDOWN].includes(panel.type); + const shouldNotApplyFilter = + PANEL_TYPES.GAUGE === panel.type || PANEL_TYPES.MARKDOWN === panel.type; return visibleSeries.filter((series) => !series.hidden || shouldNotApplyFilter); }; diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 1ca8b57ab230f4..bba086720da0a1 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -21,6 +21,7 @@ import { IRouter, KibanaRequest } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from '../../common/vis_schema'; +import { ROUTES } from '../../common/constants'; import { ValidationTelemetryServiceSetup } from '../index'; import { Framework } from '../plugin'; @@ -33,7 +34,7 @@ export const visDataRoutes = ( ) => { router.post( { - path: '/api/metrics/vis/data', + path: ROUTES.VIS_DATA, validate: { body: escapeHatch, }, @@ -43,11 +44,9 @@ export const visDataRoutes = ( visPayloadSchema.validate(request.body); } catch (error) { logFailedValidation(); - const savedObjectId = - (typeof request.body === 'object' && (request.body as any).savedObjectId) || - 'unavailable'; + framework.logger.warn( - `Request validation error: ${error.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md` + `Request validation error: ${error.message}. This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md` ); } diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index 978c67fb71721f..94c5da872b1cb0 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -3,6 +3,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = `
+
{showNoResult ? handlers.done()} /> : children} diff --git a/src/plugins/visualizations/public/components/visualization_noresults.tsx b/src/plugins/visualizations/public/components/visualization_noresults.tsx index c77ef4490a4b3a..90d5353185229e 100644 --- a/src/plugins/visualizations/public/components/visualization_noresults.tsx +++ b/src/plugins/visualizations/public/components/visualization_noresults.tsx @@ -30,7 +30,7 @@ export class VisualizationNoResults extends React.Component +
diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index 2c6cfc6fb74621..03a355c604c4d7 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -4,8 +4,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls t exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 0c210a04d20075..653542bd8837de 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -101,12 +101,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - it('handles metrics/tsvb function', () => { - const params = { foo: 'bar' }; - const actual = buildPipelineVisFunction.metrics(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 3593d62b9d2e67..d412ec918a71a9 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -222,13 +222,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; }, - metrics: ({ title, ...params }, schemas, uiState = {}) => { - const paramsJson = prepareJson('params', params); - const uiStateJson = prepareJson('uiState', uiState); - - const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); - return `tsvb ${paramsArray.join(' ')}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index 0be4fbbebe7c57..b3812af38c3480 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }) { }); it('tsvb time series shows no data message', async () => { - expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true); + expect(await testSubjects.exists('timeseriesVis > visNoResult')).to.be(true); }); it('metric value shows no data', async () => { diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 37634d0248b047..0e305eaafc82f2 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -549,7 +549,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async checkPreviewIsDisabled(): Promise { log.debug(`Check no data message is present`); - await testSubjects.existOrFail('noTSVBDataMessage', { timeout: 5000 }); + await testSubjects.existOrFail('timeseriesVis > visNoResult', { timeout: 5000 }); } public async cloneSeries(nth: number = 0): Promise { diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts index 1137abdb474acc..c226c59a542467 100644 --- a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts +++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsType } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; export const apmIndices: SavedObjectsType = { name: 'apm-indices', @@ -32,4 +33,12 @@ export const apmIndices: SavedObjectsType = { }, }, }, + management: { + importableAndExportable: true, + icon: 'apmApp', + getTitle: () => + i18n.translate('xpack.apm.apmSettings.index', { + defaultMessage: 'APM Settings - Index', + }), + }, }; diff --git a/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts b/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts index 8d60c4aa61dcaa..bfabe79f32110a 100644 --- a/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts +++ b/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts @@ -8,7 +8,6 @@ import { isValidNamespace } from './is_valid_namespace'; describe('Fleet - isValidNamespace', () => { it('returns true for valid namespaces', () => { expect(isValidNamespace('default').valid).toBe(true); - expect(isValidNamespace('namespace-with-dash').valid).toBe(true); expect(isValidNamespace('123').valid).toBe(true); expect(isValidNamespace('testlength๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€๐Ÿ˜€').valid).toBe( true @@ -19,6 +18,7 @@ describe('Fleet - isValidNamespace', () => { expect(isValidNamespace('').valid).toBe(false); expect(isValidNamespace(' ').valid).toBe(false); expect(isValidNamespace('Default').valid).toBe(false); + expect(isValidNamespace('namespace-with-dash').valid).toBe(false); expect(isValidNamespace('namespace with spaces').valid).toBe(false); expect(isValidNamespace('foo/bar').valid).toBe(false); expect(isValidNamespace('foo\\bar').valid).toBe(false); diff --git a/x-pack/plugins/fleet/common/services/is_valid_namespace.ts b/x-pack/plugins/fleet/common/services/is_valid_namespace.ts index 8bd8349580edc4..b70dc8ab67bb28 100644 --- a/x-pack/plugins/fleet/common/services/is_valid_namespace.ts +++ b/x-pack/plugins/fleet/common/services/is_valid_namespace.ts @@ -23,7 +23,7 @@ export function isValidNamespace(namespace: string): { valid: boolean; error?: s defaultMessage: 'Namespace must be lowercase', }), }; - } else if (/[\*\\/\?"<>|\s,#:]+/.test(namespace)) { + } else if (/[\*\\/\?"<>|\s,#:-]+/.test(namespace)) { return { valid: false, error: i18n.translate('xpack.fleet.namespaceValidation.invalidCharactersErrorMessage', { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx index ae2385d7142192..ab9e7cafb7c011 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx @@ -47,6 +47,8 @@ export const LogQueryBar: React.FunctionComponent<{ return ( ` uses the `"default"` source confiuration, but if ``` +### Custom columns + +It is possible to change what columns are loaded without creating a whole new source configuration. To do so the component supports the `columns` prop. The default configuration can be replicated as follows. + +```tsx + +``` + +There are three column types: + + + + + +
`type: "timestamp"` + The configured timestamp field. Defaults to `@timestamp`. +
`type: "message"` + The value of the `message` field if it exists. If it doesn't, the component will try to recompose the original log line using values of other fields. +
`type: "field"` + A specific field specified in the `field` property. +
+ ### Considerations As mentioned in the prerequisites, the component relies on `kibana-react` to access kibana's core services. If this is not the case the component will throw an exception when rendering. We advise to use an `` in your component hierarchy to catch this error if necessary. diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index a880996daaade3..c4e6bbe0946428 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -11,13 +11,18 @@ import { euiStyled } from '../../../../observability/public'; import { LogEntriesCursor } from '../../../common/http_api'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { useLogSource } from '../../containers/logs/log_source'; +import { LogSourceConfigurationProperties, useLogSource } from '../../containers/logs/log_source'; import { useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; const PAGE_THRESHOLD = 2; +type LogColumnDefinition = + | { type: 'timestamp' } + | { type: 'message' } + | { type: 'field'; field: string }; + export interface LogStreamProps { sourceId?: string; startTimestamp: number; @@ -26,6 +31,7 @@ export interface LogStreamProps { center?: LogEntriesCursor; highlight?: string; height?: string | number; + columns?: LogColumnDefinition[]; } export const LogStream: React.FC = ({ @@ -36,7 +42,13 @@ export const LogStream: React.FC = ({ center, highlight, height = '400px', + columns, }) => { + const customColumns = useMemo( + () => (columns ? convertLogColumnDefinitionToLogSourceColumnDefinition(columns) : undefined), + [columns] + ); + // source boilerplate const { services } = useKibana(); if (!services?.http?.fetch) { @@ -74,6 +86,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re endTimestamp, query, center, + columns: customColumns, }); // Derived state @@ -83,8 +96,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const isLoadingMore = pageLoadingState === 'loading'; const columnConfigurations = useMemo(() => { - return sourceConfiguration ? sourceConfiguration.configuration.logColumns : []; - }, [sourceConfiguration]); + return sourceConfiguration ? customColumns ?? sourceConfiguration.configuration.logColumns : []; + }, [sourceConfiguration, customColumns]); const streamItems = useMemo( () => @@ -163,6 +176,21 @@ const LogStreamContent = euiStyled.div<{ height: string }>` height: ${(props) => props.height}; `; +function convertLogColumnDefinitionToLogSourceColumnDefinition( + columns: LogColumnDefinition[] +): LogSourceConfigurationProperties['logColumns'] { + return columns.map((column) => { + switch (column.type) { + case 'timestamp': + return { timestampColumn: { id: '___#timestamp' } }; + case 'message': + return { messageColumn: { id: '___#message' } }; + case 'field': + return { fieldColumn: { id: `___#${column.field}`, field: column.field } }; + } + }); +} + // Allow for lazy loading // eslint-disable-next-line import/no-default-export export default LogStream; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index 566edcce913183..b0b09c76f4d85f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -12,6 +12,7 @@ import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { LogEntry, LogEntriesCursor } from '../../../../common/http_api'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { LogSourceConfigurationProperties } from '../log_source'; interface LogStreamProps { sourceId: string; @@ -19,6 +20,7 @@ interface LogStreamProps { endTimestamp: number; query?: string; center?: LogEntriesCursor; + columns?: LogSourceConfigurationProperties['logColumns']; } interface LogStreamState { @@ -60,6 +62,7 @@ export function useLogStream({ endTimestamp, query, center, + columns, }: LogStreamProps): LogStreamReturn { const { services } = useKibanaContextForPlugin(); const [state, setState] = useSetState(INITIAL_STATE); @@ -100,6 +103,7 @@ export function useLogStream({ startTimestamp, endTimestamp, query: parsedQuery, + columns, ...fetchPosition, }, services.http.fetch diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 1cf0afd50b80c5..e10eb1d7e8aad5 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -15,6 +15,7 @@ import { LogEntriesItem, LogEntriesCursor, LogColumn, + LogEntriesRequest, } from '../../../../common/http_api'; import { InfraSourceConfiguration, @@ -73,7 +74,8 @@ export class InfraLogEntriesDomain { public async getLogEntriesAround( requestContext: RequestHandlerContext, sourceId: string, - params: LogEntriesAroundParams + params: LogEntriesAroundParams, + columnOverrides?: LogEntriesRequest['columns'] ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params; @@ -97,7 +99,8 @@ export class InfraLogEntriesDomain { cursor: { before: center }, size: Math.floor(halfSize), highlightTerm, - } + }, + columnOverrides ); /* @@ -131,13 +134,16 @@ export class InfraLogEntriesDomain { public async getLogEntries( requestContext: RequestHandlerContext, sourceId: string, - params: LogEntriesParams + params: LogEntriesParams, + columnOverrides?: LogEntriesRequest['columns'] ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { configuration } = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId ); + const columnDefinitions = columnOverrides ?? configuration.logColumns; + const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -155,7 +161,7 @@ export class InfraLogEntriesDomain { return { id: doc.id, cursor: doc.cursor, - columns: configuration.logColumns.map( + columns: columnDefinitions.map( (column): LogColumn => { if ('timestampColumn' in column) { return { diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts index 2baf3fd7aa9901..67083ee9d6c0d6 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts @@ -31,6 +31,7 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) sourceId, query, size, + columns, } = payload; let entries; @@ -47,7 +48,8 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) query: parseFilterQuery(query), center: payload.center, size, - } + }, + columns )); } else { let cursor: LogEntriesParams['cursor']; @@ -66,7 +68,8 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) query: parseFilterQuery(query), cursor, size, - } + }, + columns )); } diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index 45b14543946c7c..7512c180970ad1 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -23,7 +23,6 @@ @import 'components/controls/index'; @import 'components/entity_cell/index'; @import 'components/field_title_bar/index'; - @import 'components/field_type_icon/index'; @import 'components/influencers_list/index'; @import 'components/items_grid/index'; @import 'components/job_selector/index'; diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 9dc0814e3a3e66..d3a055f957c3aa 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -120,8 +120,6 @@ export const renderApp = ( urlGenerators: deps.share.urlGenerators, }); - deps.kibanaLegacy.loadFontAwesome(); - appMountParams.onAppLeave((actions) => actions.default()); const mlLicense = setLicenseCache(deps.licensing, [ diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss b/x-pack/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss index 75118266d45dba..77d95653638aca 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss @@ -12,7 +12,7 @@ .field-type-icon { vertical-align: middle; - padding-right: $euiSizeXS; + margin-bottom: -$euiSizeXS; display: inline-block; } diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.tsx similarity index 63% rename from x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js rename to x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.tsx index b467b6bfa3654f..aed66550554d3c 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.tsx @@ -5,26 +5,22 @@ */ import { mountWithIntl } from '@kbn/test/jest'; + import React from 'react'; import { FieldTitleBar } from './field_title_bar'; - -// helper to let PropTypes throw errors instead of just doing console.error() -const error = console.error; -console.error = (warning, ...args) => { - if (/(Invalid prop|Failed prop type)/gi.test(warning)) { - throw new Error(warning); - } - error.apply(console, [warning, ...args]); -}; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; describe('FieldTitleBar', () => { - test(`throws an error because card is a required prop`, () => { - expect(() => ).toThrow(); - }); - test(`card prop is an empty object`, () => { - const props = { card: {} }; + const props = { + card: { + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + loading: false, + aggregatable: true, + }, + }; const wrapper = mountWithIntl(); @@ -36,29 +32,43 @@ describe('FieldTitleBar', () => { }); test(`card.isUnsupportedType is true`, () => { - const testFieldName = 'foo'; - const props = { card: { fieldName: testFieldName, isUnsupportedType: true } }; + const props = { + card: { + type: ML_JOB_FIELD_TYPES.UNKNOWN, + fieldName: 'foo', + existsInDocs: true, + loading: false, + aggregatable: true, + isUnsupportedType: true, + }, + }; const wrapper = mountWithIntl(); const fieldName = wrapper.find({ className: 'field-name' }).text(); - expect(fieldName).toEqual(testFieldName); + expect(fieldName).toEqual(props.card.fieldName); const hasClassName = wrapper.find('EuiText').hasClass('type-other'); expect(hasClassName).toBeTruthy(); }); test(`card.fieldName and card.type is set`, () => { - const testFieldName = 'foo'; - const testType = 'bar'; - const props = { card: { fieldName: testFieldName, type: testType } }; + const props = { + card: { + type: ML_JOB_FIELD_TYPES.KEYWORD, + fieldName: 'bar', + existsInDocs: true, + loading: false, + aggregatable: true, + }, + }; const wrapper = mountWithIntl(); const fieldName = wrapper.find({ className: 'field-name' }).text(); - expect(fieldName).toEqual(testFieldName); + expect(fieldName).toEqual(props.card.fieldName); - const hasClassName = wrapper.find('EuiText').hasClass(testType); + const hasClassName = wrapper.find('EuiText').hasClass(props.card.type); expect(hasClassName).toBeTruthy(); }); @@ -66,11 +76,19 @@ describe('FieldTitleBar', () => { // Use fake timers so we don't have to wait for the EuiToolTip timeout jest.useFakeTimers(); - const props = { card: { fieldName: 'foo', type: 'bar' } }; + const props = { + card: { + type: ML_JOB_FIELD_TYPES.KEYWORD, + fieldName: 'bar', + existsInDocs: true, + loading: false, + aggregatable: true, + }, + }; const wrapper = mountWithIntl(); const container = wrapper.find({ className: 'field-name' }); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); container.simulate('mouseover'); @@ -78,7 +96,7 @@ describe('FieldTitleBar', () => { jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(3); container.simulate('mouseout'); @@ -86,7 +104,7 @@ describe('FieldTitleBar', () => { jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); // Clearing all mocks will also reset fake timers. jest.clearAllMocks(); diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx similarity index 68% rename from x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.js rename to x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx index 28aa15c2cfab06..0e98a23637f036 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.js +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.tsx @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { FC } from 'react'; import { EuiText, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { FieldTypeIcon } from '../field_type_icon'; +import { FieldVisConfig } from '../../datavisualizer/index_based/common'; import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; -import { i18n } from '@kbn/i18n'; -export function FieldTitleBar({ card }) { - // don't render and fail gracefully if card prop isn't set - if (typeof card !== 'object' || card === null) { - return null; - } +interface Props { + card: FieldVisConfig; +} +export const FieldTitleBar: FC = ({ card }) => { const fieldName = card.fieldName || i18n.translate('xpack.ml.fieldTitleBar.documentCountLabel', { @@ -37,20 +37,23 @@ export function FieldTitleBar({ card }) { } if (card.isUnsupportedType !== true) { - cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type)); + // All the supported field types have aria labels. + cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type)!); } return ( - + -
+
{fieldName}
); -} -FieldTitleBar.propTypes = { - card: PropTypes.object.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/index.js b/x-pack/plugins/ml/public/application/components/field_title_bar/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_title_bar/index.js rename to x-pack/plugins/ml/public/application/components/field_title_bar/index.ts diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap b/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap deleted file mode 100644 index 3952d0b090a7dc..00000000000000 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldTypeIcon render component when type matches a field type 1`] = ` - -`; - -exports[`FieldTypeIcon update component 1`] = ` - -`; diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap new file mode 100644 index 00000000000000..769ebdeba9955c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldTypeIcon render component when type matches a field type 1`] = ` + + + +`; diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss b/x-pack/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss deleted file mode 100644 index 741974c56987e8..00000000000000 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss +++ /dev/null @@ -1,22 +0,0 @@ -$icon-size: 20px; - -.field-type-icon-container { - display: inline-block !important; - vertical-align: middle; - border: 1px solid; - border-radius: 4px; - width: $icon-size; - height: $icon-size; - line-height: $icon-size; - text-align: center; - position: relative; - - .field-type-icon { - padding: 0; - display: inline-block !important; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } -} diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/_index.scss b/x-pack/plugins/ml/public/application/components/field_type_icon/_index.scss deleted file mode 100644 index afd1cb353edb4e..00000000000000 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'field_type_icon'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.tsx similarity index 63% rename from x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js rename to x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.tsx index d4200c2f8366b4..667cca99389cf5 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.tsx @@ -11,18 +11,10 @@ import { FieldTypeIcon } from './field_type_icon'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; describe('FieldTypeIcon', () => { - test(`don't render component when type is undefined`, () => { - const typeIconComponent = shallow(); - expect(typeIconComponent.isEmptyRender()).toBeTruthy(); - }); - - test(`don't render component when type doesn't match a field type`, () => { - const typeIconComponent = shallow(); - expect(typeIconComponent.isEmptyRender()).toBeTruthy(); - }); - test(`render component when type matches a field type`, () => { - const typeIconComponent = shallow(); + const typeIconComponent = shallow( + + ); expect(typeIconComponent).toMatchSnapshot(); }); @@ -31,9 +23,9 @@ describe('FieldTypeIcon', () => { jest.useFakeTimers(); const typeIconComponent = mount( - + ); - const container = typeIconComponent.find({ className: 'field-type-icon-container' }); + const container = typeIconComponent.find({ 'data-test-subj': 'mlFieldTypeIcon' }); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); @@ -56,11 +48,4 @@ describe('FieldTypeIcon', () => { // Clearing all mocks will also reset fake timers. jest.clearAllMocks(); }); - - test(`update component`, () => { - const typeIconComponent = shallow(); - expect(typeIconComponent.isEmptyRender()).toBeTruthy(); - typeIconComponent.setProps({ type: ML_JOB_FIELD_TYPES.IP }); - expect(typeIconComponent).toMatchSnapshot(); - }); }); diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx similarity index 54% rename from x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js rename to x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx index 1853c3d629c3e3..7f736b65d494c5 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx @@ -4,63 +4,80 @@ * you may not use this file except in compliance with the Elastic License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { FC } from 'react'; -import { EuiToolTip } from '@elastic/eui'; +import { EuiToken, EuiToolTip } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; -import { i18n } from '@kbn/i18n'; -export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true }) => { - const ariaLabel = getMLJobTypeAriaLabel(type); +interface FieldTypeIconProps { + tooltipEnabled: boolean; + type: ML_JOB_FIELD_TYPES; + fieldName?: string; + needsAria: boolean; +} - if (ariaLabel === null) { - // All ml job field types should have associated aria labels. - // Once it is missing, it means that the passed *type* is not a valid field type. - // if type doesn't match one of ML_JOB_FIELD_TYPES - // don't render the component at all - return null; - } +interface FieldTypeIconContainerProps { + ariaLabel: string | null; + iconType: string; + color: string; + needsAria: boolean; + [key: string]: any; +} - const iconClass = ['field-type-icon']; - let iconChar = ''; +export const FieldTypeIcon: FC = ({ + tooltipEnabled = false, + type, + fieldName, + needsAria = true, +}) => { + const ariaLabel = getMLJobTypeAriaLabel(type); + + let iconType = 'questionInCircle'; + let color = 'euiColorVis6'; switch (type) { - // icon class names + // Set icon types and colors case ML_JOB_FIELD_TYPES.BOOLEAN: - iconClass.push('kuiIcon', 'fa-adjust'); + iconType = 'tokenBoolean'; + color = 'euiColorVis5'; break; case ML_JOB_FIELD_TYPES.DATE: - iconClass.push('kuiIcon', 'fa-clock-o'); + iconType = 'tokenDate'; + color = 'euiColorVis7'; break; case ML_JOB_FIELD_TYPES.GEO_POINT: - iconClass.push('kuiIcon', 'fa-globe'); + iconType = 'tokenGeo'; + color = 'euiColorVis8'; break; case ML_JOB_FIELD_TYPES.TEXT: - iconClass.push('kuiIcon', 'fa-file-text-o'); + iconType = 'document'; + color = 'euiColorVis9'; break; case ML_JOB_FIELD_TYPES.IP: - iconClass.push('kuiIcon', 'fa-laptop'); + iconType = 'tokenIP'; + color = 'euiColorVis3'; break; - - // icon chars case ML_JOB_FIELD_TYPES.KEYWORD: - iconChar = 't'; + iconType = 'tokenText'; + color = 'euiColorVis0'; break; case ML_JOB_FIELD_TYPES.NUMBER: - iconChar = '#'; + iconType = 'tokenNumber'; + color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2'; break; case ML_JOB_FIELD_TYPES.UNKNOWN: - iconChar = '?'; + // Use defaults break; } const containerProps = { ariaLabel, - className: iconClass.join(' '), - iconChar, + iconType, + color, needsAria, }; @@ -84,28 +101,27 @@ export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true } return ; }; -FieldTypeIcon.propTypes = { - tooltipEnabled: PropTypes.bool, - type: PropTypes.string, -}; - // If the tooltip is used, it will apply its events to its first inner child. // To pass on its properties we apply `rest` to the outer `span` element. -function FieldTypeIconContainer({ ariaLabel, className, iconChar, needsAria, ...rest }) { - const wrapperProps = { className }; +const FieldTypeIconContainer: FC = ({ + ariaLabel, + iconType, + color, + needsAria, + ...rest +}) => { + const wrapperProps: { className: string; 'aria-label'?: string } = { + className: 'field-type-icon', + }; if (needsAria && ariaLabel) { wrapperProps['aria-label'] = ariaLabel; } return ( - - {iconChar === '' ? ( - - ) : ( - - - - )} + + + + ); -} +}; diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/index.js b/x-pack/plugins/ml/public/application/components/field_type_icon/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_type_icon/index.js rename to x-pack/plugins/ml/public/application/components/field_type_icon/index.ts diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss index 48aab16d85be6c..f6851fcb8eca4c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss @@ -15,83 +15,47 @@ .boolean { color: $euiColorVis5; border-color: $euiColorVis5; - - .field-type-icon-container { - background-color: rgba($euiColorVis5, 0.2); - } } .date { color: $euiColorVis7; border-color: $euiColorVis7; - - .field-type-icon-container { - background-color: rgba($euiColorVis7, 0.2); - } } .document_count { color: $euiColorVis2; border-color: $euiColorVis2; - - .field-type-icon-container { - background-color: rgba($euiColorVis2, 0.2); - } } .geo_point { color: $euiColorVis8; border-color: $euiColorVis8; - - .field-type-icon-container { - background-color: rgba($euiColorVis8, 0.2); - } } .ip { color: $euiColorVis3; border-color: $euiColorVis3; - - .field-type-icon-container { - background-color: rgba($euiColorVis3, 0.2); - } } .keyword { color: $euiColorVis0; border-color: $euiColorVis0; - - .field-type-icon-container { - background-color: rgba($euiColorVis0, 0.2); - } } .number { color: $euiColorVis1; border-color: $euiColorVis1; - - .field-type-icon-container { - background-color: rgba($euiColorVis1, 0.2); - } } .text { color: $euiColorVis9; border-color: $euiColorVis9; - - .field-type-icon-container { - background-color: rgba($euiColorVis9, 0.2); - } } .type-other, .unknown { color: $euiColorVis6; border-color: $euiColorVis6; - - .field-type-icon-container { - background-color: rgba($euiColorVis6, 0.2); - } } // Use euiPanel styling diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js index e68d73fc6acfaa..2e9efa43f36bc7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js @@ -5,7 +5,15 @@ */ import React from 'react'; -import { EuiSpacer, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiProgress } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiProgress, + EuiSpacer, + EuiText, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldTypeIcon } from '../../../../components/field_type_icon'; @@ -28,7 +36,7 @@ export function FieldStatsCard({ field }) {
- +
{field.name}
@@ -38,29 +46,45 @@ export function FieldStatsCard({ field }) { {field.count > 0 && (
-
-