From d2eb3c8baf60b828a1d33e9a0dc00687802f452f Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Fri, 9 Jul 2021 17:54:25 -0700 Subject: [PATCH 01/16] resloved conflicts for rebasing --- package.json | 5 +- public/common/constants/explorer.ts | 2 +- public/components/explorer/explorer.tsx | 17 +++- .../explorer/visualizations/index.tsx | 94 +++++++++++++++++++ .../components/visualizations/plotly/plot.tsx | 67 +++++++++++++ .../visualizations/plotly/plot_template.tsx | 67 +++++++++++++ .../visualization/countDistribution.tsx | 31 ++++++ .../visualizations/visualization/index.ts | 1 + .../visualization/plotly/plot.tsx | 67 +++++++++++++ .../visualization/visualization.tsx | 24 +++++ public/plugin.ts | 2 +- yarn.lock | 40 +++++++- 12 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 public/components/explorer/visualizations/index.tsx create mode 100644 public/components/visualizations/plotly/plot.tsx create mode 100644 public/components/visualizations/plotly/plot_template.tsx create mode 100644 public/components/visualizations/visualization/countDistribution.tsx create mode 100644 public/components/visualizations/visualization/index.ts create mode 100644 public/components/visualizations/visualization/plotly/plot.tsx create mode 100644 public/components/visualizations/visualization/visualization.tsx diff --git a/package.json b/package.json index 92c5f644c..064dd66c1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "node": "10.23.1", "yarn": "^1.22.10" }, - "dependencies": {}, + "dependencies": { + "plotly.js-dist": "^2.2.0", + "react-plotly.js": "^2.5.1" + }, "devDependencies": { "cypress": "5.0.0" } diff --git a/public/common/constants/explorer.ts b/public/common/constants/explorer.ts index a776d8e62..b863a1055 100644 --- a/public/common/constants/explorer.ts +++ b/public/common/constants/explorer.ts @@ -14,7 +14,7 @@ export const SELECTED_FIELDS = 'selectedFields'; export const UNSELECTED_FIELDS = 'unselectedFields'; export const TAB_ID_TXT_PFX = 'query-panel-'; export const TAB_TITLE = 'New query'; -export const TAB_CHART_TITLE = 'Charts'; +export const TAB_CHART_TITLE = 'Visualizations'; export const TAB_EVENT_TITLE = 'Events'; export const TAB_EVENT_ID_TXT_PFX = 'main-content-events-'; export const TAB_CHART_ID_TXT_PFX = 'main-content-charts-'; \ No newline at end of file diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 259ce1ee0..53cbb700d 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -22,9 +22,12 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { Search } from '../common/seach/search'; +import { CountDistribution } from '../visualizations/visualization/countDistribution'; import { DataGrid } from './dataGrid'; import { Sidebar } from './sidebar'; import { NoResults } from './noResults'; +import { HitsCounter } from './hits_counter/hits_counter'; +import { ExplorerVisualizations } from './visualizations'; import { IField, IExplorerProps, @@ -110,6 +113,12 @@ export const Explorer = (props: IExplorerProps) => { { (props.explorerData && !_.isEmpty(props.explorerData)) ? (
+ {} } + /> +
{ }; }; + const getExplorerVis = () => { + return ( + + ); + }; + const getMainContentTabs = () => { return [ getMainContentTab( @@ -183,7 +198,7 @@ export const Explorer = (props: IExplorerProps) => { { tabId: TAB_CHART_ID, tabTitle: TAB_CHART_TITLE, - getContent: () => { return <>Charts Content } + getContent: () => getExplorerVis() } ) ]; diff --git a/public/components/explorer/visualizations/index.tsx b/public/components/explorer/visualizations/index.tsx new file mode 100644 index 000000000..8024bd7c8 --- /dev/null +++ b/public/components/explorer/visualizations/index.tsx @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiResizableContainer, + EuiListGroup, + EuiPage, + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer +} from '@elastic/eui'; + +export const ExplorerVisualizations = (props: any) => { + return ( + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + test + + + + + + + +

test label

+
+ + test text +
+
+ + + + + test elements + + + )} +
+
+ // + // + // + // fields sidebar + // + // + // + // + // visualization content + // + // + // + // + // edit panel + // + // + // + ); +}; \ No newline at end of file diff --git a/public/components/visualizations/plotly/plot.tsx b/public/components/visualizations/plotly/plot.tsx new file mode 100644 index 000000000..545a03772 --- /dev/null +++ b/public/components/visualizations/plotly/plot.tsx @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import plotComponentFactory from 'react-plotly.js/factory'; +import Plotly from 'plotly.js-dist'; + +interface PltProps { + data: Plotly.Data[]; + layout?: Partial; + onHoverHandler?: (event: Readonly) => void; + onUnhoverHandler?: (event: Readonly) => void; + onClickHandler?: (event: Readonly) => void; + height?: string; +} + +export function Plt(props: PltProps) { + const PlotComponent = plotComponentFactory(Plotly); + + return ( + + ); +} diff --git a/public/components/visualizations/plotly/plot_template.tsx b/public/components/visualizations/plotly/plot_template.tsx new file mode 100644 index 000000000..545a03772 --- /dev/null +++ b/public/components/visualizations/plotly/plot_template.tsx @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import plotComponentFactory from 'react-plotly.js/factory'; +import Plotly from 'plotly.js-dist'; + +interface PltProps { + data: Plotly.Data[]; + layout?: Partial; + onHoverHandler?: (event: Readonly) => void; + onUnhoverHandler?: (event: Readonly) => void; + onClickHandler?: (event: Readonly) => void; + height?: string; +} + +export function Plt(props: PltProps) { + const PlotComponent = plotComponentFactory(Plotly); + + return ( + + ); +} diff --git a/public/components/visualizations/visualization/countDistribution.tsx b/public/components/visualizations/visualization/countDistribution.tsx new file mode 100644 index 000000000..6be960c6b --- /dev/null +++ b/public/components/visualizations/visualization/countDistribution.tsx @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import { Plt } from '../plotly/plot_template'; + +export const CountDistribution = (props: any) => { + return ( + + ); +}; \ No newline at end of file diff --git a/public/components/visualizations/visualization/index.ts b/public/components/visualizations/visualization/index.ts new file mode 100644 index 000000000..2500a2228 --- /dev/null +++ b/public/components/visualizations/visualization/index.ts @@ -0,0 +1 @@ +export * from './visualization'; \ No newline at end of file diff --git a/public/components/visualizations/visualization/plotly/plot.tsx b/public/components/visualizations/visualization/plotly/plot.tsx new file mode 100644 index 000000000..545a03772 --- /dev/null +++ b/public/components/visualizations/visualization/plotly/plot.tsx @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import plotComponentFactory from 'react-plotly.js/factory'; +import Plotly from 'plotly.js-dist'; + +interface PltProps { + data: Plotly.Data[]; + layout?: Partial; + onHoverHandler?: (event: Readonly) => void; + onUnhoverHandler?: (event: Readonly) => void; + onClickHandler?: (event: Readonly) => void; + height?: string; +} + +export function Plt(props: PltProps) { + const PlotComponent = plotComponentFactory(Plotly); + + return ( + + ); +} diff --git a/public/components/visualizations/visualization/visualization.tsx b/public/components/visualizations/visualization/visualization.tsx new file mode 100644 index 000000000..86d2ed98b --- /dev/null +++ b/public/components/visualizations/visualization/visualization.tsx @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; + +export const Visualization = ( + { + Chart, + ...visConfig + }: { + Chart: React.ReactDOM, + visConfig: any + } +) => { + return ; +}; \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index bf749de3d..f1854327a 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -9,7 +9,7 @@ * GitHub history for details. */ -import { +import { Plugin, CoreSetup, CoreStart, diff --git a/yarn.lock b/yarn.lock index ce60fab0f..0393478eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -785,6 +785,11 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -907,6 +912,13 @@ log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -990,7 +1002,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -1056,6 +1068,11 @@ pify@^2.2.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= +plotly.js-dist@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-2.2.0.tgz#a42c9310f5dd29d5cd437457925407fb952e6fba" + integrity sha512-8frGW2md+erSEZdOHiHc7xyOORK3Oq01nsJk546kDdjyCHLGqan4pqNdoHlwlPemlcdqBnrsahYY6z2rLU6srw== + pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -1066,6 +1083,15 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -1104,6 +1130,18 @@ ramda@~0.26.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-plotly.js@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/react-plotly.js/-/react-plotly.js-2.5.1.tgz#11182bf599ef11a0dbfcd171c6f5645535a2b486" + integrity sha512-Oya14whSHvPsYXdI0nHOGs1pZhMzV2edV7HAW1xFHD58Y73m/LbG2Encvyz1tztL0vfjph0JNhiwO8cGBJnlhg== + dependencies: + prop-types "^15.7.2" + readable-stream@^2.2.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" From 6a4f97ad2e143c6c71c06ad5ebe92f8581e4a582 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Wed, 14 Jul 2021 14:56:06 -0700 Subject: [PATCH 02/16] added overall layout and render fields --- .../debounced_component.test.tsx | 32 ++ .../debounced_component.tsx | 28 + .../common/debounced_component/index.ts | 7 + public/components/explorer/explorer.tsx | 4 +- .../explorer/visualizations/_mixins.scss | 49 ++ .../explorer/visualizations/_variables.scss | 5 + .../explorer/visualizations/datapanel.scss | 36 ++ .../explorer/visualizations/datapanel.tsx | 154 +++++ .../__snapshots__/drag_drop.test.tsx.snap | 34 ++ .../visualizations/drag_drop/drag_drop.scss | 54 ++ .../drag_drop/drag_drop.test.tsx | 197 +++++++ .../visualizations/drag_drop/drag_drop.tsx | 250 ++++++++ .../visualizations/drag_drop/index.ts | 8 + .../drag_drop/providers.test.tsx | 40 ++ .../visualizations/drag_drop/providers.tsx | 86 +++ .../visualizations/drag_drop/readme.md | 69 +++ .../explorer/visualizations/fieldList.tsx | 45 ++ .../explorer/visualizations/field_item.scss | 48 ++ .../explorer/visualizations/field_item.tsx | 541 ++++++++++++++++++ .../explorer/visualizations/field_list.scss | 20 + .../explorer/visualizations/field_list.tsx | 195 +++++++ .../visualizations/fields_accordion.tsx | 121 ++++ .../explorer/visualizations/frameLayout.scss | 60 ++ .../explorer/visualizations/frameLayout.tsx | 42 ++ .../explorer/visualizations/index.tsx | 91 +-- .../visualizations/lens_field_icon.test.tsx | 24 + .../visualizations/lens_field_icon.tsx | 25 + .../lens_ui_telemetry/factory.test.ts | 109 ++++ .../lens_ui_telemetry/factory.ts | 122 ++++ .../visualizations/lens_ui_telemetry/index.ts | 7 + .../visualizations/workspacePanel.tsx | 20 + 31 files changed, 2445 insertions(+), 78 deletions(-) create mode 100644 public/components/common/debounced_component/debounced_component.test.tsx create mode 100644 public/components/common/debounced_component/debounced_component.tsx create mode 100644 public/components/common/debounced_component/index.ts create mode 100644 public/components/explorer/visualizations/_mixins.scss create mode 100644 public/components/explorer/visualizations/_variables.scss create mode 100644 public/components/explorer/visualizations/datapanel.scss create mode 100644 public/components/explorer/visualizations/datapanel.tsx create mode 100644 public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap create mode 100644 public/components/explorer/visualizations/drag_drop/drag_drop.scss create mode 100644 public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx create mode 100644 public/components/explorer/visualizations/drag_drop/drag_drop.tsx create mode 100644 public/components/explorer/visualizations/drag_drop/index.ts create mode 100644 public/components/explorer/visualizations/drag_drop/providers.test.tsx create mode 100644 public/components/explorer/visualizations/drag_drop/providers.tsx create mode 100644 public/components/explorer/visualizations/drag_drop/readme.md create mode 100644 public/components/explorer/visualizations/fieldList.tsx create mode 100644 public/components/explorer/visualizations/field_item.scss create mode 100644 public/components/explorer/visualizations/field_item.tsx create mode 100644 public/components/explorer/visualizations/field_list.scss create mode 100644 public/components/explorer/visualizations/field_list.tsx create mode 100644 public/components/explorer/visualizations/fields_accordion.tsx create mode 100644 public/components/explorer/visualizations/frameLayout.scss create mode 100644 public/components/explorer/visualizations/frameLayout.tsx create mode 100644 public/components/explorer/visualizations/lens_field_icon.test.tsx create mode 100644 public/components/explorer/visualizations/lens_field_icon.tsx create mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts create mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/factory.ts create mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/index.ts create mode 100644 public/components/explorer/visualizations/workspacePanel.tsx diff --git a/public/components/common/debounced_component/debounced_component.test.tsx b/public/components/common/debounced_component/debounced_component.test.tsx new file mode 100644 index 000000000..929dd8e43 --- /dev/null +++ b/public/components/common/debounced_component/debounced_component.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { debouncedComponent } from './debounced_component'; +import { act } from 'react-dom/test-utils'; + +describe('debouncedComponent', () => { + test('immediately renders', () => { + const TestComponent = debouncedComponent(({ title }: { title: string }) => { + return

{title}

; + }); + expect(mount().html()).toMatchInlineSnapshot(`"

hoi

"`); + }); + + test('debounces changes', async () => { + const TestComponent = debouncedComponent(({ title }: { title: string }) => { + return

{title}

; + }, 1); + const component = mount(); + component.setProps({ title: 'yall' }); + expect(component.text()).toEqual('there'); + await act(async () => { + await new Promise((r) => setTimeout(r, 1)); + }); + expect(component.text()).toEqual('yall'); + }); +}); diff --git a/public/components/common/debounced_component/debounced_component.tsx b/public/components/common/debounced_component/debounced_component.tsx new file mode 100644 index 000000000..0e148798c --- /dev/null +++ b/public/components/common/debounced_component/debounced_component.tsx @@ -0,0 +1,28 @@ +/* + * 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, { useState, useMemo, useEffect, memo, FunctionComponent } from 'react'; +import { debounce } from 'lodash'; + +/** + * debouncedComponent wraps the specified React component, returning a component which + * only renders once there is a pause in props changes for at least `delay` milliseconds. + * During the debounce phase, it will return the previously rendered value. + */ +export function debouncedComponent(component: FunctionComponent, delay = 256) { + const MemoizedComponent = (memo(component) as unknown) as FunctionComponent; + + return (props: TProps) => { + const [cachedProps, setCachedProps] = useState(props); + const debouncePropsChange = useMemo(() => debounce(setCachedProps, delay), [setCachedProps]); + + // cancel debounced prop change if component has been unmounted in the meantime + useEffect(() => () => debouncePropsChange.cancel(), [debouncePropsChange]); + debouncePropsChange(props); + + return React.createElement(MemoizedComponent, cachedProps); + }; +} diff --git a/public/components/common/debounced_component/index.ts b/public/components/common/debounced_component/index.ts new file mode 100644 index 000000000..ed940fed5 --- /dev/null +++ b/public/components/common/debounced_component/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './debounced_component'; diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 53cbb700d..7c85ce12c 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -181,7 +181,9 @@ export const Explorer = (props: IExplorerProps) => { const getExplorerVis = () => { return ( - + ); }; diff --git a/public/components/explorer/visualizations/_mixins.scss b/public/components/explorer/visualizations/_mixins.scss new file mode 100644 index 000000000..0db72d118 --- /dev/null +++ b/public/components/explorer/visualizations/_mixins.scss @@ -0,0 +1,49 @@ +// sass-lint:disable-block indentation, no-color-keywords + +// SASSTODO: Create this in EUI +@mixin lnsOverflowShadowHorizontal { + $hideHeight: $euiScrollBarCorner * 1.25; + mask-image: linear-gradient( + to right, + transparentize(red, .9) 0%, + transparentize(red, 0) $hideHeight, + transparentize(red, 0) calc(100% - #{$hideHeight}), + transparentize(red, .9) 100% + ); +} + +// Static styles for a draggable item +@mixin lnsDraggable { + @include euiSlightShadow; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border: $euiBorderWidthThin dashed transparent; + cursor: grab; +} + +// Static styles for a drop area +@mixin lnsDroppable { + border: $euiBorderWidthThin dashed $euiBorderColor; +} + +// Hovering state for drag item and drop area +@mixin lnsDragDropHover { + &:hover { + border: $euiBorderWidthThin dashed $euiColorMediumShade; + } +} + +// Style for drop area when there's an item being dragged +@mixin lnsDroppableActive { + background-color: transparentize($euiColorVis0, .9); +} + +// Style for drop area while hovering with item +@mixin lnsDroppableActiveHover { + background-color: transparentize($euiColorVis0, .75); + border: $euiBorderWidthThin dashed $euiColorVis0; +} + +// Style for drop area that is not allowed for current item +@mixin lnsDroppableNotAllowed { + opacity: .5; +} diff --git a/public/components/explorer/visualizations/_variables.scss b/public/components/explorer/visualizations/_variables.scss new file mode 100644 index 000000000..5a4869bb8 --- /dev/null +++ b/public/components/explorer/visualizations/_variables.scss @@ -0,0 +1,5 @@ +$lnsPanelMinWidth: $euiSize * 18; + +// These sizes also match canvas' page thumbnails for consistency +$lnsSuggestionHeight: 100px; +$lnsSuggestionWidth: 150px; diff --git a/public/components/explorer/visualizations/datapanel.scss b/public/components/explorer/visualizations/datapanel.scss new file mode 100644 index 000000000..df73789ea --- /dev/null +++ b/public/components/explorer/visualizations/datapanel.scss @@ -0,0 +1,36 @@ +.lnsInnerIndexPatternDataPanel { + width: 100%; + height: 100%; + padding: $euiSize $euiSize 0; +} + +.lnsInnerIndexPatternDataPanel__header { + display: flex; + align-items: center; + margin-bottom: $euiSizeS; +} + +.lnsInnerIndexPatternDataPanel__fieldItems { + // Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds + padding: $euiSizeXS; +} + +.lnsInnerIndexPatternDataPanel__textField { + @include euiFormControlLayoutPadding(1, 'right'); + @include euiFormControlLayoutPadding(1, 'left'); +} + +.lnsInnerIndexPatternDataPanel__filterType { + font-size: $euiFontSizeS; + padding: $euiSizeS; + border-bottom: 1px solid $euiColorLightestShade; +} + +.lnsInnerIndexPatternDataPanel__filterTypeInner { + display: flex; + align-items: center; + + .lnsFieldListPanel__fieldIcon { + margin-right: $euiSizeS; + } +} diff --git a/public/components/explorer/visualizations/datapanel.tsx b/public/components/explorer/visualizations/datapanel.tsx new file mode 100644 index 000000000..a29984d00 --- /dev/null +++ b/public/components/explorer/visualizations/datapanel.tsx @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import './datapanel.scss'; +import './field_item.scss'; +import React from 'react'; +import _ from 'lodash'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormControlLayout, + EuiSpacer, + EuiAccordion, + EuiFilterGroup +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FieldList } from './fieldList'; + +export const DataPanel = (props: any) => { + + const { schema } = props.queryResults; + + // const fieldsGroup = schema + + return ( + + + {/*
+ index picker +
*/} +
+ + { + // trackUiEvent('indexpattern_filters_cleared'); + // clearLocalState(); + }, + }} + > + { + // setLocalState({ ...localState, nameFilter: e.target.value }); + }} + aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { + defaultMessage: 'Search fields', + })} + /> + + + + + + {/* setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))} + button={ + { + setLocalState((s) => ({ + ...s, + isTypeFilterOpen: !localState.isTypeFilterOpen, + })); + }} + > + {fieldFiltersLabel} + + } + > + ( + { + trackUiEvent('indexpattern_type_filter_toggled'); + setLocalState((s) => ({ + ...s, + typeFilter: localState.typeFilter.includes(type) + ? localState.typeFilter.filter((t) => t !== type) + : [...localState.typeFilter, type], + })); + }} + > + + {fieldTypeNames[type]} + + + ))} + /> + */} + + + + + {/* +
+ {schema && schema.map((item) => item.name)} +
+
*/} + +
+
+ ); +} \ No newline at end of file diff --git a/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap new file mode 100644 index 000000000..dc53f3a2b --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DragDrop droppable is reflected in the className 1`] = ` + +`; + +exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = ` + +`; + +exports[`DragDrop renders if nothing is being dragged 1`] = ` + +`; diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.scss b/public/components/explorer/visualizations/drag_drop/drag_drop.scss new file mode 100644 index 000000000..410aaef9a --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.scss @@ -0,0 +1,54 @@ +@import '../variables'; +@import '../mixins'; + +.lnsDragDrop { + transition: background-color $euiAnimSpeedFast ease-in-out, border-color $euiAnimSpeedFast ease-in-out; +} + +// Draggable item +.lnsDragDrop-isDraggable { + @include lnsDraggable; + @include lnsDragDropHover; + + // Include a possible nested button like when using FieldButton + > .kbnFieldButton__button { + cursor: grab; + } + + &:focus { + @include euiFocusRing; + } +} + +// Draggable item when it is moving +.lnsDragDrop-isHidden { + opacity: 0; +} + +// Drop area +.lnsDragDrop-isDroppable { + @include lnsDroppable; +} + +// Drop area when there's an item being dragged +.lnsDragDrop-isDropTarget { + @include lnsDroppableActive; +} + +// Drop area while hovering with item +.lnsDragDrop-isActiveDropTarget { + @include lnsDroppableActiveHover; +} + +// Drop area that is not allowed for current item +.lnsDragDrop-isNotDroppable { + @include lnsDroppableNotAllowed; +} + +// Drop area will be replacing existing content +.lnsDragDrop-isReplacing { + &, + .lnsLayerPanel__triggerLink { + text-decoration: line-through; + } +} diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx b/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx new file mode 100644 index 000000000..b1cc4c06c --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx @@ -0,0 +1,197 @@ +/* + * 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 { render, mount } from 'enzyme'; +import { DragDrop } from './drag_drop'; +import { ChildDragDropProvider } from './providers'; + +jest.useFakeTimers(); + +describe('DragDrop', () => { + test('renders if nothing is being dragged', () => { + const component = render( + + + + ); + + expect(component).toMatchSnapshot(); + }); + + test('dragover calls preventDefault if droppable is true', () => { + const preventDefault = jest.fn(); + const component = mount( + + + + ); + + component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); + + expect(preventDefault).toBeCalled(); + }); + + test('dragover does not call preventDefault if droppable is false', () => { + const preventDefault = jest.fn(); + const component = mount( + + + + ); + + component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); + + expect(preventDefault).not.toBeCalled(); + }); + + test('dragstart sets dragging in the context', async () => { + const setDragging = jest.fn(); + const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), + }; + const value = {}; + + const component = mount( + + + + + + ); + + component.find('[data-test-subj="lnsDragDrop"]').simulate('dragstart', { dataTransfer }); + + jest.runAllTimers(); + + expect(dataTransfer.setData).toBeCalledWith('text', 'drag label'); + expect(setDragging).toBeCalledWith(value); + }); + + test('drop resets all the things', async () => { + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + const setDragging = jest.fn(); + const onDrop = jest.fn(); + const value = {}; + + const component = mount( + + + + + + ); + + component + .find('[data-test-subj="lnsDragDrop"]') + .simulate('drop', { preventDefault, stopPropagation }); + + expect(preventDefault).toBeCalled(); + expect(stopPropagation).toBeCalled(); + expect(setDragging).toBeCalledWith(undefined); + expect(onDrop).toBeCalledWith('hola'); + }); + + test('drop function is not called on droppable=false', async () => { + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + const setDragging = jest.fn(); + const onDrop = jest.fn(); + + const component = mount( + + + + + + ); + + component + .find('[data-test-subj="lnsDragDrop"]') + .simulate('drop', { preventDefault, stopPropagation }); + + expect(preventDefault).toBeCalled(); + expect(stopPropagation).toBeCalled(); + expect(setDragging).toBeCalledWith(undefined); + expect(onDrop).not.toHaveBeenCalled(); + }); + + test('droppable is reflected in the className', () => { + const component = render( + { + throw x; + }} + droppable + > + + + ); + + expect(component).toMatchSnapshot(); + }); + + test('items that have droppable=false get special styling when another item is dragged', () => { + const component = mount( + {}}> + + + + {}} droppable={false}> + + + + ); + + expect(component.find('[data-test-subj="lnsDragDrop"]').at(1)).toMatchSnapshot(); + }); + + test('additional styles are reflected in the className until drop', () => { + let dragging: string | undefined; + const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + const component = mount( + { + dragging = 'hello'; + }} + > + + + + {}} + droppable + getAdditionalClassesOnEnter={getAdditionalClasses} + > + + + + ); + + const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), + }; + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + expect(component.find('.additional')).toHaveLength(1); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + expect(component.find('.additional')).toHaveLength(0); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); + expect(component.find('.additional')).toHaveLength(0); + }); +}); diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.tsx b/public/components/explorer/visualizations/drag_drop/drag_drop.tsx new file mode 100644 index 000000000..69f03a254 --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.tsx @@ -0,0 +1,250 @@ +/* + * 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 './drag_drop.scss'; + +import React, { useState, useContext } from 'react'; +import classNames from 'classnames'; +import { DragContext } from './providers'; +// import { trackUiEvent } from '../lens_ui_telemetry'; + +type DroppableEvent = React.DragEvent; + +/** + * A function that handles a drop event. + */ +export type DropHandler = (item: unknown) => void; + +/** + * The base props to the DragDrop component. + */ +interface BaseProps { + /** + * The CSS class(es) for the root element. + */ + className?: string; + + /** + * The event handler that fires when an item + * is dropped onto this DragDrop component. + */ + onDrop?: DropHandler; + + /** + * The value associated with this item, if it is draggable. + * If this component is dragged, this will be the value of + * "dragging" in the root drag/drop context. + */ + value?: unknown; + + /** + * Optional comparison function to check whether a value is the dragged one + */ + isValueEqual?: (value1: unknown, value2: unknown) => boolean; + + /** + * The React element which will be passed the draggable handlers + */ + children: React.ReactElement; + + /** + * Indicates whether or not the currently dragged item + * can be dropped onto this component. + */ + droppable?: boolean; + + /** + * Additional class names to apply when another element is over the drop target + */ + getAdditionalClassesOnEnter?: () => string; + + /** + * The optional test subject associated with this DOM element. + */ + 'data-test-subj'?: string; + + /** + * Indicates to the user whether the currently dragged item + * will be moved or copied + */ + dragType?: 'copy' | 'move'; + + /** + * Indicates to the user whether the drop action will + * replace something that is existing or add a new one + */ + dropType?: 'add' | 'replace'; +} + +/** + * The props for a draggable instance of that component. + */ +interface DraggableProps extends BaseProps { + /** + * Indicates whether or not this component is draggable. + */ + draggable: true; + /** + * The label, which should be attached to the drag event, and which will e.g. + * be used if the element will be dropped into a text field. + */ + label: string; +} + +/** + * The props for a non-draggable instance of that component. + */ +interface NonDraggableProps extends BaseProps { + /** + * Indicates whether or not this component is draggable. + */ + draggable?: false; +} + +type Props = DraggableProps | NonDraggableProps; + +/** + * A draggable / droppable item. Items can be both draggable and droppable at + * the same time. + * + * @param props + */ + +export const DragDrop = (props: Props) => { + const { dragging, setDragging } = useContext(DragContext); + const { value, draggable, droppable, isValueEqual } = props; + return ( + + ); +}; + +const DragDropInner = React.memo(function DragDropInner( + props: Props & { + dragging: unknown; + setDragging: (dragging: unknown) => void; + isDragging: boolean; + isNotDroppable: boolean; + } +) { + const [state, setState] = useState({ + isActive: false, + dragEnterClassNames: '', + }); + const { + className, + onDrop, + value, + children, + droppable, + draggable, + dragging, + setDragging, + isDragging, + isNotDroppable, + dragType = 'copy', + dropType = 'add', + } = props; + + const isMoveDragging = isDragging && dragType === 'move'; + + const classes = classNames( + 'lnsDragDrop', + { + 'lnsDragDrop-isDraggable': draggable, + 'lnsDragDrop-isDragging': isDragging, + 'lnsDragDrop-isHidden': isMoveDragging, + 'lnsDragDrop-isDroppable': !draggable, + 'lnsDragDrop-isDropTarget': droppable, + 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, + 'lnsDragDrop-isNotDroppable': !isMoveDragging && isNotDroppable, + 'lnsDragDrop-isReplacing': droppable && state.isActive && dropType === 'replace', + }, + className, + state.dragEnterClassNames + ); + + const dragStart = (e: DroppableEvent) => { + // Setting stopPropgagation causes Chrome failures, so + // we are manually checking if we've already handled this + // in a nested child, and doing nothing if so... + if (e.dataTransfer.getData('text')) { + return; + } + + // We only can reach the dragStart method if the element is draggable, + // so we know we have DraggableProps if we reach this code. + e.dataTransfer.setData('text', (props as DraggableProps).label); + + // Chrome causes issues if you try to render from within a + // dragStart event, so we drop a setTimeout to avoid that. + setTimeout(() => setDragging(value)); + }; + + const dragEnd = (e: DroppableEvent) => { + e.stopPropagation(); + setDragging(undefined); + }; + + const dragOver = (e: DroppableEvent) => { + if (!droppable) { + return; + } + + e.preventDefault(); + + // An optimization to prevent a bunch of React churn. + if (!state.isActive) { + setState({ + ...state, + isActive: true, + dragEnterClassNames: props.getAdditionalClassesOnEnter + ? props.getAdditionalClassesOnEnter() + : '', + }); + } + }; + + const dragLeave = () => { + setState({ ...state, isActive: false, dragEnterClassNames: '' }); + }; + + const drop = (e: DroppableEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setState({ ...state, isActive: false, dragEnterClassNames: '' }); + setDragging(undefined); + + if (onDrop && droppable) { + // trackUiEvent('drop_total'); + onDrop(dragging); + } + }; + + return React.cloneElement(children, { + 'data-test-subj': props['data-test-subj'] || 'lnsDragDrop', + className: classNames(children.props.className, classes), + onDragOver: dragOver, + onDragLeave: dragLeave, + onDrop: drop, + draggable, + onDragEnd: dragEnd, + onDragStart: dragStart, + }); +}); diff --git a/public/components/explorer/visualizations/drag_drop/index.ts b/public/components/explorer/visualizations/drag_drop/index.ts new file mode 100644 index 000000000..e597bb8b6 --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './providers'; +export * from './drag_drop'; diff --git a/public/components/explorer/visualizations/drag_drop/providers.test.tsx b/public/components/explorer/visualizations/drag_drop/providers.test.tsx new file mode 100644 index 000000000..2a8735be4 --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/providers.test.tsx @@ -0,0 +1,40 @@ +/* + * 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, { useContext } from 'react'; +import { mount } from 'enzyme'; +import { RootDragDropProvider, DragContext } from './providers'; + +jest.useFakeTimers(); + +describe('RootDragDropProvider', () => { + test('reuses contexts for each render', () => { + const contexts: Array<{}> = []; + const TestComponent = ({ name }: { name: string }) => { + const context = useContext(DragContext); + contexts.push(context); + return ( +
+ {name} {!!context.dragging} +
+ ); + }; + + const RootComponent = ({ name }: { name: string }) => ( + + + + ); + + const component = mount(); + + component.setProps({ name: 'bbbb' }); + + expect(component.find('[data-test-subj="test-component"]').text()).toContain('bbb'); + expect(contexts.length).toEqual(2); + expect(contexts[0]).toStrictEqual(contexts[1]); + }); +}); diff --git a/public/components/explorer/visualizations/drag_drop/providers.tsx b/public/components/explorer/visualizations/drag_drop/providers.tsx new file mode 100644 index 000000000..3e2b73122 --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/providers.tsx @@ -0,0 +1,86 @@ +/* + * 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, { useState, useMemo } from 'react'; + +/** + * The shape of the drag / drop context. + */ +export interface DragContextState { + /** + * The item being dragged or undefined. + */ + dragging: unknown; + + /** + * Set the item being dragged. + */ + setDragging: (dragging: unknown) => void; +} + +/** + * The drag / drop context singleton, used like so: + * + * const { dragging, setDragging } = useContext(DragContext); + */ +export const DragContext = React.createContext({ + dragging: undefined, + setDragging: () => {}, +}); + +/** + * The argument to DragDropProvider. + */ +export interface ProviderProps { + /** + * The item being dragged. If unspecified, the provider will + * behave as if it is the root provider. + */ + dragging: unknown; + + /** + * Sets the item being dragged. If unspecified, the provider + * will behave as if it is the root provider. + */ + setDragging: (dragging: unknown) => void; + + /** + * The React children. + */ + children: React.ReactNode; +} + +/** + * A React provider that tracks the dragging state. This should + * be placed at the root of any React application that supports + * drag / drop. + * + * @param props + */ +export function RootDragDropProvider({ children }: { children: React.ReactNode }) { + const [state, setState] = useState<{ dragging: unknown }>({ + dragging: undefined, + }); + const setDragging = useMemo(() => (dragging: unknown) => setState({ dragging }), [setState]); + + return ( + + {children} + + ); +} + +/** + * A React drag / drop provider that derives its state from a RootDragDropProvider. If + * part of a React application is rendered separately from the root, this provider can + * be used to enable drag / drop functionality within the disconnected part. + * + * @param props + */ +export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) { + const value = useMemo(() => ({ dragging, setDragging }), [setDragging, dragging]); + return {children}; +} diff --git a/public/components/explorer/visualizations/drag_drop/readme.md b/public/components/explorer/visualizations/drag_drop/readme.md new file mode 100644 index 000000000..8d11cb622 --- /dev/null +++ b/public/components/explorer/visualizations/drag_drop/readme.md @@ -0,0 +1,69 @@ +# Drag / Drop + +This is a simple drag / drop mechanism that plays nice with React. + +We aren't using EUI or another library, due to the fact that Lens visualizations and datasources may or may not be written in React. Even visualizations which are written in React will end up having their own ReactDOM.render call, and in that sense will be a standalone React application. We want to enable drag / drop across React and native DOM boundaries. + +## Getting started + +First, place a RootDragDropProvider at the root of your application. + +```js + + ... your app here ... + +``` + +If you have a child React application (e.g. a visualization), you will need to pass the drag / drop context down into it. This can be obtained like so: + +```js +const context = useContext(DragContext); +``` + +In your child application, place a `ChildDragDropProvider` at the root of that, and spread the context into it: + +```js + + ... your child app here ... + +``` + +This enables your child application to share the same drag / drop context as the root application. + +## Dragging + +An item can be both draggable and droppable at the same time, but for simplicity's sake, we'll treat these two cases separately. + +To enable dragging an item, use `DragDrop` with both a `draggable` and a `value` attribute. + +```js +
+ {fields.map(f => ( + + {f.name} + + ))} +
+``` + +## Dropping + +To enable dropping, use `DragDrop` with both a `droppable` attribute and an `onDrop` handler attribute. Droppable should only be set to true if there is an item being dragged, and if a drop of the dragged item is supported. + +```js +const { dragging } = useContext(DragContext); + +return ( + onChange([...items, item])} + > + {items.map(x =>
{x.name}
)} +
+); +``` + +## Limitations + +Currently this is a very simple drag / drop mechanism. We don't support reordering out of the box, though it could probably be built on top of this solution without modification of the core. diff --git a/public/components/explorer/visualizations/fieldList.tsx b/public/components/explorer/visualizations/fieldList.tsx new file mode 100644 index 000000000..960f3f074 --- /dev/null +++ b/public/components/explorer/visualizations/fieldList.tsx @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +// import './field_list.scss'; +import { throttle } from 'lodash'; +import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { FieldItem } from './field_item'; +import { FieldsAccordion } from './fields_accordion'; + +export const FieldList = ( + { + schema, + id + } +) => { + + return ( +
+
+ { + <> + + + } +
+
+ ); +}; \ No newline at end of file diff --git a/public/components/explorer/visualizations/field_item.scss b/public/components/explorer/visualizations/field_item.scss new file mode 100644 index 000000000..1b55d9623 --- /dev/null +++ b/public/components/explorer/visualizations/field_item.scss @@ -0,0 +1,48 @@ +.lnsFieldItem { + .lnsFieldItem__infoIcon { + visibility: hidden; + opacity: 0; + } + + &:hover:not([class*='isActive']) { + cursor: grab; + + .lnsFieldItem__infoIcon { + visibility: visible; + opacity: 1; + transition: opacity $euiAnimSpeedFast ease-in-out 1s; + } + } +} + +.lnsFieldItem--missing { + background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); + color: $euiColorDarkShade; +} + +.lnsFieldItem__topValue { + margin-bottom: $euiSizeS; + + &:last-of-type { + margin-bottom: 0; + } +} + +.lnsFieldItem__topValueProgress { + background-color: $euiColorLightestShade; + + // sass-lint:disable-block no-vendor-prefixes + &::-webkit-progress-bar { + background-color: $euiColorLightestShade; + } +} + +.lnsFieldItem__fieldPanel { + min-width: 260px; + max-width: 300px; +} + +.lnsFieldItem__buttonGroup { + // Enforce lowercase for buttons or else some browsers inherit all caps from flyout title + text-transform: none; +} diff --git a/public/components/explorer/visualizations/field_item.tsx b/public/components/explorer/visualizations/field_item.tsx new file mode 100644 index 000000000..dc59b86bf --- /dev/null +++ b/public/components/explorer/visualizations/field_item.tsx @@ -0,0 +1,541 @@ +/* + * 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 './field_item.scss'; + +import React, { useState } from 'react'; +// import DateMath from '@elastic/datemath'; +import { + // EuiButtonGroup, + // EuiFlexGroup, + // EuiFlexItem, + EuiIconTip, + // EuiLoadingSpinner, + EuiPopover, + // EuiPopoverFooter, + // EuiPopoverTitle, + // EuiProgress, + // EuiText, + // EuiToolTip, +} from '@elastic/eui'; +// import { +// Axis, +// BarSeries, +// Chart, +// niceTimeFormatter, +// Position, +// ScaleType, +// Settings, +// TooltipType, +// } from '@elastic/charts'; +import { i18n } from '@osd/i18n'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { EuiHighlight } from '@elastic/eui'; +// import { +// Query, +// KBN_FIELD_TYPES, +// ES_FIELD_TYPES, +// Filter, +// esQuery, +// IIndexPattern, +// } from '../../../../../src/plugins/data/public'; +import { FieldButton } from '../../common/field_button'; +// import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +// import { DraggedField } from './indexpattern'; +import { DragDrop } from './drag_drop'; +// import { DatasourceDataPanelProps, DataType } from '../types'; +// import { BucketedAggregation, FieldStatsResponse } from '../../common'; +// import { IndexPattern, IndexPatternField } from './types'; +import { LensFieldIcon } from './lens_field_icon'; +// import { trackUiEvent } from '../lens_ui_telemetry'; + +import { debouncedComponent } from '../../common/debounced_component'; + +export interface FieldItemProps { + // core: DatasourceDataPanelProps['core']; + data: DataPublicPluginStart; + // field: IndexPatternField; + // indexPattern: IndexPattern; + highlight?: string; + exists: boolean; + // query: Query; + // dateRange: DatasourceDataPanelProps['dateRange']; + // chartsThemeService: ChartsPluginSetup['theme']; + // filters: Filter[]; + hideDetails?: boolean; +} + +interface State { + isLoading: boolean; + totalDocuments?: number; + sampledDocuments?: number; + sampledValues?: number; + // histogram?: BucketedAggregation; + // topValues?: BucketedAggregation; +} + +function wrapOnDot(str?: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; +} + +export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { + const { + // core, + field, + // indexPattern, + highlight, + exists, + // query, + // dateRange, + // filters, + hideDetails, + } = props; + + const [infoIsOpen, setOpen] = useState(false); + + const [state, setState] = useState({ + isLoading: false, + }); + + function fetchData() { + if (state.isLoading) { + return; + } + + setState((s) => ({ ...s, isLoading: true })); + + // core.http + // .post(`/api/lens/index_stats/${indexPattern.title}/field`, { + // body: JSON.stringify({ + // // dslQuery: esQuery.buildEsQuery( + // // indexPattern as IIndexPattern, + // // query, + // // filters, + // // esQuery.getEsQueryConfig(core.uiSettings) + // // ), + // fromDate: dateRange.fromDate, + // toDate: dateRange.toDate, + // timeFieldName: indexPattern.timeFieldName, + // field, + // }), + // }) + // .then((results: FieldStatsResponse) => { + // setState((s) => ({ + // ...s, + // isLoading: false, + // totalDocuments: results.totalDocuments, + // sampledDocuments: results.sampledDocuments, + // sampledValues: results.sampledValues, + // histogram: results.histogram, + // topValues: results.topValues, + // })); + // }) + // .catch(() => { + // setState((s) => ({ ...s, isLoading: false })); + // }); + } + + function togglePopover() { + if (hideDetails) { + return; + } + + setOpen(!infoIsOpen); + if (!infoIsOpen) { + // trackUiEvent('indexpattern_field_info_click'); + fetchData(); + } + } + + // const value = React.useMemo(() => ({ field, indexPatternId: indexPattern.id } as DraggedField), [ + // field, + // indexPattern.id, + // ]); + // const lensFieldIcon = ; + const lensFieldIcon = ; + const lensInfoIcon = ( + + ); + return ( + ('.application') || undefined} + button={ + + + {/* {wrapOnDot(field.displayName)} */} + {wrapOnDot(field.name)} + + } + fieldInfoIcon={lensInfoIcon} + /> + + } + isOpen={infoIsOpen} + closePopover={() => setOpen(false)} + anchorPosition="rightUp" + panelClassName="lnsFieldItem__fieldPanel" + > + {/* */} + + ); +}; + +export const FieldItem = debouncedComponent(InnerFieldItem); + +// function FieldItemPopoverContents(props: State & FieldItemProps) { +// const { +// histogram, +// topValues, +// indexPattern, +// field, +// dateRange, +// core, +// sampledValues, +// chartsThemeService, +// data: { fieldFormats }, +// } = props; + +// const chartTheme = chartsThemeService.useChartsTheme(); +// const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); +// // let histogramDefault = !!props.histogram; + +// const totalValuesCount = +// topValues && topValues.buckets.reduce((prev, bucket) => bucket.count + prev, 0); +// const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; + +// if ( +// totalValuesCount && +// histogram && +// histogram.buckets.length && +// topValues && +// topValues.buckets.length +// ) { +// // Default to histogram when top values are less than 10% of total +// // histogramDefault = otherCount / totalValuesCount > 0.9; +// } + +// // const [showingHistogram, setShowingHistogram] = useState(histogramDefault); + +// let formatter: { convert: (data: unknown) => string }; +// if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) { +// const FormatType = fieldFormats.getType(indexPattern.fieldFormatMap[field.name].id); +// if (FormatType) { +// formatter = new FormatType( +// indexPattern.fieldFormatMap[field.name].params, +// core.uiSettings.get.bind(core.uiSettings) +// ); +// } else { +// formatter = { convert: (data: unknown) => JSON.stringify(data) }; +// } +// } else { +// // formatter = fieldFormats.getDefaultInstance( +// // field.type as KBN_FIELD_TYPES, +// // field.esTypes as ES_FIELD_TYPES[] +// // ); +// } + +// const fromDate = DateMath.parse(dateRange.fromDate); +// const toDate = DateMath.parse(dateRange.toDate); + +// let title = <>; + +// if (props.isLoading) { +// return ; +// } else if ( +// // (!props.histogram || props.histogram.buckets.length === 0) && +// // (!props.topValues || props.topValues.buckets.length === 0) +// ) { +// return ( +// +// {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { +// defaultMessage: +// 'This field is empty because it doesn’t exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.', +// })} +// +// ); +// } + +// if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { +// title = ( +// { +// // setShowingHistogram(optionId === 'histogram'); +// }} +// // idSelected={showingHistogram ? 'histogram' : 'topValues'} +// /> +// ); +// } else if (field.type === 'date') { +// title = ( +// <> +// {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { +// defaultMessage: 'Time distribution', +// })} +// +// ); +// } else if (topValues && topValues.buckets.length) { +// title = ( +// <> +// {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { +// defaultMessage: 'Top values', +// })} +// +// ); +// } + +// function wrapInPopover(el: React.ReactElement) { +// return ( +// <> +// {title ? {title} : <>} +// {el} + +// {props.totalDocuments ? ( +// +// +// {props.sampledDocuments && ( +// <> +// {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { +// defaultMessage: '{percentage}% of', +// values: { +// percentage: Math.round((props.sampledDocuments / props.totalDocuments) * 100), +// }, +// })} +// +// )}{' '} +// +// {/* {fieldFormats +// .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) +// .convert(props.totalDocuments)} */} +// {' '} +// {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { +// defaultMessage: 'documents', +// })} +// +// +// ) : ( +// <> +// )} +// +// ); +// } + +// if (histogram && histogram.buckets.length) { +// const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { +// defaultMessage: 'Count', +// }); + +// if (field.type === 'date') { +// return wrapInPopover( +// +// + +// + +// +// +// ); +// // } else if (showingHistogram || !topValues || !topValues.buckets.length) { +// } else if (!topValues || !topValues.buckets.length) { +// return wrapInPopover( +// +// + +// formatter.convert(d)} +// /> + +// +// +// ); +// } +// } + +// if (props.topValues && props.topValues.buckets.length) { +// return wrapInPopover( +//
+// {props.topValues.buckets.map((topValue) => { +// const formatted = formatter.convert(topValue.key); +// return ( +//
+// +// +// {formatted === '' ? ( +// +// +// {i18n.translate('xpack.lens.indexPattern.fieldPanelEmptyStringValue', { +// defaultMessage: 'Empty string', +// })} +// +// +// ) : ( +// +// +// {formatted} +// +// +// )} +// +// +// +// {Math.round((topValue.count / props.sampledValues!) * 100)}% +// +// +// + +// +//
+// ); +// })} +// {otherCount ? ( +// <> +// +// +// +// {i18n.translate('xpack.lens.indexPattern.otherDocsLabel', { +// defaultMessage: 'Other', +// })} +// +// + +// +// +// {Math.round((otherCount / props.sampledValues!) * 100)}% +// +// +// + +// +// +// ) : ( +// <> +// )} +//
+// ); +// } +// return <>; +// } diff --git a/public/components/explorer/visualizations/field_list.scss b/public/components/explorer/visualizations/field_list.scss new file mode 100644 index 000000000..f28581b83 --- /dev/null +++ b/public/components/explorer/visualizations/field_list.scss @@ -0,0 +1,20 @@ +/** + * 1. Don't cut off the shadow of the field items + */ + +.lnsIndexPatternFieldList { + @include euiOverflowShadow; + @include euiScrollBar; + margin-left: -$euiSize; /* 1 */ + position: relative; + flex-grow: 1; + overflow: auto; +} + +.lnsIndexPatternFieldList__accordionContainer { + padding-top: $euiSizeS; + position: absolute; + top: 0; + left: $euiSize; /* 1 */ + right: $euiSizeXS; /* 1 */ +} diff --git a/public/components/explorer/visualizations/field_list.tsx b/public/components/explorer/visualizations/field_list.tsx new file mode 100644 index 000000000..eb7730677 --- /dev/null +++ b/public/components/explorer/visualizations/field_list.tsx @@ -0,0 +1,195 @@ +/* + * 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 './field_list.scss'; +import { throttle } from 'lodash'; +import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { FieldItem } from './field_item'; +import { NoFieldsCallout } from './no_fields_callout'; +import { IndexPatternField } from './types'; +import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; +const PAGINATION_SIZE = 50; + +export interface FieldsGroup { + specialFields: IndexPatternField[]; + availableFields: IndexPatternField[]; + emptyFields: IndexPatternField[]; + metaFields: IndexPatternField[]; +} + +export type FieldGroups = Record< + string, + { + fields: IndexPatternField[]; + fieldCount: number; + showInAccordion: boolean; + isInitiallyOpen: boolean; + title: string; + isAffectedByGlobalFilter: boolean; + isAffectedByTimeFilter: boolean; + hideDetails?: boolean; + defaultNoFieldsMessage?: string; + } +>; + +function getDisplayedFieldsLength( + fieldGroups: FieldGroups, + accordionState: Partial> +) { + return Object.entries(fieldGroups) + .filter(([key]) => accordionState[key]) + .reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0); +} + +export function FieldList({ + exists, + fieldGroups, + existenceFetchFailed, + fieldProps, + hasSyncedExistingFields, + filter, + currentIndexPatternId, + existFieldsInIndex, +}: { + exists: (field: IndexPatternField) => boolean; + fieldGroups: FieldGroups; + fieldProps: FieldItemSharedProps; + hasSyncedExistingFields: boolean; + existenceFetchFailed?: boolean; + filter: { + nameFilter: string; + typeFilter: string[]; + }; + currentIndexPatternId: string; + existFieldsInIndex: boolean; +}) { + const [pageSize, setPageSize] = useState(PAGINATION_SIZE); + const [scrollContainer, setScrollContainer] = useState(undefined); + const [accordionState, setAccordionState] = useState>>(() => + Object.fromEntries( + Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) + ) + ); + + useEffect(() => { + // Reset the scroll if we have made material changes to the field list + if (scrollContainer) { + scrollContainer.scrollTop = 0; + setPageSize(PAGINATION_SIZE); + } + }, [filter.nameFilter, filter.typeFilter, currentIndexPatternId, scrollContainer]); + + const lazyScroll = useCallback(() => { + if (scrollContainer) { + const nearBottom = + scrollContainer.scrollTop + scrollContainer.clientHeight > + scrollContainer.scrollHeight * 0.9; + if (nearBottom) { + setPageSize( + Math.max( + PAGINATION_SIZE, + Math.min( + pageSize + PAGINATION_SIZE * 0.5, + getDisplayedFieldsLength(fieldGroups, accordionState) + ) + ) + ); + } + } + }, [scrollContainer, pageSize, setPageSize, fieldGroups, accordionState]); + + const paginatedFields = useMemo(() => { + let remainingItems = pageSize; + return Object.fromEntries( + Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, fieldGroup]) => { + if (!accordionState[key] || remainingItems <= 0) { + return [key, []]; + } + const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); + remainingItems = remainingItems - slicedFieldList.length; + return [key, slicedFieldList]; + }) + ); + }, [pageSize, fieldGroups, accordionState]); + + return ( +
{ + if (el && !el.dataset.dynamicScroll) { + el.dataset.dynamicScroll = 'true'; + setScrollContainer(el); + } + }} + onScroll={throttle(lazyScroll, 100)} + > +
+ {Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => !showInAccordion) + .flatMap(([, { fields }]) => + fields.map((field) => ( + + )) + )} + + {Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, fieldGroup]) => ( + + { + setAccordionState((s) => ({ + ...s, + [key]: open, + })); + const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { + ...accordionState, + [key]: open, + }); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + showExistenceFetchError={existenceFetchFailed} + renderCallout={ + + } + /> + + + ))} +
+
+ ); +} diff --git a/public/components/explorer/visualizations/fields_accordion.tsx b/public/components/explorer/visualizations/fields_accordion.tsx new file mode 100644 index 000000000..26fa32e47 --- /dev/null +++ b/public/components/explorer/visualizations/fields_accordion.tsx @@ -0,0 +1,121 @@ +/* + * 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 './datapanel.scss'; +import React, { memo, useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiText, + EuiNotificationBadge, + EuiSpacer, + EuiAccordion, + EuiLoadingSpinner, + EuiIconTip, +} from '@elastic/eui'; +// import { DataPublicPluginStart } from 'src/plugins/data/public'; +// import { IndexPatternField } from './types'; +import { FieldItem } from './field_item'; +// import { Query, Filter } from '../../../../../src/plugins/data/public'; +// import { DatasourceDataPanelProps } from '../types'; +// import { IndexPattern } from './types'; +// import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +// import { schema } from 'packages/osd-config-schema/target/types'; + +export interface FieldItemSharedProps { + // core: DatasourceDataPanelProps['core']; + // data: DataPublicPluginStart; + // chartsThemeService: ChartsPluginSetup['theme']; + // indexPattern: IndexPattern; + // highlight?: string; + // query: Query; + // dateRange: DatasourceDataPanelProps['dateRange']; + // filters: Filter[]; +} + +export interface FieldsAccordionProps { + initialIsOpen: boolean; + onToggle: (open: boolean) => void; + id: string; + label: string; + hasLoaded: boolean; + fieldsCount: number; + isFiltered: boolean; + // paginatedFields: IndexPatternField[]; + fieldProps: FieldItemSharedProps; + renderCallout: JSX.Element; + // exists: (field: IndexPatternField) => boolean; + showExistenceFetchError?: boolean; + hideDetails?: boolean; +} + +export const InnerFieldsAccordion = function InnerFieldsAccordion({ + // initialIsOpen, + // onToggle, + id, + label, + // hasLoaded, + // fieldsCount, + isFiltered, + paginatedFields, + // fieldProps, + // renderCallout, + // exists, + // hideDetails, + showExistenceFetchError, +}) { + const renderField = useCallback( + (field) => ( + + ), + [] + ); + + return ( + + {label} + + } + extraAction={ + showExistenceFetchError ? ( + + ) : true ? ( + + {paginatedFields?.length} + + ) : ( + + ) + } + > + +
+ {paginatedFields && paginatedFields.map(renderField)} +
+
+ ); +}; + +export const FieldsAccordion = memo(InnerFieldsAccordion); diff --git a/public/components/explorer/visualizations/frameLayout.scss b/public/components/explorer/visualizations/frameLayout.scss new file mode 100644 index 000000000..a742aa43c --- /dev/null +++ b/public/components/explorer/visualizations/frameLayout.scss @@ -0,0 +1,60 @@ +$lnsPanelMinWidth: $euiSize * 18; + +// These sizes also match canvas' page thumbnails for consistency +$lnsSuggestionHeight: 100px; +$lnsSuggestionWidth: 150px; + +.lnsFrameLayout { + padding: 0; + // position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + flex-direction: column; +} + +.lnsFrameLayout__pageContent { + display: flex; + overflow: hidden; + flex-grow: 1; +} + +.lnsFrameLayout__pageBody { + @include euiScrollBar; + min-width: $lnsPanelMinWidth + $euiSizeXL; + overflow: hidden auto; + // Leave out bottom padding so the suggestions scrollbar stays flush to window edge + // Leave out left padding so the left sidebar's focus states are visible outside of content bounds + // This also means needing to add same amount of margin to page content and suggestion items + padding: $euiSize $euiSize 0; + + &:first-child { + padding-left: $euiSize; + } +} + +.lnsFrameLayout__sidebar { + margin: 0; + flex: 1 0 18%; + min-width: $lnsPanelMinWidth + $euiSize; + display: flex; + flex-direction: column; + position: relative; +} + +.lnsFrameLayout__sidebar--right { + flex-basis: 25%; + background-color: lightOrDarkTheme($euiColorLightestShade, $euiColorInk); + min-width: $lnsPanelMinWidth + $euiSizeXL; + max-width: $euiFormMaxWidth + $euiSizeXXL; + max-height: 100%; + + .lnsConfigPanel { + @include euiScrollBar; + padding: $euiSize 0 $euiSize $euiSize; + overflow-x: hidden; + overflow-y: scroll; + } +} diff --git a/public/components/explorer/visualizations/frameLayout.tsx b/public/components/explorer/visualizations/frameLayout.tsx new file mode 100644 index 000000000..09d4cc6e1 --- /dev/null +++ b/public/components/explorer/visualizations/frameLayout.tsx @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import './frameLayout.scss'; + +import React from 'react'; +import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; + +export interface FrameLayoutProps { + dataPanel: React.ReactNode; + configPanel?: React.ReactNode; + suggestionsPanel?: React.ReactNode; + workspacePanel?: React.ReactNode; +} + +export function FrameLayout(props: FrameLayoutProps) { + return ( + +
+ + {props.dataPanel} + + + {props.workspacePanel} + {/* {props.suggestionsPanel} */} + + + {/* {props.configPanel} */} + right sidebar + +
+
+ ); +} \ No newline at end of file diff --git a/public/components/explorer/visualizations/index.tsx b/public/components/explorer/visualizations/index.tsx index 8024bd7c8..773a028d7 100644 --- a/public/components/explorer/visualizations/index.tsx +++ b/public/components/explorer/visualizations/index.tsx @@ -9,86 +9,23 @@ * GitHub history for details. */ +import _ from 'lodash'; + import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiResizableContainer, - EuiListGroup, - EuiPage, - EuiPanel, - EuiTitle, - EuiText, - EuiSpacer -} from '@elastic/eui'; +import { FrameLayout } from './frameLayout'; +import { DataPanel } from './datapanel'; +import { WorkspacePanel } from './workspacePanel'; export const ExplorerVisualizations = (props: any) => { - return ( - - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - test - - - - - - -

test label

-
- - test text -
-
- - - - - test elements - - - )} -
-
- // - // - // - // fields sidebar - // - // - // - // - // visualization content - // - // - // - // - // edit panel - // - // - // + return ( + } + workspacePanel={ + + } + /> ); }; \ No newline at end of file diff --git a/public/components/explorer/visualizations/lens_field_icon.test.tsx b/public/components/explorer/visualizations/lens_field_icon.test.tsx new file mode 100644 index 000000000..317ce8f03 --- /dev/null +++ b/public/components/explorer/visualizations/lens_field_icon.test.tsx @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/* + * 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 { shallow } from 'enzyme'; +import { LensFieldIcon } from './lens_field_icon'; + +test('LensFieldIcon renders properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('LensFieldIcon accepts FieldIcon props', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); diff --git a/public/components/explorer/visualizations/lens_field_icon.tsx b/public/components/explorer/visualizations/lens_field_icon.tsx new file mode 100644 index 000000000..b48198b00 --- /dev/null +++ b/public/components/explorer/visualizations/lens_field_icon.tsx @@ -0,0 +1,25 @@ +/* + * 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 { FieldIcon, FieldIconProps } from '../../common/field_icon'; +// import { DataType } from '../types'; +// import { normalizeOperationDataType } from './utils'; + +// export function LensFieldIcon({ type, ...rest }: FieldIconProps & { type: DataType }) { +export function LensFieldIcon({ type, ...rest }) { + return ( + + ); +} + +export function normalizeOperationDataType(type) { + return type === 'document' ? 'number' : type; +} diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts b/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts new file mode 100644 index 000000000..fa7747dd1 --- /dev/null +++ b/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { + LensReportManager, + setReportManager, + stopReportManager, + trackUiEvent, + trackSuggestionEvent, +} from './factory'; +import { coreMock } from 'src/core/public/mocks'; +import { HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; + +jest.useFakeTimers(); + +const createMockStorage = () => { + let lastData = { events: {}, suggestionEvents: {} }; + return { + get: jest.fn().mockImplementation(() => lastData), + set: jest.fn().mockImplementation((key, value) => { + lastData = value; + }), + remove: jest.fn(), + clear: jest.fn(), + }; +}; + +describe('Lens UI telemetry', () => { + let storage: jest.Mocked; + let http: jest.Mocked; + let dateSpy: jest.SpyInstance; + + beforeEach(() => { + dateSpy = jest + .spyOn(Date, 'now') + .mockImplementation(() => new Date(Date.UTC(2019, 9, 23)).valueOf()); + + storage = createMockStorage(); + http = coreMock.createSetup().http; + http.post.mockClear(); + const fakeManager = new LensReportManager({ + http, + storage, + }); + setReportManager(fakeManager); + }); + + afterEach(() => { + stopReportManager(); + dateSpy.mockRestore(); + }); + + it('should write immediately and track local state', () => { + trackUiEvent('loaded'); + + expect(storage.set).toHaveBeenCalledWith('lens-ui-telemetry', { + events: expect.any(Object), + suggestionEvents: {}, + }); + + trackSuggestionEvent('reload'); + + expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', { + events: expect.any(Object), + suggestionEvents: expect.any(Object), + }); + }); + + it('should post the results after waiting 10 seconds, if there is data', async () => { + jest.runOnlyPendingTimers(); + + http.post.mockResolvedValue({}); + + expect(http.post).not.toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalledTimes(0); + + trackUiEvent('load'); + expect(storage.set).toHaveBeenCalledTimes(1); + + jest.runOnlyPendingTimers(); + + expect(http.post).toHaveBeenCalledWith(`/api/lens/stats`, { + body: JSON.stringify({ + events: { + '2019-10-23': { + load: 1, + }, + }, + suggestionEvents: {}, + }), + }); + }); + + it('should keep its local state after an http error', () => { + http.post.mockRejectedValue('http error'); + + trackUiEvent('load'); + expect(storage.set).toHaveBeenCalledTimes(1); + + jest.runOnlyPendingTimers(); + + expect(http.post).toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalledTimes(1); + }); +}); diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts b/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts new file mode 100644 index 000000000..8f9ce7f2c --- /dev/null +++ b/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts @@ -0,0 +1,122 @@ +/* + * 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 moment from 'moment'; +import { HttpSetup } from 'kibana/public'; + +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { BASE_API_URL } from '../../common'; + +const STORAGE_KEY = 'lens-ui-telemetry'; + +let reportManager: LensReportManager; + +export function setReportManager(newManager: LensReportManager) { + if (reportManager) { + reportManager.stop(); + } + reportManager = newManager; +} + +export function stopReportManager() { + if (reportManager) { + reportManager.stop(); + } +} + +export function trackUiEvent(name: string) { + if (reportManager) { + reportManager.trackEvent(name); + } +} + +export function trackSuggestionEvent(name: string) { + if (reportManager) { + reportManager.trackSuggestionEvent(name); + } +} + +export class LensReportManager { + private events: Record> = {}; + private suggestionEvents: Record> = {}; + + private storage: IStorageWrapper; + private http: HttpSetup; + private timer: ReturnType; + + constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) { + this.storage = storage; + this.http = http; + + this.readFromStorage(); + + this.timer = setInterval(() => { + this.postToServer(); + }, 10000); + } + + public trackEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.events, name); + } + + public trackSuggestionEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.suggestionEvents, name); + } + + public stop() { + if (this.timer) { + clearInterval(this.timer); + } + } + + private readFromStorage() { + const data = this.storage.get(STORAGE_KEY); + if (data && typeof data.events === 'object' && typeof data.suggestionEvents === 'object') { + this.events = data.events; + this.suggestionEvents = data.suggestionEvents; + } + } + + private async postToServer() { + this.readFromStorage(); + if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { + try { + await this.http.post(`${BASE_API_URL}/stats`, { + body: JSON.stringify({ + events: this.events, + suggestionEvents: this.suggestionEvents, + }), + }); + this.events = {}; + this.suggestionEvents = {}; + this.write(); + } catch (e) { + // Silent error because events will be reported during the next timer + } + } + } + + private trackTo(target: Record>, name: string) { + const date = moment().utc().format('YYYY-MM-DD'); + if (!target[date]) { + target[date] = { + [name]: 1, + }; + } else if (!target[date][name]) { + target[date][name] = 1; + } else { + target[date][name] += 1; + } + + this.write(); + } + + private write() { + this.storage.set(STORAGE_KEY, { events: this.events, suggestionEvents: this.suggestionEvents }); + } +} diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/index.ts b/public/components/explorer/visualizations/lens_ui_telemetry/index.ts new file mode 100644 index 000000000..79575a59f --- /dev/null +++ b/public/components/explorer/visualizations/lens_ui_telemetry/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './factory'; diff --git a/public/components/explorer/visualizations/workspacePanel.tsx b/public/components/explorer/visualizations/workspacePanel.tsx new file mode 100644 index 000000000..8d91e0895 --- /dev/null +++ b/public/components/explorer/visualizations/workspacePanel.tsx @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; + +export const WorkspacePanel = (props: any) => { + return ( + <> + central panel + + ); +}; \ No newline at end of file From fd7a466345484bb30e41985a9414573eb3e37ce6 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Tue, 20 Jul 2021 10:56:58 -0700 Subject: [PATCH 03/16] added two types of charts --- public/components/explorer/explorer.tsx | 2 +- public/components/explorer/logExplorer.tsx | 2 +- .../explorer/visualizations/app.scss | 42 + .../visualizations/assets/axis_bottom.tsx | 30 + .../visualizations/assets/axis_left.tsx | 31 + .../visualizations/assets/axis_right.tsx | 31 + .../visualizations/assets/axis_top.tsx | 34 + .../visualizations/assets/chart_area.tsx | 30 + .../assets/chart_area_percentage.tsx | 34 + .../assets/chart_area_stacked.tsx | 34 + .../visualizations/assets/chart_bar.tsx | 30 + .../assets/chart_bar_horizontal.tsx | 34 + .../chart_bar_horizontal_percentage.tsx | 34 + .../assets/chart_bar_horizontal_stacked.tsx | 34 + .../assets/chart_bar_percentage.tsx | 34 + .../assets/chart_bar_stacked.tsx | 34 + .../visualizations/assets/chart_datatable.tsx | 34 + .../visualizations/assets/chart_donut.tsx | 30 + .../visualizations/assets/chart_line.tsx | 30 + .../visualizations/assets/chart_metric.tsx | 30 + .../visualizations/assets/chart_mixed_xy.tsx | 34 + .../visualizations/assets/chart_pie.tsx | 30 + .../visualizations/assets/chart_treemap.tsx | 34 + .../assets/drop_illustration.tsx | 48 ++ .../explorer/visualizations/assets/legend.tsx | 39 + .../assets/lens_app_graphic_dark_2x.png | Bin 0 -> 82733 bytes .../assets/lens_app_graphic_light_2x.png | Bin 0 -> 94444 bytes .../config_panel/config_panel.scss | 7 + .../config_panel/config_panel.tsx | 163 ++++ .../config_panel/dimension_container.scss | 19 + .../config_panel/dimension_container.tsx | 90 ++ .../visualizations/config_panel/index.ts | 7 + .../config_panel/layer_actions.test.ts | 127 +++ .../config_panel/layer_actions.ts | 90 ++ .../config_panel/layer_panel.scss | 70 ++ .../config_panel/layer_panel.test.tsx | 473 ++++++++++ .../config_panel/layer_panel.tsx | 477 ++++++++++ .../config_panel/layer_settings.tsx | 64 ++ .../visualizations/config_panel/types.ts | 38 + .../explorer/visualizations/frameLayout.tsx | 3 +- .../explorer/visualizations/index.tsx | 25 +- .../shared_components/empty_placeholder.tsx | 24 + .../visualizations/shared_components/index.ts | 10 + .../legend_settings_popover.test.tsx | 106 +++ .../legend_settings_popover.tsx | 159 ++++ .../shared_components/toolbar_button.scss | 60 ++ .../shared_components/toolbar_button.tsx | 74 ++ .../shared_components/toolbar_popover.tsx | 81 ++ .../workspace_panel/chartSwitch.tsx | 145 ++++ .../workspace_panel/chart_switch.scss | 22 + .../workspace_panel/chart_switch.test.tsx | 649 ++++++++++++++ .../workspace_panel/chart_switch.tsx | 332 +++++++ .../visualizations/workspace_panel/index.ts | 7 + .../workspace_panel/workspace_panel.test.tsx | 811 ++++++++++++++++++ .../workspace_panel/workspace_panel.tsx | 338 ++++++++ .../workspace_panel_wrapper.scss | 128 +++ .../workspace_panel_wrapper.test.tsx | 71 ++ .../workspace_panel_wrapper.tsx | 134 +++ .../visualizations/plotly/plot_template.tsx | 8 +- .../visualizations/visualization/bar.tsx | 45 + .../visualization/countDistribution.tsx | 31 - .../visualizations/visualization/line.tsx | 52 ++ public/plugin.ts | 14 +- public/{ => services}/requests/ppl.ts | 4 +- public/services/visualizations/index.ts | 1 + .../visualizations/visualizationBase.ts | 28 + .../visualizations/xyVisualization.ts | 30 + 67 files changed, 5752 insertions(+), 44 deletions(-) create mode 100644 public/components/explorer/visualizations/app.scss create mode 100644 public/components/explorer/visualizations/assets/axis_bottom.tsx create mode 100644 public/components/explorer/visualizations/assets/axis_left.tsx create mode 100644 public/components/explorer/visualizations/assets/axis_right.tsx create mode 100644 public/components/explorer/visualizations/assets/axis_top.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_area.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_area_percentage.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_area_stacked.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar_percentage.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_bar_stacked.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_datatable.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_donut.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_line.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_metric.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_mixed_xy.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_pie.tsx create mode 100644 public/components/explorer/visualizations/assets/chart_treemap.tsx create mode 100644 public/components/explorer/visualizations/assets/drop_illustration.tsx create mode 100644 public/components/explorer/visualizations/assets/legend.tsx create mode 100644 public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png create mode 100644 public/components/explorer/visualizations/assets/lens_app_graphic_light_2x.png create mode 100644 public/components/explorer/visualizations/config_panel/config_panel.scss create mode 100644 public/components/explorer/visualizations/config_panel/config_panel.tsx create mode 100644 public/components/explorer/visualizations/config_panel/dimension_container.scss create mode 100644 public/components/explorer/visualizations/config_panel/dimension_container.tsx create mode 100644 public/components/explorer/visualizations/config_panel/index.ts create mode 100644 public/components/explorer/visualizations/config_panel/layer_actions.test.ts create mode 100644 public/components/explorer/visualizations/config_panel/layer_actions.ts create mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.scss create mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.test.tsx create mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.tsx create mode 100644 public/components/explorer/visualizations/config_panel/layer_settings.tsx create mode 100644 public/components/explorer/visualizations/config_panel/types.ts create mode 100644 public/components/explorer/visualizations/shared_components/empty_placeholder.tsx create mode 100644 public/components/explorer/visualizations/shared_components/index.ts create mode 100644 public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx create mode 100644 public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx create mode 100644 public/components/explorer/visualizations/shared_components/toolbar_button.scss create mode 100644 public/components/explorer/visualizations/shared_components/toolbar_button.tsx create mode 100644 public/components/explorer/visualizations/shared_components/toolbar_popover.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/chart_switch.scss create mode 100644 public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/chart_switch.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/index.ts create mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss create mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx create mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx create mode 100644 public/components/visualizations/visualization/bar.tsx delete mode 100644 public/components/visualizations/visualization/countDistribution.tsx create mode 100644 public/components/visualizations/visualization/line.tsx rename public/{ => services}/requests/ppl.ts (87%) create mode 100644 public/services/visualizations/index.ts create mode 100644 public/services/visualizations/visualizationBase.ts create mode 100644 public/services/visualizations/xyVisualization.ts diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 7c85ce12c..b5f314e14 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { Search } from '../common/seach/search'; -import { CountDistribution } from '../visualizations/visualization/countDistribution'; +import { CountDistribution } from '../visualizations/visualization/bar'; import { DataGrid } from './dataGrid'; import { Sidebar } from './sidebar'; import { NoResults } from './noResults'; diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index 1a5f77947..1ffc35686 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -20,7 +20,7 @@ import { EuiTabbedContent } from '@elastic/eui'; import { Explorer } from './explorer'; -import { handlePplRequest } from '../../requests/ppl'; +import { handlePplRequest } from '../../services/requests/ppl'; import { IField, } from '../../common/types/explorer'; diff --git a/public/components/explorer/visualizations/app.scss b/public/components/explorer/visualizations/app.scss new file mode 100644 index 000000000..8416577a6 --- /dev/null +++ b/public/components/explorer/visualizations/app.scss @@ -0,0 +1,42 @@ +.lnsAppWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.lnsApp { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.lnsApp__header { + border-bottom: $euiBorderThin; +} + +.lnsApp__frame { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.lensChartIcon__subdued { + fill: $euiTextSubduedColor; + + // Not great, but the easiest way to fix the gray fill when stuck in a button with a fill + // Like when selected in a button group + .euiButton--fill & { + fill: currentColor; + } +} + +.lensChartIcon__accent { + fill: $euiColorVis0; +} diff --git a/public/components/explorer/visualizations/assets/axis_bottom.tsx b/public/components/explorer/visualizations/assets/axis_bottom.tsx new file mode 100644 index 000000000..9529a93e4 --- /dev/null +++ b/public/components/explorer/visualizations/assets/axis_bottom.tsx @@ -0,0 +1,30 @@ +/* + * 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 * as React from 'react'; + +export const EuiIconAxisBottom = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/axis_left.tsx b/public/components/explorer/visualizations/assets/axis_left.tsx new file mode 100644 index 000000000..d1ec0b76a --- /dev/null +++ b/public/components/explorer/visualizations/assets/axis_left.tsx @@ -0,0 +1,31 @@ +/* + * 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 * as React from 'react'; + +export const EuiIconAxisLeft = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + +); diff --git a/public/components/explorer/visualizations/assets/axis_right.tsx b/public/components/explorer/visualizations/assets/axis_right.tsx new file mode 100644 index 000000000..e61f87b96 --- /dev/null +++ b/public/components/explorer/visualizations/assets/axis_right.tsx @@ -0,0 +1,31 @@ +/* + * 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 * as React from 'react'; + +export const EuiIconAxisRight = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + +); diff --git a/public/components/explorer/visualizations/assets/axis_top.tsx b/public/components/explorer/visualizations/assets/axis_top.tsx new file mode 100644 index 000000000..90fbdc4a2 --- /dev/null +++ b/public/components/explorer/visualizations/assets/axis_top.tsx @@ -0,0 +1,34 @@ +/* + * 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 * as React from 'react'; + +export const EuiIconAxisTop = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_area.tsx b/public/components/explorer/visualizations/assets/chart_area.tsx new file mode 100644 index 000000000..ae817e979 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_area.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartArea = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_area_percentage.tsx b/public/components/explorer/visualizations/assets/chart_area_percentage.tsx new file mode 100644 index 000000000..45c208d5d --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_area_percentage.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartAreaPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_area_stacked.tsx b/public/components/explorer/visualizations/assets/chart_area_stacked.tsx new file mode 100644 index 000000000..0320ad7e9 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_area_stacked.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartAreaStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar.tsx b/public/components/explorer/visualizations/assets/chart_bar.tsx new file mode 100644 index 000000000..9408f77bd --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBar = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx new file mode 100644 index 000000000..7ec48b107 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontal = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx new file mode 100644 index 000000000..6ce09265d --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontalPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx new file mode 100644 index 000000000..c862121fd --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarHorizontalStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx b/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx new file mode 100644 index 000000000..b7d6a0ed6 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarPercentage = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx b/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx new file mode 100644 index 000000000..edf8e6751 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarStacked = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_datatable.tsx b/public/components/explorer/visualizations/assets/chart_datatable.tsx new file mode 100644 index 000000000..48cc844ea --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_datatable.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartDatatable = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_donut.tsx b/public/components/explorer/visualizations/assets/chart_donut.tsx new file mode 100644 index 000000000..9482161de --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_donut.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartDonut = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_line.tsx b/public/components/explorer/visualizations/assets/chart_line.tsx new file mode 100644 index 000000000..5b57e1fe2 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_line.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartLine = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_metric.tsx b/public/components/explorer/visualizations/assets/chart_metric.tsx new file mode 100644 index 000000000..9faa4d658 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_metric.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartMetric = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx b/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx new file mode 100644 index 000000000..08eac8eb1 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartMixedXy = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_pie.tsx b/public/components/explorer/visualizations/assets/chart_pie.tsx new file mode 100644 index 000000000..cc26df441 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_pie.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartPie = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/public/components/explorer/visualizations/assets/chart_treemap.tsx b/public/components/explorer/visualizations/assets/chart_treemap.tsx new file mode 100644 index 000000000..57205e941 --- /dev/null +++ b/public/components/explorer/visualizations/assets/chart_treemap.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartTreemap = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/public/components/explorer/visualizations/assets/drop_illustration.tsx b/public/components/explorer/visualizations/assets/drop_illustration.tsx new file mode 100644 index 000000000..1076f4875 --- /dev/null +++ b/public/components/explorer/visualizations/assets/drop_illustration.tsx @@ -0,0 +1,48 @@ +/* + * 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 * as React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const DropIllustration = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + + + + + +); diff --git a/public/components/explorer/visualizations/assets/legend.tsx b/public/components/explorer/visualizations/assets/legend.tsx new file mode 100644 index 000000000..d73e68839 --- /dev/null +++ b/public/components/explorer/visualizations/assets/legend.tsx @@ -0,0 +1,39 @@ +/* + * 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 * as React from 'react'; + +export const EuiIconLegend = ({ title, titleId, ...props }: { title: string; titleId: string }) => ( + + {title ? {title} : null} + + + + + + + +); diff --git a/public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png b/public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2c71b82180a7574427c284cfffa91771a2296a GIT binary patch literal 82733 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

D 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

s4&bcDbHgys`Q8|Ssnb#9qa?* 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 + ); +}); + +function LayerPanels( + props: ConfigPanelWrapperProps & { + activeDatasourceId: string; + activeVisualization: Visualization; + } +) { + const { + // activeVisualization, + // visualizationState, + dispatch, + // activeDatasourceId, + // datasourceMap, + } = props; + const setVisualizationState = useMemo( + () => (newState: unknown) => { + // dispatch({ + // type: 'UPDATE_VISUALIZATION_STATE', + // visualizationId: activeVisualization.id, + // newState, + // clearStagedPreview: false, + // }); + }, + // [dispatch, activeVisualization] + [] + ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: (prevState) => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + ...prevState.visualization, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [dispatch] + ); + // const layerIds = activeVisualization.getLayerIds(visualizationState); + + return ( + + {/* {layerIds.map((layerId, index) => ( + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'REMOVE_OR_CLEAR_LAYER', + updater: (state) => + // removeLayer({ + // activeVisualization, + // layerId, + // trackUiEvent, + // datasourceMap, + // state, + // }), + }); + }} + /> + ))} */} + {true && ( + + + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: (state) => + // appendLayer({ + // activeVisualization, + // generateId, + // trackUiEvent, + // activeDatasource: datasourceMap[activeDatasourceId], + // state, + // }), + }); + }} + iconType="plusInCircleFilled" + /> + + + )} + + ); +} diff --git a/public/components/explorer/visualizations/config_panel/dimension_container.scss b/public/components/explorer/visualizations/config_panel/dimension_container.scss new file mode 100644 index 000000000..bd2789cf6 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/dimension_container.scss @@ -0,0 +1,19 @@ +@import '@elastic/eui/src/components/flyout/variables'; +@import '@elastic/eui/src/components/flyout/mixins'; + +.lnsDimensionContainer { + // Use the EuiFlyout style + @include euiFlyout; + // But with custom positioning to keep it within the sidebar contents + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; + animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance; +} + +.lnsDimensionContainer__footer, +.lnsDimensionContainer__header { + padding: $euiSizeS; +} diff --git a/public/components/explorer/visualizations/config_panel/dimension_container.tsx b/public/components/explorer/visualizations/config_panel/dimension_container.tsx new file mode 100644 index 000000000..8f1b441d1 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/dimension_container.tsx @@ -0,0 +1,90 @@ +/* + * 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 './dimension_container.scss'; + +import React, { useState, useEffect } from 'react'; +import { + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiTitle, + EuiButtonEmpty, + EuiFlexItem, + EuiFocusTrap, + EuiOutsideClickDetector, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +export function DimensionContainer({ + isOpen, + groupLabel, + handleClose, + panel, +}: { + isOpen: boolean; + handleClose: () => void; + panel: React.ReactElement; + groupLabel: string; +}) { + const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false); + + const closeFlyout = () => { + handleClose(); + setFocusTrapIsEnabled(false); + }; + + useEffect(() => { + if (isOpen) { + // without setTimeout here the flyout pushes content when animating + setTimeout(() => { + setFocusTrapIsEnabled(true); + }, 255); + } + }, [isOpen]); + + return isOpen ? ( + + +

+ + + + + {i18n.translate('xpack.lens.configure.configurePanelTitle', { + defaultMessage: '{groupLabel} configuration', + values: { + groupLabel, + }, + })} + + + + + + {panel} + + + + {i18n.translate('xpack.lens.dimensionContainer.close', { + defaultMessage: 'Close', + })} + + +
+ + + ) : null; +} diff --git a/public/components/explorer/visualizations/config_panel/index.ts b/public/components/explorer/visualizations/config_panel/index.ts new file mode 100644 index 000000000..754b3fb5c --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { ConfigPanelWrapper } from './config_panel'; diff --git a/public/components/explorer/visualizations/config_panel/layer_actions.test.ts b/public/components/explorer/visualizations/config_panel/layer_actions.test.ts new file mode 100644 index 000000000..3363e34a0 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_actions.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { removeLayer, appendLayer } from './layer_actions'; + +function createTestArgs(initialLayerIds: string[]) { + const trackUiEvent = jest.fn(); + const testDatasource = (datasourceId: string) => ({ + id: datasourceId, + clearLayer: (layerIds: unknown, layerId: string) => + (layerIds as string[]).map((id: string) => + id === layerId ? `${datasourceId}_clear_${layerId}` : id + ), + removeLayer: (layerIds: unknown, layerId: string) => + (layerIds as string[]).filter((id: string) => id !== layerId), + insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], + }); + + const activeVisualization = { + clearLayer: (layerIds: unknown, layerId: string) => + (layerIds as string[]).map((id: string) => (id === layerId ? `vis_clear_${layerId}` : id)), + removeLayer: (layerIds: unknown, layerId: string) => + (layerIds as string[]).filter((id: string) => id !== layerId), + getLayerIds: (layerIds: unknown) => layerIds as string[], + appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], + }; + + const datasourceStates = { + ds1: { + isLoading: false, + state: initialLayerIds.slice(0, 1), + }, + ds2: { + isLoading: false, + state: initialLayerIds.slice(1), + }, + }; + + return { + state: { + activeDatasourceId: 'ds1', + datasourceStates, + title: 'foo', + visualization: { + activeId: 'vis1', + state: initialLayerIds, + }, + }, + activeVisualization, + datasourceMap: { + ds1: testDatasource('ds1'), + ds2: testDatasource('ds2'), + }, + trackUiEvent, + stagedPreview: { + visualization: { + activeId: 'vis1', + state: initialLayerIds, + }, + datasourceStates, + }, + }; +} + +describe('removeLayer', () => { + it('should clear the layer if it is the only layer', () => { + const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs(['layer1']); + const newState = removeLayer({ + activeVisualization, + datasourceMap, + layerId: 'layer1', + state, + trackUiEvent, + }); + + expect(newState.visualization.state).toEqual(['vis_clear_layer1']); + expect(newState.datasourceStates.ds1.state).toEqual(['ds1_clear_layer1']); + expect(newState.datasourceStates.ds2.state).toEqual([]); + expect(newState.stagedPreview).not.toBeDefined(); + expect(trackUiEvent).toHaveBeenCalledWith('layer_cleared'); + }); + + it('should remove the layer if it is not the only layer', () => { + const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([ + 'layer1', + 'layer2', + ]); + const newState = removeLayer({ + activeVisualization, + datasourceMap, + layerId: 'layer1', + state, + trackUiEvent, + }); + + expect(newState.visualization.state).toEqual(['layer2']); + expect(newState.datasourceStates.ds1.state).toEqual([]); + expect(newState.datasourceStates.ds2.state).toEqual(['layer2']); + expect(newState.stagedPreview).not.toBeDefined(); + expect(trackUiEvent).toHaveBeenCalledWith('layer_removed'); + }); +}); + +describe('appendLayer', () => { + it('should add the layer to the datasource and visualization', () => { + const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([ + 'layer1', + 'layer2', + ]); + const newState = appendLayer({ + activeDatasource: datasourceMap.ds1, + activeVisualization, + generateId: () => 'foo', + state, + trackUiEvent, + }); + + expect(newState.visualization.state).toEqual(['layer1', 'layer2', 'foo']); + expect(newState.datasourceStates.ds1.state).toEqual(['layer1', 'foo']); + expect(newState.datasourceStates.ds2.state).toEqual(['layer2']); + expect(newState.stagedPreview).not.toBeDefined(); + expect(trackUiEvent).toHaveBeenCalledWith('layer_added'); + }); +}); diff --git a/public/components/explorer/visualizations/config_panel/layer_actions.ts b/public/components/explorer/visualizations/config_panel/layer_actions.ts new file mode 100644 index 000000000..131bb6208 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_actions.ts @@ -0,0 +1,90 @@ +/* + * 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 _ from 'lodash'; +import { EditorFrameState } from '../state_management'; +import { Datasource, Visualization } from '../../../types'; + +interface RemoveLayerOptions { + trackUiEvent: (name: string) => void; + state: EditorFrameState; + layerId: string; + activeVisualization: Pick; + datasourceMap: Record>; +} + +interface AppendLayerOptions { + trackUiEvent: (name: string) => void; + state: EditorFrameState; + generateId: () => string; + activeDatasource: Pick; + activeVisualization: Pick; +} + +export function removeLayer(opts: RemoveLayerOptions): EditorFrameState { + const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts; + const isOnlyLayer = activeVisualization + .getLayerIds(state.visualization.state) + .every((id) => id === opts.layerId); + + trackUiEvent(isOnlyLayer ? 'layer_cleared' : 'layer_removed'); + + return { + ...state, + datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => { + const datasource = datasourceMap[datasourceId!]; + return { + ...datasourceState, + state: isOnlyLayer + ? datasource.clearLayer(datasourceState.state, layerId) + : datasource.removeLayer(datasourceState.state, layerId), + }; + }), + visualization: { + ...state.visualization, + state: + isOnlyLayer || !activeVisualization.removeLayer + ? activeVisualization.clearLayer(state.visualization.state, layerId) + : activeVisualization.removeLayer(state.visualization.state, layerId), + }, + stagedPreview: undefined, + }; +} + +export function appendLayer({ + trackUiEvent, + activeVisualization, + state, + generateId, + activeDatasource, +}: AppendLayerOptions): EditorFrameState { + trackUiEvent('layer_added'); + + if (!activeVisualization.appendLayer) { + return state; + } + + const layerId = generateId(); + + return { + ...state, + datasourceStates: { + ...state.datasourceStates, + [activeDatasource.id]: { + ...state.datasourceStates[activeDatasource.id], + state: activeDatasource.insertLayer( + state.datasourceStates[activeDatasource.id].state, + layerId + ), + }, + }, + visualization: { + ...state.visualization, + state: activeVisualization.appendLayer(state.visualization.state, layerId), + }, + stagedPreview: undefined, + }; +} diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.scss b/public/components/explorer/visualizations/config_panel/layer_panel.scss new file mode 100644 index 000000000..54c922957 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_panel.scss @@ -0,0 +1,70 @@ +.lnsLayerPanel { + margin-bottom: $euiSizeS; +} + +.lnsLayerPanel__sourceFlexItem { + max-width: calc(100% - #{$euiSize * 3.625}); +} + +.lnsLayerPanel__settingsFlexItem:empty + .lnsLayerPanel__sourceFlexItem { + max-width: calc(100% - #{$euiSizeS}); +} + +.lnsLayerPanel__settingsFlexItem:empty { + margin: 0; +} + +.lnsLayerPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsLayerPanel__dimension { + @include euiFontSizeS; + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; + width: 100%; + min-height: $euiSizeXXL; + + // NativeRenderer is messing this up + > div { + flex-grow: 1; + } + + &:focus, + &:focus-within { + @include euiFocusRing; + } +} + +.lnsLayerPanel__triggerLink { + width: 100%; + padding: $euiSizeS; + min-height: $euiSizeXXL - 2; + word-break: break-word; + + &:focus { + background-color: transparent !important; // sass-lint:disable-line no-important + outline: none !important; // sass-lint:disable-line no-important + } +} + +.lnsLayerPanel__triggerLinkContent { + // Make EUI button content not centered + justify-content: flex-start; + padding: 0 !important; // sass-lint:disable-line no-important + color: $euiTextSubduedColor; +} + +.lnsLayerPanel__styleEditor { + padding: 0 $euiSizeS $euiSizeS; +} diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx b/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx new file mode 100644 index 000000000..44dc22d20 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx @@ -0,0 +1,473 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, + DatasourceMock, +} from '../../mocks'; +import { ChildDragDropProvider } from '../../../drag_drop'; +import { EuiFormRow } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { Visualization } from '../../../types'; +import { LayerPanel } from './layer_panel'; +import { coreMock } from 'src/core/public/mocks'; +import { generateId } from '../../../id_generator'; + +jest.mock('../../../id_generator'); + +describe('LayerPanel', () => { + let mockVisualization: jest.Mocked; + let mockVisualization2: jest.Mocked; + let mockDatasource: DatasourceMock; + + function getDefaultProps() { + const frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + return { + layerId: 'first', + activeVisualizationId: 'vis1', + visualizationMap: { + vis1: mockVisualization, + vis2: mockVisualization2, + }, + activeDatasourceId: 'ds1', + datasourceMap: { + ds1: mockDatasource, + }, + datasourceStates: { + ds1: { + isLoading: false, + state: 'state', + }, + }, + visualizationState: 'state', + updateVisualization: jest.fn(), + updateDatasource: jest.fn(), + updateAll: jest.fn(), + framePublicAPI: frame, + isOnlyLayer: true, + onRemoveLayer: jest.fn(), + dispatch: jest.fn(), + core: coreMock.createStart(), + dataTestSubj: 'lns_layerPanel-0', + }; + } + + beforeEach(() => { + mockVisualization = { + ...createMockVisualization(), + id: 'testVis', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis', + label: 'TEST1', + }, + ], + }; + + mockVisualization2 = { + ...createMockVisualization(), + id: 'testVis2', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis2', + label: 'TEST2', + }, + ], + }; + + mockVisualization.getLayerIds.mockReturnValue(['first']); + mockDatasource = createMockDatasource('ds1'); + }); + + it('should fail to render if the public API is out of date', () => { + const props = getDefaultProps(); + props.framePublicAPI.datasourceLayers = {}; + const component = mountWithIntl(); + expect(component.isEmptyRender()).toBe(true); + }); + + it('should fail to render if the active visualization is missing', () => { + const component = mountWithIntl( + + ); + expect(component.isEmptyRender()).toBe(true); + }); + + describe('layer reset and remove', () => { + it('should show the reset button when single layer', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + 'Reset layer' + ); + }); + + it('should show the delete button when multiple layers', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + 'Delete layer' + ); + }); + + it('should call the clear callback', () => { + const cb = jest.fn(); + const component = mountWithIntl(); + act(() => { + component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); + }); + expect(cb).toHaveBeenCalled(); + }); + }); + + describe('single group', () => { + it('should render the non-editable state', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the group with a way to add a new column', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the required warning when only one group is configured', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + required: true, + }, + ], + }); + + const component = mountWithIntl(); + + const group = component + .find(EuiFormRow) + .findWhere((e) => e.prop('error') === 'Required dimension'); + expect(group).toHaveLength(1); + }); + + it('should render the datasource and visualization panels inside the dimension container', () => { + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + enableDimensionEditor: true, + }, + ], + }); + mockVisualization.renderDimensionEditor = jest.fn(); + + const component = mountWithIntl(); + act(() => { + component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + const group = component.find('DimensionContainer').first(); + const panel: React.ReactElement = group.prop('panel'); + expect(panel.props.children).toHaveLength(2); + }); + + it('should keep the DimensionContainer open when configuring a new dimension', () => { + /** + * The ID generation system for new dimensions has been messy before, so + * this tests that the ID used in the first render is used to keep the container + * open in future renders + */ + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValueOnce(`bad`); + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + // Normally the configuration would change in response to a state update, + // but this test is updating it directly + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + act(() => { + component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + }); + + it('should close the DimensionContainer when the active visualization changes', () => { + /** + * The ID generation system for new dimensions has been messy before, so + * this tests that the ID used in the first render is used to keep the container + * open in future renders + */ + + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValueOnce(`bad`); + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + // Normally the configuration would change in response to a state update, + // but this test is updating it directly + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + act(() => { + component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + act(() => { + component.setProps({ activeVisualizationId: 'vis2' }); + }); + component.update(); + expect(component.find('EuiFlyoutHeader').exists()).toBe(false); + }); + }); + + // This test is more like an integration test, since the layer panel owns all + // the coordination between drag and drop + describe('drag and drop behavior', () => { + it('should determine if the datasource supports dropping of a field onto empty dimension', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + + component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + }); + + it('should allow drag to move between groups', () => { + (generateId as jest.Mock).mockReturnValue(`newid`); + + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroupA', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: ['b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroupB', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2); + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the pre-populated dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'b', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the empty dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'newid', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + }); + + it('should prevent dropping in the same group', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a', 'b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(2).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.tsx b/public/components/explorer/visualizations/config_panel/layer_panel.tsx new file mode 100644 index 000000000..fdf225832 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_panel.tsx @@ -0,0 +1,477 @@ +/* + * 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 './layer_panel.scss'; + +import React, { useContext, useState, useEffect } from 'react'; +import _ from 'lodash'; +import { + EuiPanel, + EuiSpacer, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +// import { NativeRenderer } from '../../../native_renderer'; +// import { StateSetter, isDraggedOperation } from '../../../types'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../drag_drop'; +import { LayerSettings } from './layer_settings'; +// import { trackUiEvent } from '../../../lens_ui_telemetry'; +// import { generateId } from '../../../id_generator'; +import { ConfigPanelWrapperProps, ActiveDimensionState } from './types'; +import { DimensionContainer } from './dimension_container'; + +const initialActiveDimensionState = { + isNew: false, +}; + +function isConfiguration( + value: unknown +): value is { columnId: string; groupId: string; layerId: string } { + return ( + value && + typeof value === 'object' && + 'columnId' in value && + 'groupId' in value && + 'layerId' in value + ); +} + +function isSameConfiguration(config1: unknown, config2: unknown) { + return ( + isConfiguration(config1) && + isConfiguration(config2) && + config1.columnId === config2.columnId && + config1.groupId === config2.groupId && + config1.layerId === config2.layerId + ); +} + +export function LayerPanel( + props: Exclude & { + layerId: string; + dataTestSubj: string; + isOnlyLayer: boolean; + // updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } +) { + const dragDropContext = useContext(DragContext); + const [activeDimension, setActiveDimension] = useState( + initialActiveDimensionState + ); + + const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, dataTestSubj } = props; + const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + + useEffect(() => { + setActiveDimension(initialActiveDimensionState); + }, [props.activeVisualizationId]); + + if ( + !datasourcePublicAPI || + !props.activeVisualizationId || + !props.visualizationMap[props.activeVisualizationId] + ) { + return null; + } + const activeVisualization = props.visualizationMap[props.activeVisualizationId]; + const layerVisualizationConfigProps = { + layerId, + dragDropContext, + state: props.visualizationState, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; + + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; + + const layerDatasourceConfigProps = { + ...layerDatasourceDropProps, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); + const { activeId, activeGroup } = activeDimension; + return ( + + + + + + + + {layerDatasource && ( + + {/* { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); + + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> */} + + )} + + + + + {groups.map((group, index) => { + const newId = _.uniqueId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + + return ( + + <> + {group.accessors.map((accessor) => { + return ( + { + const dropResult = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + // props.updateVisualization( + // activeVisualization.removeDimension({ + // layerId, + // columnId: dropResult.deleted, + // prevState: props.visualizationState, + // }) + // ); + } + }} + > +
+ {/* { + if (activeId) { + setActiveDimension(initialActiveDimensionState); + } else { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: accessor, + }); + } + }, + }} + /> */} + { + // trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> +
+
+ ); + })} + {group.supportsMoreColumns ? ( + { + const dropResult = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropResult) { + // props.updateVisualization( + // activeVisualization.setDimension({ + // layerId, + // groupId: group.groupId, + // columnId: newId, + // prevState: props.visualizationState, + // }) + // ); + + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + // props.updateVisualization( + // activeVisualization.removeDimension({ + // layerId, + // columnId: dropResult.deleted, + // prevState: props.visualizationState, + // }) + // ); + } + } + }} + > +
+ { + if (activeId) { + setActiveDimension(initialActiveDimensionState); + } else { + setActiveDimension({ + isNew: true, + activeGroup: group, + activeId: newId, + }); + } + }} + > + + +
+
+ ) : null} + +
+ ); + })} + setActiveDimension(initialActiveDimensionState)} + panel={ + <> + {activeGroup && activeId && ( + // { + // props.updateAll( + // datasourceId, + // newState, + // activeVisualization.setDimension({ + // layerId, + // groupId: activeGroup.groupId, + // columnId: activeId, + // prevState: props.visualizationState, + // }) + // ); + // setActiveDimension({ + // ...activeDimension, + // isNew: false, + // }); + // }, + // }} + // /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ {/* */} +
+ )} + + } + /> + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + +
+
+ ); +} diff --git a/public/components/explorer/visualizations/config_panel/layer_settings.tsx b/public/components/explorer/visualizations/config_panel/layer_settings.tsx new file mode 100644 index 000000000..abbd7e083 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/layer_settings.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useState } from 'react'; +import { EuiPopover, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; +import { ToolbarButton } from '../../../shared_components'; + +export function LayerSettings({ + layerId, + activeVisualization, + layerConfigProps, +}: { + layerId: string; + activeVisualization: Visualization; + layerConfigProps: VisualizationLayerWidgetProps; +}) { + const [isOpen, setIsOpen] = useState(false); + + if (!activeVisualization.renderLayerContextMenu) { + return null; + } + + const a11yText = i18n.translate('xpack.lens.editLayerSettings', { + defaultMessage: 'Edit layer settings', + }); + + return ( + + setIsOpen(!isOpen)} + data-test-subj="lns_layer_settings" + /> + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="downLeft" + > + + + ); +} diff --git a/public/components/explorer/visualizations/config_panel/types.ts b/public/components/explorer/visualizations/config_panel/types.ts new file mode 100644 index 000000000..c172c6da6 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { Action } from '../state_management'; +import { + Visualization, + FramePublicAPI, + Datasource, + DatasourceDimensionEditorProps, + VisualizationDimensionGroupConfig, +} from '../../../types'; + +export interface ConfigPanelWrapperProps { + activeDatasourceId: string; + visualizationState: unknown; + visualizationMap: Record; + activeVisualizationId: string | null; + dispatch: (action: Action) => void; + framePublicAPI: FramePublicAPI; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + core: DatasourceDimensionEditorProps['core']; +} + +export interface ActiveDimensionState { + isNew: boolean; + activeId?: string; + activeGroup?: VisualizationDimensionGroupConfig; +} diff --git a/public/components/explorer/visualizations/frameLayout.tsx b/public/components/explorer/visualizations/frameLayout.tsx index 09d4cc6e1..11a74aedb 100644 --- a/public/components/explorer/visualizations/frameLayout.tsx +++ b/public/components/explorer/visualizations/frameLayout.tsx @@ -33,8 +33,7 @@ export function FrameLayout(props: FrameLayoutProps) { {/* {props.suggestionsPanel} */} - {/* {props.configPanel} */} - right sidebar + {props.configPanel}
diff --git a/public/components/explorer/visualizations/index.tsx b/public/components/explorer/visualizations/index.tsx index 773a028d7..564446016 100644 --- a/public/components/explorer/visualizations/index.tsx +++ b/public/components/explorer/visualizations/index.tsx @@ -9,12 +9,30 @@ * GitHub history for details. */ +import './app.scss'; + import _ from 'lodash'; import React from 'react'; import { FrameLayout } from './frameLayout'; import { DataPanel } from './datapanel'; -import { WorkspacePanel } from './workspacePanel'; +import { WorkspacePanel } from './workspace_panel'; +// import {} + +// const VIS_MAPS = { +// 'lnsXY': { +// visualizationTypes: [ +// { +// id: 'bar', +// label: 'Bar' +// }, +// { +// id: 'line', +// label: 'Line' +// } +// ] +// } +// }; export const ExplorerVisualizations = (props: any) => { @@ -24,7 +42,10 @@ export const ExplorerVisualizations = (props: any) => { queryResults={ props.queryResults } />} workspacePanel={ - + {} } + // visualizationMap={ VIS_MAPS } + /> } /> ); diff --git a/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx b/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx new file mode 100644 index 000000000..a2ea5c10d --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const EmptyPlaceholder = (props: { icon: IconType }) => ( + <> + + + +

+ +

+
+ +); diff --git a/public/components/explorer/visualizations/shared_components/index.ts b/public/components/explorer/visualizations/shared_components/index.ts new file mode 100644 index 000000000..c0362a566 --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './empty_placeholder'; +export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; +export { ToolbarButtonProps, ToolbarButton } from './toolbar_button'; +export { LegendSettingsPopover } from './legend_settings_popover'; diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx b/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx new file mode 100644 index 000000000..1e0e6b33b --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; +import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover'; + +describe('Legend Settings', () => { + const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [ + { + id: `test_legend_auto`, + value: 'auto', + label: 'Auto', + }, + { + id: `test_legend_show`, + value: 'show', + label: 'Show', + }, + { + id: `test_legend_hide`, + value: 'hide', + label: 'Hide', + }, + ]; + let props: LegendSettingsPopoverProps; + beforeEach(() => { + props = { + legendOptions, + mode: 'auto', + onDisplayChange: jest.fn(), + onPositionChange: jest.fn(), + }; + }); + + it('should have selected the given mode as Display value', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lens-legend-display-btn"]').prop('idSelected')).toEqual( + 'test_legend_auto' + ); + }); + + it('should have called the onDisplayChange function on ButtonGroup change', () => { + const component = shallow(); + component.find('[data-test-subj="lens-legend-display-btn"]').simulate('change'); + expect(props.onDisplayChange).toHaveBeenCalled(); + }); + + it('should have default the Position to right when no position is given', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-position-btn"]').prop('idSelected') + ).toEqual(Position.Right); + }); + + it('should have called the onPositionChange function on ButtonGroup change', () => { + const component = shallow(); + component.find('[data-test-subj="lens-legend-position-btn"]').simulate('change'); + expect(props.onPositionChange).toHaveBeenCalled(); + }); + + it('should disable the position button group on hide mode', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-position-btn"]').prop('isDisabled') + ).toEqual(true); + }); + + it('should enable the Nested Legend Switch when renderNestedLegendSwitch prop is true', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1); + }); + + it('should set the switch state on nestedLegend prop value', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('checked')).toEqual( + true + ); + }); + + it('should have called the onNestedLegendChange function on switch change', () => { + const nestedProps = { + ...props, + renderNestedLegendSwitch: true, + onNestedLegendChange: jest.fn(), + }; + const component = shallow(); + component.find('[data-test-subj="lens-legend-nested-switch"]').simulate('change'); + expect(nestedProps.onNestedLegendChange).toHaveBeenCalled(); + }); + + it('should disable switch group on hide mode', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('disabled')).toEqual( + true + ); + }); +}); diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx b/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx new file mode 100644 index 000000000..452a75400 --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx @@ -0,0 +1,159 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiButtonGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { Position } from '@elastic/charts'; +import { ToolbarPopover } from '../shared_components'; + +export interface LegendSettingsPopoverProps { + /** + * Determines the legend display options + */ + legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide' | 'default'; label: string }>; + /** + * Determines the legend mode + */ + mode: 'default' | 'show' | 'hide' | 'auto'; + /** + * Callback on display option change + */ + onDisplayChange: (id: string) => void; + /** + * Sets the legend position + */ + position?: Position; + /** + * Callback on position option change + */ + onPositionChange: (id: string) => void; + /** + * If true, nested legend switch is rendered + */ + renderNestedLegendSwitch?: boolean; + /** + * nested legend switch status + */ + nestedLegend?: boolean; + /** + * Callback on nested switch status change + */ + onNestedLegendChange?: (event: EuiSwitchEvent) => void; +} + +const toggleButtonsIcons = [ + { + id: Position.Bottom, + label: i18n.translate('xpack.lens.shared.legendPositionBottom', { + defaultMessage: 'Bottom', + }), + iconType: 'arrowDown', + }, + { + id: Position.Left, + label: i18n.translate('xpack.lens.shared.legendPositionLeft', { + defaultMessage: 'Left', + }), + iconType: 'arrowLeft', + }, + { + id: Position.Right, + label: i18n.translate('xpack.lens.shared.legendPositionRight', { + defaultMessage: 'Right', + }), + iconType: 'arrowRight', + }, + { + id: Position.Top, + label: i18n.translate('xpack.lens.shared.legendPositionTop', { + defaultMessage: 'Top', + }), + iconType: 'arrowUp', + }, +]; + +export const LegendSettingsPopover: React.FunctionComponent = ({ + legendOptions, + mode, + onDisplayChange, + position, + onPositionChange, + renderNestedLegendSwitch, + nestedLegend, + onNestedLegendChange = () => {}, +}) => { + return ( + + + value === mode)!.id} + onChange={onDisplayChange} + /> + + + + + {renderNestedLegendSwitch && ( + + + + )} + + ); +}; diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.scss b/public/components/explorer/visualizations/shared_components/toolbar_button.scss new file mode 100644 index 000000000..61b02f476 --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/toolbar_button.scss @@ -0,0 +1,60 @@ +.lnsToolbarButton { + line-height: $euiButtonHeight; // Keeps alignment of text and chart icon + background-color: $euiColorEmptyShade; + + // Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed + min-width: 0; + + &[class*='--text'] { + // Lighten the border color for all states + border-color: $euiBorderColor !important; // sass-lint:disable-line no-important + } + + &[class*='isDisabled'] { + // There is a popover `pointer-events: none` that messes with the not-allowed cursor + pointer-events: initial; + } + + .lnsToolbarButton__text > svg { + margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType` + } + + .lnsToolbarButton__text:empty { + margin: 0; + } + + // Toolbar buttons don't look good with centered text when fullWidth + &[class*='fullWidth'] { + text-align: left; + + .lnsToolbarButton__content { + justify-content: space-between; + } + } + +} + +.lnsToolbarButton--groupLeft { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.lnsToolbarButton--groupCenter { + border-radius: 0; + border-left: none; +} + +.lnsToolbarButton--groupRight { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: none; +} + +.lnsToolbarButton--bold { + font-weight: $euiFontWeightBold; +} + +.lnsToolbarButton--s { + box-shadow: none !important; // sass-lint:disable-line no-important + font-size: $euiFontSizeS; +} diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx new file mode 100644 index 000000000..2ba227e6f --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx @@ -0,0 +1,74 @@ +/* + * 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 './toolbar_button.scss'; +import React from 'react'; +import classNames from 'classnames'; +import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui'; + +const groupPositionToClassMap = { + none: null, + left: 'lnsToolbarButton--groupLeft', + center: 'lnsToolbarButton--groupCenter', + right: 'lnsToolbarButton--groupRight', +}; + +export type ToolbarButtonProps = PropsOf & { + /** + * Determines prominence + */ + fontWeight?: 'normal' | 'bold'; + /** + * Smaller buttons also remove extra shadow for less prominence + */ + size?: EuiButtonProps['size']; + /** + * Determines if the button will have a down arrow or not + */ + hasArrow?: boolean; + /** + * Adjusts the borders for groupings + */ + groupPosition?: 'none' | 'left' | 'center' | 'right'; + dataTestSubj?: string; +}; + +export const ToolbarButton: React.FunctionComponent = ({ + children, + className, + fontWeight = 'normal', + size = 'm', + hasArrow = true, + groupPosition = 'none', + dataTestSubj = '', + ...rest +}) => { + const classes = classNames( + 'lnsToolbarButton', + groupPositionToClassMap[groupPosition], + [`lnsToolbarButton--${fontWeight}`, `lnsToolbarButton--${size}`], + className + ); + return ( + + {children} + + ); +}; diff --git a/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx b/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx new file mode 100644 index 000000000..679f3a44b --- /dev/null +++ b/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx @@ -0,0 +1,81 @@ +/* + * 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, { useState } from 'react'; +import { EuiFlexItem, EuiPopover, EuiIcon, EuiPopoverTitle, IconType } from '@elastic/eui'; +import { ToolbarButton, ToolbarButtonProps } from './toolbar_button'; +import { EuiIconLegend } from '../assets/legend'; + +const typeToIconMap: { [type: string]: string | IconType } = { + legend: EuiIconLegend as IconType, + values: 'visText', +}; + +export interface ToolbarPopoverProps { + /** + * Determines popover title + */ + title: string; + /** + * Determines the button icon + */ + type: 'legend' | 'values' | IconType; + /** + * Determines if the popover is disabled + */ + isDisabled?: boolean; + /** + * Button group position + */ + groupPosition?: ToolbarButtonProps['groupPosition']; + buttonDataTestSubj?: string; +} + +export const ToolbarPopover: React.FunctionComponent = ({ + children, + title, + type, + isDisabled = false, + groupPosition, + buttonDataTestSubj, +}) => { + const [open, setOpen] = useState(false); + + const iconType: string | IconType = typeof type === 'string' ? typeToIconMap[type] : type; + + return ( + + { + setOpen(!open); + }} + title={title} + hasArrow={false} + isDisabled={isDisabled} + groupPosition={groupPosition} + dataTestSubj={buttonDataTestSubj} + > + + + } + isOpen={open} + closePopover={() => { + setOpen(false); + }} + anchorPosition="downRight" + > + {title} + {children} + + + ); +}; diff --git a/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx b/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx new file mode 100644 index 000000000..356db5471 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx @@ -0,0 +1,145 @@ +import './chart_switch.scss'; + +import React, { useState } from 'react'; +import { uniqueId } from 'lodash'; +import { i18n } from '@osd/i18n'; +// import { FormattedMessage } from '@osd/i18n/react'; +import { + EuiPopover, + EuiPopoverTitle, + EuiFlexGroup, + EuiFlexItem, + EuiKeyPadMenu, + EuiKeyPadMenuItem, + EuiIcon, + // EuiSelectableMessage +} from '@elastic/eui'; +import { ToolbarButton } from '../shared_components/toolbar_button'; +// import { LensIconChartBar } from '../assets/chart_bar'; +// import { LensIconChartLine } from '../assets/chart_line'; + +function VisualizationSummary(vis: any) { + // const visualization = props.visualizationMap[props.visualizationId || '']; + + // if (!visualization) { + // return ( + // <> + // {i18n.translate('xpack.lens.configPanel.selectVisualization', { + // defaultMessage: 'Select a visualization', + // })} + // + // ); + // } + + // const description = visualization.getDescription(props.visualizationState); + + return ( + <> + + + { vis.label } + + ); +} + +export const ChartSwitch = ({ + setVis, + vis, + visualizationTypes +}: any) => { + + const [flyoutOpen, setFlyoutOpen] = useState(false); + // const [vis, setVis] = useState(visualizationTypes[0]); + + const popoverWrappedSwitch = ( + setFlyoutOpen(!flyoutOpen)} + data-test-subj="lnsChartSwitchPopover" + fontWeight="bold" + > + + + } + isOpen={flyoutOpen} + closePopover={() => setFlyoutOpen(false)} + anchorPosition="downLeft" + > + + + + {i18n.translate('xpack.lens.configPanel.chartType', { + defaultMessage: 'Chart type', + })} + + {/* + setSearchTerm(e.target.value)} + /> + */} + + + + {(visualizationTypes || []).map((v) => ( + {v.label}} + title={v.fullLabel} + role="menuitem" + data-test-subj={`lnsChartSwitchPopover_${v.id}`} + // onClick={() => commitSelection(v.selection)} + onClick={() => { + setVis(v); + setFlyoutOpen(false); + }} + betaBadgeLabel={ + v.selection.dataLoss !== 'nothing' + ? i18n.translate('xpack.lens.chartSwitch.dataLossLabel', { + defaultMessage: 'Data loss', + }) + : undefined + } + betaBadgeTooltipContent={ + v.selection.dataLoss !== 'nothing' + ? i18n.translate('xpack.lens.chartSwitch.dataLossDescription', { + defaultMessage: 'Switching to this chart will lose some of the configuration', + }) + : undefined + } + betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined} + > + + + ))} + + {/* {searchTerm && (visualizationTypes || []).length === 0 && ( + + {searchTerm}, + }} + /> + + )} */} + + ); + + return ( +
+ { popoverWrappedSwitch } +
+ ); +}; \ No newline at end of file diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.scss b/public/components/explorer/visualizations/workspace_panel/chart_switch.scss new file mode 100644 index 000000000..e0031d051 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/chart_switch.scss @@ -0,0 +1,22 @@ +.lnsChartSwitch__header { + > * { + display: flex; + align-items: center; + } +} + +.lnsChartSwitch__summaryIcon { + margin-right: $euiSizeS; + transform: translateY(-1px); + color: $euiTextSubduedColor; +} + +// Targeting img as this won't target normal EuiIcon's only the custom svgs's +img.lnsChartSwitch__chartIcon { // sass-lint:disable-line no-qualifying-elements + // The large icons aren't square so max out the width to fill the height + width: 100%; +} + +.lnsChartSwitch__search { + width: 4 * $euiSizeXXL; +} \ No newline at end of file diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx b/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx new file mode 100644 index 000000000..c78de9d14 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx @@ -0,0 +1,649 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, +} from '../../mocks'; +import { EuiKeyPadMenuItem } from '@elastic/eui'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; +import { Action } from '../state_management'; +import { ChartSwitch } from './chart_switch'; + +describe('chart_switch', () => { + function generateVisualization(id: string): jest.Mocked { + return { + ...createMockVisualization(), + id, + getVisualizationTypeId: jest.fn((_state) => id), + visualizationTypes: [ + { + icon: 'empty', + id, + label: `Label ${id}`, + }, + ], + initialize: jest.fn((_frame, state?: unknown) => { + return state || `${id} initial state`; + }), + getSuggestions: jest.fn((options) => { + return [ + { + score: 1, + title: '', + state: `suggestion ${id}`, + previewIcon: 'empty', + }, + ]; + }), + }; + } + + /** + * There are three visualizations. Each one has the same suggestion behavior: + * + * visA: suggests an empty state + * visB: suggests an empty state + * visC: + * - Never switches to subvisC2 + * - Allows a switch to subvisC3 + * - Allows a switch to subvisC1 + */ + function mockVisualizations() { + return { + visA: generateVisualization('visA'), + visB: generateVisualization('visB'), + visC: { + ...generateVisualization('visC'), + initialize: jest.fn((_frame, state) => state ?? { type: 'subvisC1' }), + visualizationTypes: [ + { + icon: 'empty', + id: 'subvisC1', + label: 'C1', + }, + { + icon: 'empty', + id: 'subvisC2', + label: 'C2', + }, + { + icon: 'empty', + id: 'subvisC3', + label: 'C3', + }, + ], + getVisualizationTypeId: jest.fn((state) => state.type), + getSuggestions: jest.fn((options) => { + if (options.subVisualizationId === 'subvisC2') { + return []; + } + // Multiple suggestions need to be filtered + return [ + { + score: 1, + title: 'Primary suggestion', + state: { type: 'subvisC3' }, + previewIcon: 'empty', + }, + { + score: 1, + title: '', + state: { type: 'subvisC1', notPrimary: true }, + previewIcon: 'empty', + }, + ]; + }), + }, + }; + } + + function mockFrame(layers: string[]) { + return { + ...createMockFramePublicAPI(), + datasourceLayers: layers.reduce( + (acc, layerId) => ({ + ...acc, + [layerId]: ({ + getTableSpec: jest.fn(() => { + return [{ columnId: 2 }]; + }), + getOperationForColumnId() { + return {}; + }, + } as unknown) as DatasourcePublicAPI, + }), + {} as Record + ), + } as FramePublicAPI; + } + + function mockDatasourceMap() { + const datasource = createMockDatasource('testDatasource'); + datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a'], + }, + ]); + return { + testDatasource: datasource, + }; + } + + function mockDatasourceStates() { + return { + testDatasource: { + state: {}, + isLoading: false, + }, + }; + } + + function showFlyout(component: ReactWrapper) { + component.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click'); + } + + function switchTo(subType: string, component: ReactWrapper) { + showFlyout(component); + component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click'); + } + + function getMenuItem(subType: string, component: ReactWrapper) { + showFlyout(component); + return component + .find(EuiKeyPadMenuItem) + .find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`) + .first(); + } + + it('should use suggested state if there is a suggestion from the target visualization', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const component = mount( + + ); + + switchTo('visB', component); + + expect(dispatch).toHaveBeenCalledWith({ + initialState: 'suggestion visB', + newVisualizationId: 'visB', + type: 'SWITCH_VISUALIZATION', + datasourceId: 'testDatasource', + datasourceState: {}, + }); + }); + + it('should use initial state if there is no suggestion from the target visualization', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + visualizations.visB.getSuggestions.mockReturnValueOnce([]); + const frame = mockFrame(['a']); + (frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]); + + const component = mount( + + ); + + switchTo('visB', component); + + expect(frame.removeLayers).toHaveBeenCalledWith(['a']); + + expect(dispatch).toHaveBeenCalledWith({ + initialState: 'visB initial state', + newVisualizationId: 'visB', + type: 'SWITCH_VISUALIZATION', + }); + }); + + it('should indicate data loss if not all columns will be used', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a']); + + const datasourceMap = mockDatasourceMap(); + datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [ + { + columnId: 'col1', + operation: { + label: '', + dataType: 'string', + isBucketed: true, + }, + }, + { + columnId: 'col2', + operation: { + label: '', + dataType: 'number', + isBucketed: false, + }, + }, + ], + layerId: 'first', + isMultiRow: true, + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + datasourceMap.testDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'col1' }, + { columnId: 'col2' }, + { columnId: 'col3' }, + ]); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); + }); + + it('should indicate data loss if not all layers will be used', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a', 'b']); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); + }); + + it('should support multi-layer suggestions without data loss', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a', 'b']); + + const datasourceMap = mockDatasourceMap(); + datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: {}, + table: { + columns: [ + { + columnId: 'a', + operation: { + label: '', + dataType: 'string', + isBucketed: true, + }, + }, + ], + isMultiRow: true, + layerId: 'a', + changeType: 'unchanged', + }, + keptLayerIds: ['a', 'b'], + }, + ]); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); + }); + + it('should indicate data loss if no data will be used', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + visualizations.visB.getSuggestions.mockReturnValueOnce([]); + const frame = mockFrame(['a']); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); + }); + + it('should not indicate data loss if there is no data', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + visualizations.visB.getSuggestions.mockReturnValueOnce([]); + const frame = mockFrame(['a']); + (frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]); + + const component = mount( + + ); + + expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); + }); + + it('should not show a warning when the subvisualization is the same', () => { + const dispatch = jest.fn(); + const frame = mockFrame(['a', 'b', 'c']); + const visualizations = mockVisualizations(); + visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); + const switchVisualizationType = jest.fn(() => ({ type: 'subvisC1' })); + + visualizations.visC.switchVisualizationType = switchVisualizationType; + + const component = mount( + + ); + + expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); + }); + + it('should get suggestions when switching subvisualization', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + visualizations.visB.getSuggestions.mockReturnValueOnce([]); + const frame = mockFrame(['a', 'b', 'c']); + + const component = mount( + + ); + + switchTo('visB', component); + + expect(frame.removeLayers).toHaveBeenCalledTimes(1); + expect(frame.removeLayers).toHaveBeenCalledWith(['a', 'b', 'c']); + + expect(visualizations.visB.getSuggestions).toHaveBeenCalledWith( + expect.objectContaining({ + keptLayerIds: ['a'], + }) + ); + + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SWITCH_VISUALIZATION', + initialState: 'visB initial state', + }) + ); + }); + + it('should not remove layers when switching between subtypes', () => { + const dispatch = jest.fn(); + const frame = mockFrame(['a', 'b', 'c']); + const visualizations = mockVisualizations(); + const switchVisualizationType = jest.fn(() => 'switched'); + + visualizations.visC.switchVisualizationType = switchVisualizationType; + + const component = mount( + + ); + + switchTo('subvisC3', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', { type: 'subvisC3' }); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SWITCH_VISUALIZATION', + initialState: 'switched', + }) + ); + expect(frame.removeLayers).not.toHaveBeenCalled(); + }); + + it('should not remove layers and initialize with existing state when switching between subtypes without data', () => { + const dispatch = jest.fn(); + const frame = mockFrame(['a']); + frame.datasourceLayers.a.getTableSpec = jest.fn().mockReturnValue([]); + const visualizations = mockVisualizations(); + visualizations.visC.getSuggestions = jest.fn().mockReturnValue([]); + visualizations.visC.switchVisualizationType = jest.fn(() => 'switched'); + + const component = mount( + + ); + + switchTo('subvisC3', component); + + expect(visualizations.visC.switchVisualizationType).toHaveBeenCalledWith('subvisC3', { + type: 'subvisC1', + }); + expect(frame.removeLayers).not.toHaveBeenCalled(); + }); + + it('should switch to the updated datasource state', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const frame = mockFrame(['a', 'b']); + + const datasourceMap = mockDatasourceMap(); + datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ + { + state: 'testDatasource suggestion', + table: { + columns: [ + { + columnId: 'col1', + operation: { + label: '', + dataType: 'string', + isBucketed: true, + }, + }, + { + columnId: 'col2', + operation: { + label: '', + dataType: 'number', + isBucketed: false, + }, + }, + ], + layerId: 'a', + isMultiRow: true, + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + + const component = mount( + + ); + + switchTo('visB', component); + + expect(dispatch).toHaveBeenCalledWith({ + type: 'SWITCH_VISUALIZATION', + newVisualizationId: 'visB', + datasourceId: 'testDatasource', + datasourceState: 'testDatasource suggestion', + initialState: 'suggestion visB', + } as Action); + }); + + it('should ensure the new visualization has the proper subtype', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const switchVisualizationType = jest.fn( + (visualizationType, state) => `${state} ${visualizationType}` + ); + + visualizations.visB.switchVisualizationType = switchVisualizationType; + + const component = mount( + + ); + + switchTo('visB', component); + + expect(dispatch).toHaveBeenCalledWith({ + initialState: 'suggestion visB visB', + newVisualizationId: 'visB', + type: 'SWITCH_VISUALIZATION', + datasourceId: 'testDatasource', + datasourceState: {}, + }); + }); + + it('should use the suggestion that matches the subtype', () => { + const dispatch = jest.fn(); + const visualizations = mockVisualizations(); + const switchVisualizationType = jest.fn(); + + visualizations.visC.switchVisualizationType = switchVisualizationType; + + const component = mount( + + ); + + switchTo('subvisC1', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC1', { + type: 'subvisC1', + notPrimary: true, + }); + }); + + it('should show all visualization types', () => { + const component = mount( + + ); + + showFlyout(component); + + const allDisplayed = ['visA', 'visB', 'subvisC1', 'subvisC2', 'subvisC3'].every( + (subType) => component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0 + ); + + expect(allDisplayed).toBeTruthy(); + }); +}); diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx b/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx new file mode 100644 index 000000000..dec6fb077 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx @@ -0,0 +1,332 @@ +/* + * 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 './chart_switch.scss'; +import React, { useState, useMemo } from 'react'; +import { + EuiIcon, + EuiPopover, + EuiPopoverTitle, + EuiKeyPadMenu, + EuiKeyPadMenuItem, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiSelectableMessage, +} from '@elastic/eui'; +import { flatten } from 'lodash'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +// import { Visualization, FramePublicAPI, Datasource } from '../../../types'; +// import { Action } from '../state_management'; +// import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; +// import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { ToolbarButton } from '../shared_components'; + +interface VisualizationSelection { + visualizationId: string; + subVisualizationId: string; + getVisualizationState: () => unknown; + keptLayerIds: string[]; + dataLoss: 'nothing' | 'layers' | 'everything' | 'columns'; + datasourceId?: string; + datasourceState?: unknown; + sameDatasources?: boolean; +} + +interface Props { + // dispatch: (action: Action) => void; + // visualizationMap: Record; + visualizationId: string | null; + visualizationState: unknown; + // framePublicAPI: FramePublicAPI; + // datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; +} + +function VisualizationSummary(props: Props) { + // const visualization = props.visualizationMap[props.visualizationId || '']; + const visualization = null; + if (!visualization) { + return ( + <> + {i18n.translate('xpack.lens.configPanel.selectVisualization', { + defaultMessage: 'Select a visualization', + })} + + ); + } + + const description = visualization.getDescription(props.visualizationState); + + return ( + <> + {description.icon && ( + + )} + {description.label} + + ); +} + +export function ChartSwitch(props: Props) { + const [flyoutOpen, setFlyoutOpen] = useState(false); + + const commitSelection = (selection: VisualizationSelection) => { + setFlyoutOpen(false); + + // trackUiEvent(`chart_switch`); + + switchToSuggestion( + props.dispatch, + { + ...selection, + visualizationState: selection.getVisualizationState(), + }, + 'SWITCH_VISUALIZATION' + ); + + if ( + (!selection.datasourceId && !selection.sameDatasources) || + selection.dataLoss === 'everything' + ) { + // props.framePublicAPI.removeLayers(Object.keys(props.framePublicAPI.datasourceLayers)); + } + }; + + function getSelection( + visualizationId: string, + subVisualizationId: string + ): VisualizationSelection { + const newVisualization = props.visualizationMap[visualizationId]; + const switchVisType = + props.visualizationMap[visualizationId].switchVisualizationType || + ((_type: string, initialState: unknown) => initialState); + const layers = Object.entries(props.framePublicAPI.datasourceLayers); + const containsData = layers.some( + ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + ); + // Always show the active visualization as a valid selection + if ( + props.visualizationId === visualizationId && + props.visualizationState && + newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId + ) { + return { + visualizationId, + subVisualizationId, + dataLoss: 'nothing', + keptLayerIds: Object.keys(props.framePublicAPI.datasourceLayers), + getVisualizationState: () => switchVisType(subVisualizationId, props.visualizationState), + sameDatasources: true, + }; + } + + const topSuggestion = getTopSuggestion( + props, + visualizationId, + newVisualization, + subVisualizationId + ); + + let dataLoss: VisualizationSelection['dataLoss']; + + if (!containsData) { + dataLoss = 'nothing'; + } else if (!topSuggestion) { + dataLoss = 'everything'; + } else if (layers.length > 1 && layers.length !== topSuggestion.keptLayerIds.length) { + dataLoss = 'layers'; + } else if (topSuggestion.columns !== layers[0][1].getTableSpec().length) { + dataLoss = 'columns'; + } else { + dataLoss = 'nothing'; + } + + return { + visualizationId, + subVisualizationId, + dataLoss, + getVisualizationState: topSuggestion + ? () => + switchVisType( + subVisualizationId, + newVisualization.initialize(props.framePublicAPI, topSuggestion.visualizationState) + ) + : () => { + return switchVisType( + subVisualizationId, + newVisualization.initialize( + props.framePublicAPI, + props.visualizationId === newVisualization.id ? props.visualizationState : undefined + ) + ); + }, + keptLayerIds: topSuggestion ? topSuggestion.keptLayerIds : [], + datasourceState: topSuggestion ? topSuggestion.datasourceState : undefined, + datasourceId: topSuggestion ? topSuggestion.datasourceId : undefined, + sameDatasources: dataLoss === 'nothing' && props.visualizationId === newVisualization.id, + }; + } + + const [searchTerm, setSearchTerm] = useState(''); + + const visualizationTypes = useMemo( + () => + flyoutOpen && + flatten( + Object.values(props.visualizationMap).map((v) => + v.visualizationTypes.map((t) => ({ + visualizationId: v.id, + ...t, + icon: t.icon, + })) + ) + ) + .filter( + (visualizationType) => + visualizationType.label.toLowerCase().includes(searchTerm.toLowerCase()) || + (visualizationType.fullLabel && + visualizationType.fullLabel.toLowerCase().includes(searchTerm.toLowerCase())) + ) + .map((visualizationType) => ({ + ...visualizationType, + selection: getSelection(visualizationType.visualizationId, visualizationType.id), + })), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + flyoutOpen, + props.visualizationMap, + props.framePublicAPI, + props.visualizationId, + props.visualizationState, + searchTerm, + ] + ); + + const popover = ( + setFlyoutOpen(!flyoutOpen)} + data-test-subj="lnsChartSwitchPopover" + fontWeight="bold" + > + + + } + isOpen={flyoutOpen} + closePopover={() => setFlyoutOpen(false)} + anchorPosition="downLeft" + > + + + + {i18n.translate('xpack.lens.configPanel.chartType', { + defaultMessage: 'Chart type', + })} + + + setSearchTerm(e.target.value)} + /> + + + + + {(visualizationTypes || []).map((v) => ( + {v.label}} + title={v.fullLabel} + role="menuitem" + data-test-subj={`lnsChartSwitchPopover_${v.id}`} + onClick={() => commitSelection(v.selection)} + betaBadgeLabel={ + v.selection.dataLoss !== 'nothing' + ? i18n.translate('xpack.lens.chartSwitch.dataLossLabel', { + defaultMessage: 'Data loss', + }) + : undefined + } + betaBadgeTooltipContent={ + v.selection.dataLoss !== 'nothing' + ? i18n.translate('xpack.lens.chartSwitch.dataLossDescription', { + defaultMessage: 'Switching to this chart will lose some of the configuration', + }) + : undefined + } + betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined} + > + + + ))} + + {searchTerm && (visualizationTypes || []).length === 0 && ( + + {searchTerm}, + }} + /> + + )} + + ); + + return
{popover}
; +} + +function getTopSuggestion( + props: Props, + visualizationId: string, + newVisualization: Visualization, + subVisualizationId?: string +): Suggestion | undefined { + const unfilteredSuggestions = getSuggestions({ + datasourceMap: props.datasourceMap, + datasourceStates: props.datasourceStates, + visualizationMap: { [visualizationId]: newVisualization }, + activeVisualizationId: props.visualizationId, + visualizationState: props.visualizationState, + subVisualizationId, + }); + const suggestions = unfilteredSuggestions.filter((suggestion) => { + // don't use extended versions of current data table on switching between visualizations + // to avoid confusing the user. + return ( + suggestion.changeType !== 'extended' && + newVisualization.getVisualizationTypeId(suggestion.visualizationState) === subVisualizationId + ); + }); + + // We prefer unchanged or reduced suggestions when switching + // charts since that allows you to switch from A to B and back + // to A with the greatest chance of preserving your original state. + return ( + suggestions.find((s) => s.changeType === 'unchanged') || + suggestions.find((s) => s.changeType === 'reduced') || + suggestions[0] + ); +} diff --git a/public/components/explorer/visualizations/workspace_panel/index.ts b/public/components/explorer/visualizations/workspace_panel/index.ts new file mode 100644 index 000000000..d23afd412 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { WorkspacePanel } from './workspace_panel'; diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx new file mode 100644 index 000000000..82205e930 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx @@ -0,0 +1,811 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; +import { FramePublicAPI, TableSuggestion, Visualization } from '../../../types'; +import { + createMockVisualization, + createMockDatasource, + createExpressionRendererMock, + DatasourceMock, + createMockFramePublicAPI, +} from '../../mocks'; + +jest.mock('../../../debounced_component', () => { + return { + debouncedComponent: (fn: unknown) => fn, + }; +}); + +import { WorkspacePanel, WorkspacePanelProps } from './workspace_panel'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; +import { Ast } from '@kbn/interpreter/common'; +import { coreMock } from 'src/core/public/mocks'; +import { + DataPublicPluginStart, + esFilters, + IFieldType, + IIndexPattern, +} from '../../../../../../../src/plugins/data/public'; +import { TriggerId, UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; +import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; + +describe('workspace_panel', () => { + let mockVisualization: jest.Mocked; + let mockVisualization2: jest.Mocked; + let mockDatasource: DatasourceMock; + + let expressionRendererMock: jest.Mock; + let uiActionsMock: jest.Mocked; + let dataMock: jest.Mocked; + let trigger: jest.Mocked>; + + let instance: ReactWrapper; + + beforeEach(() => { + trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; + uiActionsMock = uiActionsPluginMock.createStartContract(); + dataMock = dataPluginMock.createStartContract(); + uiActionsMock.getTrigger.mockReturnValue(trigger); + mockVisualization = createMockVisualization(); + mockVisualization2 = createMockVisualization(); + + mockDatasource = createMockDatasource('a'); + + expressionRendererMock = createExpressionRendererMock(); + }); + + afterEach(() => { + instance.unmount(); + }); + + it('should render an explanatory text if no visualization is active', () => { + instance = mount( + {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should render an explanatory text if the visualization does not produce an expression', () => { + instance = mount( + null }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should render an explanatory text if the datasource does not produce an expression', () => { + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should render the resulting expression using the expression renderer', () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('should execute a trigger on expression event', () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; + + const eventData = {}; + onEvent({ name: 'brush', data: eventData }); + + expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); + expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); + }); + + it('should include data fetching for each layer in the expression', () => { + const mockDatasource2 = createMockDatasource('a'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + second: mockDatasource2.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + mockDatasource2.toExpression.mockReturnValue('datasource2'); + mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect( + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds + ).toEqual(['first', 'second', 'third']); + expect( + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables + ).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('should run the expression again if the date range changes', async () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.getLayers.mockReturnValue(['first']); + + mockDatasource.toExpression + .mockReturnValueOnce('datasource') + .mockReturnValueOnce('datasource second'); + + expressionRendererMock = jest.fn((_arg) => ); + + await act(async () => { + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + }); + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + + await act(async () => { + instance.setProps({ + framePublicAPI: { + ...framePublicAPI, + dateRange: { fromDate: 'now-90d', toDate: 'now-30d' }, + }, + }); + }); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(2); + }); + + it('should run the expression again if the filters change', async () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.getLayers.mockReturnValue(['first']); + + mockDatasource.toExpression + .mockReturnValueOnce('datasource') + .mockReturnValueOnce('datasource second'); + + expressionRendererMock = jest.fn((_arg) => ); + await act(async () => { + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + }); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + + await act(async () => { + instance.setProps({ + framePublicAPI: { + ...framePublicAPI, + filters: [esFilters.buildExistsFilter(field, indexPattern)], + }, + }); + }); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(2); + }); + + it('should show an error message if the expression fails to parse', () => { + mockDatasource.toExpression.mockReturnValue('|||'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should not attempt to run the expression again if it does not change', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + await act(async () => { + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + }); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + }); + + it('should attempt to run the expression again if it changes', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + await act(async () => { + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + }); + + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + + expressionRendererMock.mockImplementation((_) => { + return ; + }); + + instance.setProps({ visualizationState: {} }); + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(2); + + expect(instance.find(expressionRendererMock)).toHaveLength(1); + }); + + describe('suggestions from dropping in workspace panel', () => { + let mockDispatch: jest.Mock; + let frame: jest.Mocked; + + const draggedField: unknown = {}; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDispatch = jest.fn(); + }); + + function initComponent(draggingContext: unknown = draggedField) { + instance = mount( + {}}> + + + ); + } + + it('should immediately transition if exactly one suggestion is returned', () => { + const expectedTable: TableSuggestion = { + isMultiRow: true, + layerId: '1', + columns: [], + changeType: 'unchanged', + }; + mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ + { + state: {}, + table: expectedTable, + keptLayerIds: [], + }, + ]); + mockVisualization.getSuggestions.mockReturnValueOnce([ + { + score: 0.5, + title: 'my title', + state: {}, + previewIcon: 'empty', + }, + ]); + initComponent(); + + instance.find(DragDrop).prop('onDrop')!(draggedField); + + expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1); + expect(mockVisualization.getSuggestions).toHaveBeenCalledWith( + expect.objectContaining({ + table: expectedTable, + }) + ); + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'SWITCH_VISUALIZATION', + newVisualizationId: 'vis', + initialState: {}, + datasourceState: {}, + datasourceId: 'mock', + }); + }); + + it('should allow to drop if there are suggestions', () => { + mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ + { + state: {}, + table: { + isMultiRow: true, + layerId: '1', + columns: [], + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + mockVisualization.getSuggestions.mockReturnValueOnce([ + { + score: 0.5, + title: 'my title', + state: {}, + previewIcon: 'empty', + }, + ]); + initComponent(); + expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); + }); + + it('should refuse to drop if there only suggestions from other visualizations if there are data tables', () => { + frame.datasourceLayers.a = mockDatasource.publicAPIMock; + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); + mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ + { + state: {}, + table: { + isMultiRow: true, + layerId: '1', + columns: [], + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + mockVisualization2.getSuggestions.mockReturnValueOnce([ + { + score: 0.5, + title: 'my title', + state: {}, + previewIcon: 'empty', + }, + ]); + initComponent(); + expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); + }); + + it('should allow to drop if there are suggestions from active visualization even if there are data tables', () => { + frame.datasourceLayers.a = mockDatasource.publicAPIMock; + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); + mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ + { + state: {}, + table: { + isMultiRow: true, + layerId: '1', + columns: [], + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + mockVisualization.getSuggestions.mockReturnValueOnce([ + { + score: 0.5, + title: 'my title', + state: {}, + previewIcon: 'empty', + }, + ]); + initComponent(); + expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); + }); + + it('should refuse to drop if there are no suggestions', () => { + initComponent(); + expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); + }); + + it('should immediately transition to the first suggestion if there are multiple', () => { + mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ + { + state: {}, + table: { + isMultiRow: true, + columns: [], + layerId: '1', + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + { + state: {}, + table: { + isMultiRow: true, + columns: [], + layerId: '1', + changeType: 'unchanged', + }, + keptLayerIds: [], + }, + ]); + mockVisualization.getSuggestions.mockReturnValueOnce([ + { + score: 0.5, + title: 'second suggestion', + state: {}, + previewIcon: 'empty', + }, + ]); + mockVisualization.getSuggestions.mockReturnValueOnce([ + { + score: 0.8, + title: 'first suggestion', + state: { + isFirst: true, + }, + previewIcon: 'empty', + }, + ]); + + initComponent(); + instance.find(DragDrop).prop('onDrop')!(draggedField); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'SWITCH_VISUALIZATION', + newVisualizationId: 'vis', + initialState: { + isFirst: true, + }, + datasourceState: {}, + datasourceId: 'mock', + }); + }); + }); +}); diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx new file mode 100644 index 000000000..3eb32b2ff --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx @@ -0,0 +1,338 @@ +/* + * 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, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; +import { uniqueId } from 'lodash'; +import classNames from 'classnames'; +import { FormattedMessage } from '@osd/i18n/react'; +import { Ast } from '@osd/interpreter/common'; +import { i18n } from '@osd/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +// import { CoreStart, CoreSetup } from 'kibana/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; +import { + ExpressionRendererEvent, + ExpressionRenderError, + ReactExpressionRendererType, +} from '../../../../../../../src/plugins/expressions/public'; +// import { Action } from '../state_management'; +// import { +// Datasource, +// Visualization, +// FramePublicAPI, +// isLensBrushEvent, +// isLensFilterEvent, +// } from '../../../types'; +import { DragDrop, DragContext } from '../drag_drop'; +// import { getSuggestions, switchToSuggestion } from '../suggestion_helpers'; +// import { buildExpression } from '../expression_helpers'; +import { debouncedComponent } from '../../../common/debounced_component'; +// import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { + UiActionsStart, + VisualizeFieldContext, +} from '../../../../../../../src/plugins/ui_actions/public'; +// import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; +import { + DataPublicPluginStart, + TimefilterContract, +} from '../../../../../../../src/plugins/data/public'; +import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; +// import { DropIllustration } from '../../../assets/drop_illustration'; +// import { getOriginalRequestErrorMessage } from '../../error_helper'; +import { Bar } from '../../../visualizations/visualization/bar'; +import { Line } from '../../../visualizations/visualization/line'; +import { LensIconChartBar } from '../assets/chart_bar'; +import { LensIconChartLine } from '../assets/chart_line'; +// import { vis } from 'src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks'; + +const visualizationTypes = [ + { + id: 'bar', + label: 'Bar', + fullLabel: 'Bar', + icon: LensIconChartBar, + visualizationId: uniqueId('vis-bar-'), + selection: { + dataLoss: 'nothing' + }, + chart: Bar + }, + { + id: 'line', + label: 'Line', + fullLabel: 'Line', + icon: LensIconChartLine, + visualizationId: uniqueId('vis-line-'), + selection: { + dataLoss: 'nothing' + }, + chart: Line + } +]; + +export interface WorkspacePanelProps { + activeVisualizationId: string | null; + // visualizationMap: Record; + visualizationState: unknown; + activeDatasourceId: string | null; + // datasourceMap: Record; + datasourceStates: Record< + string, + { + state: unknown; + isLoading: boolean; + } + >; + // framePublicAPI: FramePublicAPI; + // dispatch: (action: Action) => void; + ExpressionRenderer: ReactExpressionRendererType; + // core: CoreStart | CoreSetup; + plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; + title?: string; + visualizeTriggerFieldContext?: VisualizeFieldContext; +} + +interface WorkspaceState { + expressionBuildError: string | undefined; + expandError: boolean; +} + +// Exported for testing purposes only. +export function WorkspacePanel({ + // activeDatasourceId, + // activeVisualizationId, + // visualizationMap, + // visualizationState, + // datasourceMap, + // datasourceStates, + // framePublicAPI, + // dispatch, + // core, + // plugins, + ExpressionRenderer: ExpressionRendererComponent, + title, + // visualizeTriggerFieldContext, +}: WorkspacePanelProps) { + const dragDropContext = useContext(DragContext); + + const [vis, setVis] = useState(visualizationTypes[0]); + + console.log('outer vis: ', vis); + + function onDrop() { + // if (suggestionForDraggedField) { + // trackUiEvent('drop_onto_workspace'); + // trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); + // switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); + // } + } + + function renderEmptyWorkspace() { + return ( + +

+ + {true + ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { + // defaultMessage: 'Drop some fields here to start', + defaultMessage: 'Use PPL stats commandin query to render visualization', + }) + : i18n.translate('xpack.lens.editorFrame.emptyWorkspaceSimple', { + defaultMessage: 'Drop field here', + })} + +

+ {/* */} + {true === null && ( + <> +

+ {i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { + defaultMessage: 'Lens is a new tool for creating visualization', + })} +

+

+ + + {i18n.translate('xpack.lens.editorFrame.goToForums', { + defaultMessage: 'Make requests and give feedback', + })} + + +

+ + )} +
+ ); + } + + function renderVisualization() { + // we don't want to render the emptyWorkspace on visualizing field from Discover + // as it is specific for the drag and drop functionality and can confuse the users + // return renderEmptyWorkspace(); + // if (expression === null && !visualizeTriggerFieldContext) { + // return renderEmptyWorkspace(); + // } + // return ( + // + // ); + // console.log('vis: ', vis); + // return ; + return vis.chart(); + } + + return ( + {}} + emptyExpression={true} + setVis={ setVis } + vis={ vis } + visualizationTypes={ visualizationTypes } + // visualizationState={visualizationState} + // visualizationId={activeVisualizationId} + // datasourceStates={datasourceStates} + // datasourceMap={datasourceMap} + // visualizationMap={visualizationMap} + > + +
+ {renderVisualization()} + {/* {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()} */} + {/* {renderEmptyWorkspace()} */} +
+
+
+ ); +} + +export const InnerVisualizationWrapper = ({ + expression, + framePublicAPI, + timefilter, + onEvent, + setLocalState, + localState, + ExpressionRendererComponent, +}: { + expression: Ast | null | undefined; + // framePublicAPI: FramePublicAPI; + timefilter: TimefilterContract; + onEvent: (event: ExpressionRendererEvent) => void; + setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void; + localState: WorkspaceState; + ExpressionRendererComponent: ReactExpressionRendererType; +}) => { + const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]); + + const context: ExecutionContextSearch = useMemo( + () => ({ + query: framePublicAPI.query, + timeRange: { + from: framePublicAPI.dateRange.fromDate, + to: framePublicAPI.dateRange.toDate, + }, + filters: framePublicAPI.filters, + }), + [ + framePublicAPI.query, + framePublicAPI.dateRange.fromDate, + framePublicAPI.dateRange.toDate, + framePublicAPI.filters, + ] + ); + + if (localState.expressionBuildError) { + return ( + + + + + + + + {localState.expressionBuildError} + + ); + } + return ( +
+ { + // const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; + return ( + + + + + + + + {false ? ( + + { + setLocalState((prevState: WorkspaceState) => ({ + ...prevState, + expandError: !prevState.expandError, + })); + }} + > + {i18n.translate('xpack.lens.editorFrame.expandRenderingErrorButton', { + defaultMessage: 'Show details of error', + })} + + + {/* {localState.expandError ? visibleErrorMessage : null} */} + + ) : null} + + ); + }} + /> +
+ ); +}; + +export const VisualizationWrapper = debouncedComponent(InnerVisualizationWrapper); diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss new file mode 100644 index 000000000..21b7da48f --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss @@ -0,0 +1,128 @@ +@import '../mixins'; + +.lnsWorkspacePanelWrapper { + @include euiScrollBar; + overflow: hidden; + // Override panel size padding + padding: 0 !important; // sass-lint:disable-line no-important + margin-bottom: $euiSize; + display: flex; + flex-direction: column; + position: relative; // For positioning the dnd overlay + min-height: $euiSizeXXL * 10; + + .lnsWorkspacePanelWrapper__pageContentHeader { + @include euiTitle('xs'); + padding: $euiSizeM; + // override EuiPage + margin-bottom: 0 !important; // sass-lint:disable-line no-important + } + + .lnsWorkspacePanelWrapper__pageContentHeader--unsaved { + color: $euiTextSubduedColor; + } + + .lnsWorkspacePanelWrapper__pageContentBody { + @include euiScrollBar; + flex-grow: 1; + display: flex; + align-items: stretch; + justify-content: stretch; + overflow: auto; + + > * { + flex: 1 1 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + } +} + +.lnsWorkspacePanel__dragDrop { + // Disable the coloring of the DnD for this element as we'll + // Color the whole panel instead + background-color: transparent !important; // sass-lint:disable-line no-important + border: none !important; // sass-lint:disable-line no-important +} + +.lnsExpressionRenderer { + .lnsDragDrop-isDropTarget & { + transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out; + filter: blur($euiSizeXS); + opacity: .25; + } +} + +.lnsWorkspacePanel__emptyContent { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + transition: background-color $euiAnimSpeedFast ease-in-out; + + .lnsDragDrop-isDropTarget & { + @include lnsDroppable; + @include lnsDroppableActive; + + p { + transition: filter $euiAnimSpeedFast ease-in-out; + filter: blur(5px); + } + } + + .lnsDragDrop-isActiveDropTarget & { + @include lnsDroppableActiveHover; + + .lnsDropIllustration__hand { + animation: lnsWorkspacePanel__illustrationPulseContinuous 1.5s ease-in-out 0s infinite normal forwards; + } + } +} + +.lnsWorkspacePanelWrapper__toolbar { + margin-bottom: 0; +} + +.lnsDropIllustration__adjustFill { + fill: $euiColorFullShade; +} + +.lnsWorkspacePanel__dropIllustration { + overflow: visible; // Shows arrow animation when it gets out of bounds + margin-top: $euiSizeL; + margin-bottom: $euiSizeXXL; + // Drop shadow values is a dupe of @euiBottomShadowMedium but used as a filter + // Hard-coded px values OK (@cchaos) + // sass-lint:disable-block indentation + filter: + drop-shadow(0 6px 12px transparentize($euiShadowColor, .8)) + drop-shadow(0 4px 4px transparentize($euiShadowColor, .8)) + drop-shadow(0 2px 2px transparentize($euiShadowColor, .8)); +} + +.lnsDropIllustration__hand { + animation: lnsWorkspacePanel__illustrationPulseArrow 5s ease-in-out 0s infinite normal forwards; +} + +@keyframes lnsWorkspacePanel__illustrationPulseArrow { + 0% { transform: translateY(0%); } + 65% { transform: translateY(0%); } + 72% { transform: translateY(10%); } + 79% { transform: translateY(7%); } + 86% { transform: translateY(10%); } + 95% { transform: translateY(0); } +} + +@keyframes lnsWorkspacePanel__illustrationPulseContinuous { + 0% { transform: translateY(10%); } + 25% { transform: translateY(15%); } + 50% { transform: translateY(10%); } + 75% { transform: translateY(15%); } + 100% { transform: translateY(10%); } +} diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx new file mode 100644 index 000000000..f7ae77536 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Visualization } from '../../../types'; +import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../mocks'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper'; + +describe('workspace_panel_wrapper', () => { + let mockVisualization: jest.Mocked; + let mockFrameAPI: FrameMock; + let instance: ReactWrapper; + + beforeEach(() => { + mockVisualization = createMockVisualization(); + mockFrameAPI = createMockFramePublicAPI(); + }); + + afterEach(() => { + instance.unmount(); + }); + + it('should render its children', () => { + const MyChild = () => The child elements; + instance = mount( + + + + ); + + expect(instance.find(MyChild)).toHaveLength(1); + }); + + it('should call the toolbar renderer if provided', () => { + const renderToolbarMock = jest.fn(); + const visState = { internalState: 123 }; + instance = mount( + } + visualizationId="myVis" + visualizationMap={{ myVis: { ...mockVisualization, renderToolbar: renderToolbarMock } }} + datasourceMap={{}} + datasourceStates={{}} + emptyExpression={false} + /> + ); + + expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), { + state: visState, + frame: mockFrameAPI, + setState: expect.anything(), + }); + }); +}); diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx new file mode 100644 index 000000000..8cf2aff26 --- /dev/null +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx @@ -0,0 +1,134 @@ +/* + * 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 './workspace_panel_wrapper.scss'; + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import classNames from 'classnames'; +import { + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +// import { Datasource, FramePublicAPI, Visualization } from '../../../types'; +// import { NativeRenderer } from '../../../native_renderer'; +// import { Action } from '../state_management'; +import { ChartSwitch } from './chartSwitch'; + +export interface WorkspacePanelWrapperProps { + children: React.ReactNode | React.ReactNode[]; + // framePublicAPI: FramePublicAPI; + visualizationState: unknown; + // dispatch: (action: Action) => void; + emptyExpression: boolean; + title?: string; + // visualizationMap: Record; + visualizationId: string | null; + // datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; +} + +export function WorkspacePanelWrapper({ + children, + // framePublicAPI, + // visualizationState, + // dispatch, + title, + emptyExpression, + setVis, + vis, + visualizationTypes + // visualizationId, + // visualizationMap, + // datasourceMap, + // datasourceStates, +}: WorkspacePanelWrapperProps) { + // const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; + const setVisualizationState = useCallback( + (newState: unknown) => { + // if (!activeVisualization) { + // return; + // } + // dispatch({ + // type: 'UPDATE_VISUALIZATION_STATE', + // visualizationId: activeVisualization.id, + // newState, + // clearStagedPreview: false, + // }); + }, + [] + // [dispatch, activeVisualization] + ); + return ( + <> +
+ + + + + {/* {activeVisualization && activeVisualization.renderToolbar && ( + + + + )} */} + +
+ + {(!emptyExpression || title) && ( + + + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + + + )} + + {children} + + + + ); +} diff --git a/public/components/visualizations/plotly/plot_template.tsx b/public/components/visualizations/plotly/plot_template.tsx index 545a03772..31a05edcc 100644 --- a/public/components/visualizations/plotly/plot_template.tsx +++ b/public/components/visualizations/plotly/plot_template.tsx @@ -37,10 +37,10 @@ export function Plt(props: PltProps) { layout={{ autosize: true, margin: { - l: 30, - r: 5, - b: 30, - t: 5, + l: 'auto', + r: 'auto', + b: 'auto', + t: 'auto', pad: 4, }, barmode: 'stack', diff --git a/public/components/visualizations/visualization/bar.tsx b/public/components/visualizations/visualization/bar.tsx new file mode 100644 index 000000000..04c7ea254 --- /dev/null +++ b/public/components/visualizations/visualization/bar.tsx @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import { Plt } from '../plotly/plot_template'; + +export const Bar = (props: any) => { + return ( + + ); +}; \ No newline at end of file diff --git a/public/components/visualizations/visualization/countDistribution.tsx b/public/components/visualizations/visualization/countDistribution.tsx deleted file mode 100644 index 6be960c6b..000000000 --- a/public/components/visualizations/visualization/countDistribution.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import React from 'react'; -import { Plt } from '../plotly/plot_template'; - -export const CountDistribution = (props: any) => { - return ( - - ); -}; \ No newline at end of file diff --git a/public/components/visualizations/visualization/line.tsx b/public/components/visualizations/visualization/line.tsx new file mode 100644 index 000000000..f9de9bb6e --- /dev/null +++ b/public/components/visualizations/visualization/line.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React, { useMemo } from 'react'; +import { Plt } from '../plotly/plot_template'; + +export const Line = (props: any) => { + + return ( + + ); +}; \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index f1854327a..50535eed7 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -26,12 +26,24 @@ import { observabilityTitle, observabilityPluginOrder } from '../common/index'; +// import { +// XYVisualization +// } from './services/visualizations' export class ObservabilityPlugin implements Plugin { + + // private xyVisualization; - constructor(initializerContext: PluginInitializerContext) {} + constructor() { + // this.xyVisualization = new XYVisualization(); + } public setup(core: CoreSetup): ObservabilitySetup { + + /** setup all services **/ + // Visualization setup + // this.xyVisualization.setup(); + core.application.register({ id: observabilityID, title: observabilityTitle, diff --git a/public/requests/ppl.ts b/public/services/requests/ppl.ts similarity index 87% rename from public/requests/ppl.ts rename to public/services/requests/ppl.ts index 0194c3eb9..6db14e29d 100644 --- a/public/requests/ppl.ts +++ b/public/services/requests/ppl.ts @@ -9,11 +9,11 @@ * GitHub history for details. */ -import { CoreStart } from '../../../../src/core/public'; +import { CoreStart } from '../../../../../src/core/public'; import { PPL_BASE, PPL_SEARCH -} from '../../common/index'; +} from '../../../common/index'; export const handlePplRequest = async ( http: CoreStart['http'], diff --git a/public/services/visualizations/index.ts b/public/services/visualizations/index.ts new file mode 100644 index 000000000..6c2d2c9ef --- /dev/null +++ b/public/services/visualizations/index.ts @@ -0,0 +1 @@ +export * from './xyVisualization'; \ No newline at end of file diff --git a/public/services/visualizations/visualizationBase.ts b/public/services/visualizations/visualizationBase.ts new file mode 100644 index 000000000..9183fe76b --- /dev/null +++ b/public/services/visualizations/visualizationBase.ts @@ -0,0 +1,28 @@ +export class VisualizationBase { + + private visId: string; + private category: string; + private visConfig: any = {}; + private types; + + constructor( + visualizationId: string, + category: string, + config?: any, + types?: any + ) { + this.visId = visualizationId; + this.category = category; + this.visConfig = config + this.types = types; + } + + getVisId = () => this.visId; + + getCategory = () => this.category; + + getVisConfig = () => this.visConfig; + + getTypes = () => this.types; + +} \ No newline at end of file diff --git a/public/services/visualizations/xyVisualization.ts b/public/services/visualizations/xyVisualization.ts new file mode 100644 index 000000000..d1940333c --- /dev/null +++ b/public/services/visualizations/xyVisualization.ts @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { VisualizationBase } from './visualizationBase'; + +export class XYVisualization extends VisualizationBase { + constructor( + visualizationId: string, + category: string, + configuration?: any, + types?:any + ) { + super( + visualizationId, + category, + configuration, + types + ); + } + + setup = () => {}; +} \ No newline at end of file From e4958a924353af60d5ed6de0ae8744ea9a745fb5 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 22 Jul 2021 23:25:23 -0700 Subject: [PATCH 04/16] added config panel for vis --- public/components/common/seach/search.tsx | 1 + public/components/explorer/explorer.tsx | 3 +- public/components/explorer/logExplorer.tsx | 1 + .../config_panel/configPanelItem.tsx | 61 ++++++++ .../config_panel/config_panel.tsx | 148 +++++++++--------- .../explorer/visualizations/index.tsx | 7 + .../shared_components/toolbar_button.tsx | 2 +- 7 files changed, 143 insertions(+), 80 deletions(-) create mode 100644 public/components/explorer/visualizations/config_panel/configPanelItem.tsx diff --git a/public/components/common/seach/search.tsx b/public/components/common/seach/search.tsx index 1403fb512..4a11e6c84 100644 --- a/public/components/common/seach/search.tsx +++ b/public/components/common/seach/search.tsx @@ -82,6 +82,7 @@ export const Search = (props: any) => { actionItems.map((item) => { return ( { return ( ); }; diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index 1ffc35686..fe713f1f9 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -151,6 +151,7 @@ export const LogExplorer = ({ const handleQuerySearch = async (tabId: string) => { const latestQueries = curQueriesRef.current; const res = await handlePplRequest(http, { query: latestQueries[tabId][RAW_QUERY].trim() }); + console.log('res: ', res); setQueryResults(staleQueryResults => { return { ...staleQueryResults, diff --git a/public/components/explorer/visualizations/config_panel/configPanelItem.tsx b/public/components/explorer/visualizations/config_panel/configPanelItem.tsx new file mode 100644 index 000000000..212bf2481 --- /dev/null +++ b/public/components/explorer/visualizations/config_panel/configPanelItem.tsx @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React, { useState } from 'react'; +import { uniqueId} from 'lodash'; +import { + EuiPanel, + EuiTitle, + EuiAccordion, + EuiComboBox, + EuiSpacer +} from '@elastic/eui'; + +export const PanelItem = ({ + paddingTitle, + advancedTitle, + dropdownList, + children +}: any) => { + const options = dropdownList.map((item) => { + return { + label: item.name + }; + } + ); + const [selectedOption, setValue] = useState(options.length !== 0 ? [options[0]] : []); + const handleSelect = (selectedOption) => { + setValue(selectedOption); + } + return ( + + +

{ paddingTitle }

+
+ + { handleSelect(e)} + aria-label="Use aria labels when no actual label is in use" + /> } + + + + { children } + + +
+ ); +}; \ No newline at end of file diff --git a/public/components/explorer/visualizations/config_panel/config_panel.tsx b/public/components/explorer/visualizations/config_panel/config_panel.tsx index 4047772fc..ba5fb07d2 100644 --- a/public/components/explorer/visualizations/config_panel/config_panel.tsx +++ b/public/components/explorer/visualizations/config_panel/config_panel.tsx @@ -6,35 +6,44 @@ import './config_panel.scss'; import React, { useMemo, memo } from 'react'; -import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; +import { uniqueId} from 'lodash'; +import { + EuiForm, + EuiSpacer, + EuiTabbedContent +} from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { PanelItem } from './configPanelItem' // import { Visualization } from '../../../types'; // import { LayerPanel } from './layer_panel'; // import { trackUiEvent } from '../../../lens_ui_telemetry'; // import { generateId } from '../../../id_generator'; // import { removeLayer, appendLayer } from './layer_actions'; import { ConfigPanelWrapperProps } from './types'; +import { ToolbarButton } from '../shared_components/toolbar_button'; export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; - const { visualizationState } = props; + // const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; + // const { visualizationState } = props; - return ( - activeVisualization && - visualizationState && - ); + // return ( + // activeVisualization && + // visualizationState && + // ); + return ; }); function LayerPanels( props: ConfigPanelWrapperProps & { activeDatasourceId: string; - activeVisualization: Visualization; + // activeVisualization: Visualization; } ) { const { // activeVisualization, // visualizationState, dispatch, + queryResults // activeDatasourceId, // datasourceMap, } = props; @@ -87,77 +96,60 @@ function LayerPanels( }, [dispatch] ); - // const layerIds = activeVisualization.getLayerIds(visualizationState); + + const panelItems = [ + { + paddingTitle: 'X-axis', + advancedTitle: 'advanced', + dropdownList: queryResults && queryResults.schema ? queryResults.schema : [] + }, + { + paddingTitle: 'Y-axis', + advancedTitle: 'advanced', + dropdownList: [] + } + ]; + + const ConfigPanelItems = (props) => { + const { + panelItems + } = props; + return ( + + { panelItems.map((item) => { + return ( + <> + + here goes advanced setting + + + + ); + }) } + + ); + } + + const tabs = [ + { + id: 'setting-panel', + name: 'Settings', + content: + } + ]; return ( - - {/* {layerIds.map((layerId, index) => ( - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'REMOVE_OR_CLEAR_LAYER', - updater: (state) => - // removeLayer({ - // activeVisualization, - // layerId, - // trackUiEvent, - // datasourceMap, - // state, - // }), - }); - }} - /> - ))} */} - {true && ( - - - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: (state) => - // appendLayer({ - // activeVisualization, - // generateId, - // trackUiEvent, - // activeDatasource: datasourceMap[activeDatasourceId], - // state, - // }), - }); - }} - iconType="plusInCircleFilled" - /> - - - )} - + ); } diff --git a/public/components/explorer/visualizations/index.tsx b/public/components/explorer/visualizations/index.tsx index 564446016..9cbf4c3a4 100644 --- a/public/components/explorer/visualizations/index.tsx +++ b/public/components/explorer/visualizations/index.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { FrameLayout } from './frameLayout'; import { DataPanel } from './datapanel'; import { WorkspacePanel } from './workspace_panel'; +import { ConfigPanelWrapper } from './config_panel'; // import {} // const VIS_MAPS = { @@ -44,9 +45,15 @@ export const ExplorerVisualizations = (props: any) => { workspacePanel={ {} } + query={ props.query } // visualizationMap={ VIS_MAPS } /> } + configPanel={ + + } /> ); }; \ No newline at end of file diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx index 2ba227e6f..355116db4 100644 --- a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx +++ b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx @@ -40,7 +40,7 @@ export const ToolbarButton: React.FunctionComponent = ({ children, className, fontWeight = 'normal', - size = 'm', + size = 's', hasArrow = true, groupPosition = 'none', dataTestSubj = '', From ab4f6ddb81275f5a01c93d9d4070b43fe64cfe89 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Fri, 23 Jul 2021 11:51:02 -0700 Subject: [PATCH 05/16] removed unused files and for a quick demo --- public/components/explorer/explorer.tsx | 6 +- .../config_panel/config_panel.tsx | 78 +-- .../config_panel/dimension_container.scss | 19 - .../config_panel/dimension_container.tsx | 90 ---- .../config_panel/layer_actions.test.ts | 127 ----- .../config_panel/layer_actions.ts | 90 ---- .../config_panel/layer_panel.scss | 70 --- .../config_panel/layer_panel.test.tsx | 473 ----------------- .../config_panel/layer_panel.tsx | 477 ------------------ .../config_panel/layer_settings.tsx | 64 --- .../visualizations/config_panel/types.ts | 38 -- 11 files changed, 7 insertions(+), 1525 deletions(-) delete mode 100644 public/components/explorer/visualizations/config_panel/dimension_container.scss delete mode 100644 public/components/explorer/visualizations/config_panel/dimension_container.tsx delete mode 100644 public/components/explorer/visualizations/config_panel/layer_actions.test.ts delete mode 100644 public/components/explorer/visualizations/config_panel/layer_actions.ts delete mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.scss delete mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.test.tsx delete mode 100644 public/components/explorer/visualizations/config_panel/layer_panel.tsx delete mode 100644 public/components/explorer/visualizations/config_panel/layer_settings.tsx delete mode 100644 public/components/explorer/visualizations/config_panel/types.ts diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 429b8326f..f22eda24d 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -113,12 +113,12 @@ export const Explorer = (props: IExplorerProps) => { { (props.explorerData && !_.isEmpty(props.explorerData)) ? (
- {} } - /> - + /> */} + {/* */}
- // ); return ; }); function LayerPanels( props: ConfigPanelWrapperProps & { activeDatasourceId: string; - // activeVisualization: Visualization; } ) { - const { - // activeVisualization, - // visualizationState, - dispatch, + const { queryResults - // activeDatasourceId, - // datasourceMap, } = props; - const setVisualizationState = useMemo( - () => (newState: unknown) => { - // dispatch({ - // type: 'UPDATE_VISUALIZATION_STATE', - // visualizationId: activeVisualization.id, - // newState, - // clearStagedPreview: false, - // }); - }, - // [dispatch, activeVisualization] - [] - ); - const updateDatasource = useMemo( - () => (datasourceId: string, newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: () => newState, - datasourceId, - clearStagedPreview: false, - }); - }, - [dispatch] - ); - const updateAll = useMemo( - () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { - dispatch({ - type: 'UPDATE_STATE', - subType: 'UPDATE_ALL_STATES', - updater: (prevState) => { - return { - ...prevState, - datasourceStates: { - ...prevState.datasourceStates, - [datasourceId]: { - state: newDatasourceState, - isLoading: false, - }, - }, - visualization: { - ...prevState.visualization, - state: newVisualizationState, - }, - stagedPreview: undefined, - }; - }, - }); - }, - [dispatch] - ); const panelItems = [ { paddingTitle: 'X-axis', - advancedTitle: 'advanced', + advancedTitle: 'Advanced', dropdownList: queryResults && queryResults.schema ? queryResults.schema : [] }, { paddingTitle: 'Y-axis', - advancedTitle: 'advanced', + advancedTitle: 'Advanced', dropdownList: [] } ]; diff --git a/public/components/explorer/visualizations/config_panel/dimension_container.scss b/public/components/explorer/visualizations/config_panel/dimension_container.scss deleted file mode 100644 index bd2789cf6..000000000 --- a/public/components/explorer/visualizations/config_panel/dimension_container.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import '@elastic/eui/src/components/flyout/variables'; -@import '@elastic/eui/src/components/flyout/mixins'; - -.lnsDimensionContainer { - // Use the EuiFlyout style - @include euiFlyout; - // But with custom positioning to keep it within the sidebar contents - position: absolute; - right: 0; - left: 0; - top: 0; - bottom: 0; - animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance; -} - -.lnsDimensionContainer__footer, -.lnsDimensionContainer__header { - padding: $euiSizeS; -} diff --git a/public/components/explorer/visualizations/config_panel/dimension_container.tsx b/public/components/explorer/visualizations/config_panel/dimension_container.tsx deleted file mode 100644 index 8f1b441d1..000000000 --- a/public/components/explorer/visualizations/config_panel/dimension_container.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 './dimension_container.scss'; - -import React, { useState, useEffect } from 'react'; -import { - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiTitle, - EuiButtonEmpty, - EuiFlexItem, - EuiFocusTrap, - EuiOutsideClickDetector, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -export function DimensionContainer({ - isOpen, - groupLabel, - handleClose, - panel, -}: { - isOpen: boolean; - handleClose: () => void; - panel: React.ReactElement; - groupLabel: string; -}) { - const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false); - - const closeFlyout = () => { - handleClose(); - setFocusTrapIsEnabled(false); - }; - - useEffect(() => { - if (isOpen) { - // without setTimeout here the flyout pushes content when animating - setTimeout(() => { - setFocusTrapIsEnabled(true); - }, 255); - } - }, [isOpen]); - - return isOpen ? ( - - -
- - - - - {i18n.translate('xpack.lens.configure.configurePanelTitle', { - defaultMessage: '{groupLabel} configuration', - values: { - groupLabel, - }, - })} - - - - - - {panel} - - - - {i18n.translate('xpack.lens.dimensionContainer.close', { - defaultMessage: 'Close', - })} - - -
-
-
- ) : null; -} diff --git a/public/components/explorer/visualizations/config_panel/layer_actions.test.ts b/public/components/explorer/visualizations/config_panel/layer_actions.test.ts deleted file mode 100644 index 3363e34a0..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_actions.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { removeLayer, appendLayer } from './layer_actions'; - -function createTestArgs(initialLayerIds: string[]) { - const trackUiEvent = jest.fn(); - const testDatasource = (datasourceId: string) => ({ - id: datasourceId, - clearLayer: (layerIds: unknown, layerId: string) => - (layerIds as string[]).map((id: string) => - id === layerId ? `${datasourceId}_clear_${layerId}` : id - ), - removeLayer: (layerIds: unknown, layerId: string) => - (layerIds as string[]).filter((id: string) => id !== layerId), - insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], - }); - - const activeVisualization = { - clearLayer: (layerIds: unknown, layerId: string) => - (layerIds as string[]).map((id: string) => (id === layerId ? `vis_clear_${layerId}` : id)), - removeLayer: (layerIds: unknown, layerId: string) => - (layerIds as string[]).filter((id: string) => id !== layerId), - getLayerIds: (layerIds: unknown) => layerIds as string[], - appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], - }; - - const datasourceStates = { - ds1: { - isLoading: false, - state: initialLayerIds.slice(0, 1), - }, - ds2: { - isLoading: false, - state: initialLayerIds.slice(1), - }, - }; - - return { - state: { - activeDatasourceId: 'ds1', - datasourceStates, - title: 'foo', - visualization: { - activeId: 'vis1', - state: initialLayerIds, - }, - }, - activeVisualization, - datasourceMap: { - ds1: testDatasource('ds1'), - ds2: testDatasource('ds2'), - }, - trackUiEvent, - stagedPreview: { - visualization: { - activeId: 'vis1', - state: initialLayerIds, - }, - datasourceStates, - }, - }; -} - -describe('removeLayer', () => { - it('should clear the layer if it is the only layer', () => { - const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs(['layer1']); - const newState = removeLayer({ - activeVisualization, - datasourceMap, - layerId: 'layer1', - state, - trackUiEvent, - }); - - expect(newState.visualization.state).toEqual(['vis_clear_layer1']); - expect(newState.datasourceStates.ds1.state).toEqual(['ds1_clear_layer1']); - expect(newState.datasourceStates.ds2.state).toEqual([]); - expect(newState.stagedPreview).not.toBeDefined(); - expect(trackUiEvent).toHaveBeenCalledWith('layer_cleared'); - }); - - it('should remove the layer if it is not the only layer', () => { - const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([ - 'layer1', - 'layer2', - ]); - const newState = removeLayer({ - activeVisualization, - datasourceMap, - layerId: 'layer1', - state, - trackUiEvent, - }); - - expect(newState.visualization.state).toEqual(['layer2']); - expect(newState.datasourceStates.ds1.state).toEqual([]); - expect(newState.datasourceStates.ds2.state).toEqual(['layer2']); - expect(newState.stagedPreview).not.toBeDefined(); - expect(trackUiEvent).toHaveBeenCalledWith('layer_removed'); - }); -}); - -describe('appendLayer', () => { - it('should add the layer to the datasource and visualization', () => { - const { state, trackUiEvent, datasourceMap, activeVisualization } = createTestArgs([ - 'layer1', - 'layer2', - ]); - const newState = appendLayer({ - activeDatasource: datasourceMap.ds1, - activeVisualization, - generateId: () => 'foo', - state, - trackUiEvent, - }); - - expect(newState.visualization.state).toEqual(['layer1', 'layer2', 'foo']); - expect(newState.datasourceStates.ds1.state).toEqual(['layer1', 'foo']); - expect(newState.datasourceStates.ds2.state).toEqual(['layer2']); - expect(newState.stagedPreview).not.toBeDefined(); - expect(trackUiEvent).toHaveBeenCalledWith('layer_added'); - }); -}); diff --git a/public/components/explorer/visualizations/config_panel/layer_actions.ts b/public/components/explorer/visualizations/config_panel/layer_actions.ts deleted file mode 100644 index 131bb6208..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_actions.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { EditorFrameState } from '../state_management'; -import { Datasource, Visualization } from '../../../types'; - -interface RemoveLayerOptions { - trackUiEvent: (name: string) => void; - state: EditorFrameState; - layerId: string; - activeVisualization: Pick; - datasourceMap: Record>; -} - -interface AppendLayerOptions { - trackUiEvent: (name: string) => void; - state: EditorFrameState; - generateId: () => string; - activeDatasource: Pick; - activeVisualization: Pick; -} - -export function removeLayer(opts: RemoveLayerOptions): EditorFrameState { - const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts; - const isOnlyLayer = activeVisualization - .getLayerIds(state.visualization.state) - .every((id) => id === opts.layerId); - - trackUiEvent(isOnlyLayer ? 'layer_cleared' : 'layer_removed'); - - return { - ...state, - datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => { - const datasource = datasourceMap[datasourceId!]; - return { - ...datasourceState, - state: isOnlyLayer - ? datasource.clearLayer(datasourceState.state, layerId) - : datasource.removeLayer(datasourceState.state, layerId), - }; - }), - visualization: { - ...state.visualization, - state: - isOnlyLayer || !activeVisualization.removeLayer - ? activeVisualization.clearLayer(state.visualization.state, layerId) - : activeVisualization.removeLayer(state.visualization.state, layerId), - }, - stagedPreview: undefined, - }; -} - -export function appendLayer({ - trackUiEvent, - activeVisualization, - state, - generateId, - activeDatasource, -}: AppendLayerOptions): EditorFrameState { - trackUiEvent('layer_added'); - - if (!activeVisualization.appendLayer) { - return state; - } - - const layerId = generateId(); - - return { - ...state, - datasourceStates: { - ...state.datasourceStates, - [activeDatasource.id]: { - ...state.datasourceStates[activeDatasource.id], - state: activeDatasource.insertLayer( - state.datasourceStates[activeDatasource.id].state, - layerId - ), - }, - }, - visualization: { - ...state.visualization, - state: activeVisualization.appendLayer(state.visualization.state, layerId), - }, - stagedPreview: undefined, - }; -} diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.scss b/public/components/explorer/visualizations/config_panel/layer_panel.scss deleted file mode 100644 index 54c922957..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_panel.scss +++ /dev/null @@ -1,70 +0,0 @@ -.lnsLayerPanel { - margin-bottom: $euiSizeS; -} - -.lnsLayerPanel__sourceFlexItem { - max-width: calc(100% - #{$euiSize * 3.625}); -} - -.lnsLayerPanel__settingsFlexItem:empty + .lnsLayerPanel__sourceFlexItem { - max-width: calc(100% - #{$euiSizeS}); -} - -.lnsLayerPanel__settingsFlexItem:empty { - margin: 0; -} - -.lnsLayerPanel__row { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsLayerPanel__dimension { - @include euiFontSizeS; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; - width: 100%; - min-height: $euiSizeXXL; - - // NativeRenderer is messing this up - > div { - flex-grow: 1; - } - - &:focus, - &:focus-within { - @include euiFocusRing; - } -} - -.lnsLayerPanel__triggerLink { - width: 100%; - padding: $euiSizeS; - min-height: $euiSizeXXL - 2; - word-break: break-word; - - &:focus { - background-color: transparent !important; // sass-lint:disable-line no-important - outline: none !important; // sass-lint:disable-line no-important - } -} - -.lnsLayerPanel__triggerLinkContent { - // Make EUI button content not centered - justify-content: flex-start; - padding: 0 !important; // sass-lint:disable-line no-important - color: $euiTextSubduedColor; -} - -.lnsLayerPanel__styleEditor { - padding: 0 $euiSizeS $euiSizeS; -} diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx b/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx deleted file mode 100644 index 44dc22d20..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_panel.test.tsx +++ /dev/null @@ -1,473 +0,0 @@ -/* - * 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 { act } from 'react-dom/test-utils'; -import { - createMockVisualization, - createMockFramePublicAPI, - createMockDatasource, - DatasourceMock, -} from '../../mocks'; -import { ChildDragDropProvider } from '../../../drag_drop'; -import { EuiFormRow } from '@elastic/eui'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Visualization } from '../../../types'; -import { LayerPanel } from './layer_panel'; -import { coreMock } from 'src/core/public/mocks'; -import { generateId } from '../../../id_generator'; - -jest.mock('../../../id_generator'); - -describe('LayerPanel', () => { - let mockVisualization: jest.Mocked; - let mockVisualization2: jest.Mocked; - let mockDatasource: DatasourceMock; - - function getDefaultProps() { - const frame = createMockFramePublicAPI(); - frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - return { - layerId: 'first', - activeVisualizationId: 'vis1', - visualizationMap: { - vis1: mockVisualization, - vis2: mockVisualization2, - }, - activeDatasourceId: 'ds1', - datasourceMap: { - ds1: mockDatasource, - }, - datasourceStates: { - ds1: { - isLoading: false, - state: 'state', - }, - }, - visualizationState: 'state', - updateVisualization: jest.fn(), - updateDatasource: jest.fn(), - updateAll: jest.fn(), - framePublicAPI: frame, - isOnlyLayer: true, - onRemoveLayer: jest.fn(), - dispatch: jest.fn(), - core: coreMock.createStart(), - dataTestSubj: 'lns_layerPanel-0', - }; - } - - beforeEach(() => { - mockVisualization = { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - }, - ], - }; - - mockVisualization2 = { - ...createMockVisualization(), - id: 'testVis2', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis2', - label: 'TEST2', - }, - ], - }; - - mockVisualization.getLayerIds.mockReturnValue(['first']); - mockDatasource = createMockDatasource('ds1'); - }); - - it('should fail to render if the public API is out of date', () => { - const props = getDefaultProps(); - props.framePublicAPI.datasourceLayers = {}; - const component = mountWithIntl(); - expect(component.isEmptyRender()).toBe(true); - }); - - it('should fail to render if the active visualization is missing', () => { - const component = mountWithIntl( - - ); - expect(component.isEmptyRender()).toBe(true); - }); - - describe('layer reset and remove', () => { - it('should show the reset button when single layer', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( - 'Reset layer' - ); - }); - - it('should show the delete button when multiple layers', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( - 'Delete layer' - ); - }); - - it('should call the clear callback', () => { - const cb = jest.fn(); - const component = mountWithIntl(); - act(() => { - component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); - }); - expect(cb).toHaveBeenCalled(); - }); - }); - - describe('single group', () => { - it('should render the non-editable state', () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['x'], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const component = mountWithIntl(); - - const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); - expect(group).toHaveLength(1); - }); - - it('should render the group with a way to add a new column', () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const component = mountWithIntl(); - - const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); - expect(group).toHaveLength(1); - }); - - it('should render the required warning when only one group is configured', () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['x'], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - { - groupLabel: 'B', - groupId: 'b', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - required: true, - }, - ], - }); - - const component = mountWithIntl(); - - const group = component - .find(EuiFormRow) - .findWhere((e) => e.prop('error') === 'Required dimension'); - expect(group).toHaveLength(1); - }); - - it('should render the datasource and visualization panels inside the dimension container', () => { - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['newid'], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - enableDimensionEditor: true, - }, - ], - }); - mockVisualization.renderDimensionEditor = jest.fn(); - - const component = mountWithIntl(); - act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); - }); - component.update(); - - const group = component.find('DimensionContainer').first(); - const panel: React.ReactElement = group.prop('panel'); - expect(panel.props.children).toHaveLength(2); - }); - - it('should keep the DimensionContainer open when configuring a new dimension', () => { - /** - * The ID generation system for new dimensions has been messy before, so - * this tests that the ID used in the first render is used to keep the container - * open in future renders - */ - (generateId as jest.Mock).mockReturnValueOnce(`newid`); - (generateId as jest.Mock).mockReturnValueOnce(`bad`); - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - // Normally the configuration would change in response to a state update, - // but this test is updating it directly - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['newid'], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const component = mountWithIntl(); - act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); - }); - component.update(); - - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); - }); - - it('should close the DimensionContainer when the active visualization changes', () => { - /** - * The ID generation system for new dimensions has been messy before, so - * this tests that the ID used in the first render is used to keep the container - * open in future renders - */ - - (generateId as jest.Mock).mockReturnValueOnce(`newid`); - (generateId as jest.Mock).mockReturnValueOnce(`bad`); - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - // Normally the configuration would change in response to a state update, - // but this test is updating it directly - mockVisualization.getConfiguration.mockReturnValueOnce({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['newid'], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const component = mountWithIntl(); - - act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); - }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); - act(() => { - component.setProps({ activeVisualizationId: 'vis2' }); - }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(false); - }); - }); - - // This test is more like an integration test, since the layer panel owns all - // the coordination between drag and drop - describe('drag and drop behavior', () => { - it('should determine if the datasource supports dropping of a field onto empty dimension', () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - mockDatasource.canHandleDrop.mockReturnValue(true); - - const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a' }; - - const component = mountWithIntl( - - - - ); - - expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( - expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), - }) - ); - - component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); - - expect(mockDatasource.onDrop).toHaveBeenCalledWith( - expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), - }) - ); - }); - - it('should allow drag to move between groups', () => { - (generateId as jest.Mock).mockReturnValue(`newid`); - - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['a'], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroupA', - }, - { - groupLabel: 'B', - groupId: 'b', - accessors: ['b'], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroupB', - }, - ], - }); - - mockDatasource.canHandleDrop.mockReturnValue(true); - - const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; - - const component = mountWithIntl( - - - - ); - - expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2); - expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( - expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), - }) - ); - - // Simulate drop on the pre-populated dimension - component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop'); - expect(mockDatasource.onDrop).toHaveBeenCalledWith( - expect.objectContaining({ - columnId: 'b', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), - }) - ); - - // Simulate drop on the empty dimension - component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop'); - expect(mockDatasource.onDrop).toHaveBeenCalledWith( - expect.objectContaining({ - columnId: 'newid', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), - }) - ); - }); - - it('should prevent dropping in the same group', () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: ['a', 'b'], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - }, - ], - }); - - const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; - - const component = mountWithIntl( - - - - ); - - expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled(); - - component.find('DragDrop[data-test-subj="lnsGroup"]').at(0).simulate('drop'); - expect(mockDatasource.onDrop).not.toHaveBeenCalled(); - - component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).simulate('drop'); - expect(mockDatasource.onDrop).not.toHaveBeenCalled(); - - component.find('DragDrop[data-test-subj="lnsGroup"]').at(2).simulate('drop'); - expect(mockDatasource.onDrop).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/public/components/explorer/visualizations/config_panel/layer_panel.tsx b/public/components/explorer/visualizations/config_panel/layer_panel.tsx deleted file mode 100644 index fdf225832..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_panel.tsx +++ /dev/null @@ -1,477 +0,0 @@ -/* - * 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 './layer_panel.scss'; - -import React, { useContext, useState, useEffect } from 'react'; -import _ from 'lodash'; -import { - EuiPanel, - EuiSpacer, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage } from '@osd/i18n/react'; -// import { NativeRenderer } from '../../../native_renderer'; -// import { StateSetter, isDraggedOperation } from '../../../types'; -import { DragContext, DragDrop, ChildDragDropProvider } from '../drag_drop'; -import { LayerSettings } from './layer_settings'; -// import { trackUiEvent } from '../../../lens_ui_telemetry'; -// import { generateId } from '../../../id_generator'; -import { ConfigPanelWrapperProps, ActiveDimensionState } from './types'; -import { DimensionContainer } from './dimension_container'; - -const initialActiveDimensionState = { - isNew: false, -}; - -function isConfiguration( - value: unknown -): value is { columnId: string; groupId: string; layerId: string } { - return ( - value && - typeof value === 'object' && - 'columnId' in value && - 'groupId' in value && - 'layerId' in value - ); -} - -function isSameConfiguration(config1: unknown, config2: unknown) { - return ( - isConfiguration(config1) && - isConfiguration(config2) && - config1.columnId === config2.columnId && - config1.groupId === config2.groupId && - config1.layerId === config2.layerId - ); -} - -export function LayerPanel( - props: Exclude & { - layerId: string; - dataTestSubj: string; - isOnlyLayer: boolean; - // updateVisualization: StateSetter; - updateDatasource: (datasourceId: string, newState: unknown) => void; - updateAll: ( - datasourceId: string, - newDatasourcestate: unknown, - newVisualizationState: unknown - ) => void; - onRemoveLayer: () => void; - } -) { - const dragDropContext = useContext(DragContext); - const [activeDimension, setActiveDimension] = useState( - initialActiveDimensionState - ); - - const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, dataTestSubj } = props; - const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - - useEffect(() => { - setActiveDimension(initialActiveDimensionState); - }, [props.activeVisualizationId]); - - if ( - !datasourcePublicAPI || - !props.activeVisualizationId || - !props.visualizationMap[props.activeVisualizationId] - ) { - return null; - } - const activeVisualization = props.visualizationMap[props.activeVisualizationId]; - const layerVisualizationConfigProps = { - layerId, - dragDropContext, - state: props.visualizationState, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; - const layerDatasource = props.datasourceMap[datasourceId]; - - const layerDatasourceDropProps = { - layerId, - dragDropContext, - state: layerDatasourceState, - setState: (newState: unknown) => { - props.updateDatasource(datasourceId, newState); - }, - }; - - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); - const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); - const { activeId, activeGroup } = activeDimension; - return ( - - - - - - - - {layerDatasource && ( - - {/* { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> */} - - )} - - - - - {groups.map((group, index) => { - const newId = _.uniqueId(); - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - - return ( - - <> - {group.accessors.map((accessor) => { - return ( - { - const dropResult = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - // props.updateVisualization( - // activeVisualization.removeDimension({ - // layerId, - // columnId: dropResult.deleted, - // prevState: props.visualizationState, - // }) - // ); - } - }} - > -
- {/* { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: accessor, - }); - } - }, - }} - /> */} - { - // trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); - }} - /> -
-
- ); - })} - {group.supportsMoreColumns ? ( - { - const dropResult = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: group.filterOperations, - }); - if (dropResult) { - // props.updateVisualization( - // activeVisualization.setDimension({ - // layerId, - // groupId: group.groupId, - // columnId: newId, - // prevState: props.visualizationState, - // }) - // ); - - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - // props.updateVisualization( - // activeVisualization.removeDimension({ - // layerId, - // columnId: dropResult.deleted, - // prevState: props.visualizationState, - // }) - // ); - } - } - }} - > -
- { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: true, - activeGroup: group, - activeId: newId, - }); - } - }} - > - - -
-
- ) : null} - -
- ); - })} - setActiveDimension(initialActiveDimensionState)} - panel={ - <> - {activeGroup && activeId && ( - // { - // props.updateAll( - // datasourceId, - // newState, - // activeVisualization.setDimension({ - // layerId, - // groupId: activeGroup.groupId, - // columnId: activeId, - // prevState: props.visualizationState, - // }) - // ); - // setActiveDimension({ - // ...activeDimension, - // isNew: false, - // }); - // }, - // }} - // /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- {/* */} -
- )} - - } - /> - - - - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; - - if (el?.blur) { - el.blur(); - } - - onRemoveLayer(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - -
-
- ); -} diff --git a/public/components/explorer/visualizations/config_panel/layer_settings.tsx b/public/components/explorer/visualizations/config_panel/layer_settings.tsx deleted file mode 100644 index abbd7e083..000000000 --- a/public/components/explorer/visualizations/config_panel/layer_settings.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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, { useState } from 'react'; -import { EuiPopover, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { NativeRenderer } from '../../../native_renderer'; -import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; -import { ToolbarButton } from '../../../shared_components'; - -export function LayerSettings({ - layerId, - activeVisualization, - layerConfigProps, -}: { - layerId: string; - activeVisualization: Visualization; - layerConfigProps: VisualizationLayerWidgetProps; -}) { - const [isOpen, setIsOpen] = useState(false); - - if (!activeVisualization.renderLayerContextMenu) { - return null; - } - - const a11yText = i18n.translate('xpack.lens.editLayerSettings', { - defaultMessage: 'Edit layer settings', - }); - - return ( - - setIsOpen(!isOpen)} - data-test-subj="lns_layer_settings" - /> - - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - anchorPosition="downLeft" - > - - - ); -} diff --git a/public/components/explorer/visualizations/config_panel/types.ts b/public/components/explorer/visualizations/config_panel/types.ts deleted file mode 100644 index c172c6da6..000000000 --- a/public/components/explorer/visualizations/config_panel/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { Action } from '../state_management'; -import { - Visualization, - FramePublicAPI, - Datasource, - DatasourceDimensionEditorProps, - VisualizationDimensionGroupConfig, -} from '../../../types'; - -export interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - visualizationMap: Record; - activeVisualizationId: string | null; - dispatch: (action: Action) => void; - framePublicAPI: FramePublicAPI; - datasourceMap: Record; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - core: DatasourceDimensionEditorProps['core']; -} - -export interface ActiveDimensionState { - isNew: boolean; - activeId?: string; - activeGroup?: VisualizationDimensionGroupConfig; -} From 0cf388232c592651d9b426642906e0349d3ab550 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Mon, 19 Jul 2021 12:47:57 -0700 Subject: [PATCH 06/16] add intial redux setup --- public/components/app.tsx | 166 +++++++++++---------- public/components/explorer/logExplorer.tsx | 1 + public/framework/redux/reducers/index.ts | 5 + public/framework/redux/store/index.ts | 38 +++++ 4 files changed, 130 insertions(+), 80 deletions(-) create mode 100644 public/framework/redux/reducers/index.ts create mode 100644 public/framework/redux/store/index.ts diff --git a/public/components/app.tsx b/public/components/app.tsx index 343c39b93..c927126b4 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -10,9 +10,13 @@ */ import React, { useState, useRef } from 'react'; +import { Provider } from 'react-redux'; import _ from 'lodash'; import { I18nProvider } from '@osd/i18n/react'; import { HashRouter, Route, Switch } from 'react-router-dom'; + +import { store } from '../framework/redux/store'; + import { CoreStart } from '../../../../src/core/public'; import { renderPageWithSidebar } from './common/side_nav'; import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; @@ -73,85 +77,87 @@ export const App = ({ }; return ( - - - <> - - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { - text: 'Application analytics', - href: '#/application_analytics' - }, - ]); - return renderPageWithSidebar(, 1); - } } - /> - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { - text: 'Trace analytics', - href: '#/trace_analytics/home' - }, - ]); - return renderPageWithSidebar(, 2) } - } - /> - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { - text: 'Event analytics', - href: '#/event/home' - }, - ]); - return renderPageWithSidebar(, 3); - } } - /> - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { - text: 'Operational panels', - href: '#/operational_panels/home' - }, - ]); - return renderPageWithSidebar(, 4); - } } - /> - } - /> - - - - + + + + <> + + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics' + }, + ]); + return renderPageWithSidebar(, 1); + } } + /> + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + { + text: 'Trace analytics', + href: '#/trace_analytics/home' + }, + ]); + return renderPageWithSidebar(, 2) } + } + /> + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + { + text: 'Event analytics', + href: '#/event/home' + }, + ]); + return renderPageWithSidebar(, 3); + } } + /> + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + { + text: 'Operational panels', + href: '#/operational_panels/home' + }, + ]); + return renderPageWithSidebar(, 4); + } } + /> + } + /> + + + + + ); }; \ No newline at end of file diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index fe713f1f9..c3f14f39c 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -11,6 +11,7 @@ import './logExplorer.scss'; import React, { useEffect, useMemo } from 'react'; +import Redux from 'redux'; import _ from 'lodash'; import $ from 'jquery'; import { diff --git a/public/framework/redux/reducers/index.ts b/public/framework/redux/reducers/index.ts new file mode 100644 index 000000000..b02ab2460 --- /dev/null +++ b/public/framework/redux/reducers/index.ts @@ -0,0 +1,5 @@ +import { combineReducers } from 'redux'; + +export const rootReducer = combineReducers({ + +}); \ No newline at end of file diff --git a/public/framework/redux/store/index.ts b/public/framework/redux/store/index.ts new file mode 100644 index 000000000..f902d4a49 --- /dev/null +++ b/public/framework/redux/store/index.ts @@ -0,0 +1,38 @@ +import { + createStore, + compose, + applyMiddleware +} from 'redux'; +import { rootReducer } from '../reducers'; + +const initialState = {}; + +export const configureStore = (initialState: any) => { + + const middleware: Array = []; + + const composeEnhancers = + typeof window === 'object' && + process.env.NODE_ENV === 'development' && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ + + // add extendsion options here + name: 'Observability', + // actionsBlacklist: ['REDUX_STORAGE_SAVE'] + + }) : compose; + + const enhancer = composeEnhancers( + applyMiddleware(...middleware), + // other store enhancers if any + ); + + return createStore( + rootReducer, + initialState, + enhancer + ); +} + +export const store = configureStore(initialState); \ No newline at end of file From 0d82d03f0a07a13c55c8beee81057859cb9dfe06 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Mon, 19 Jul 2021 15:16:20 -0700 Subject: [PATCH 07/16] added initial reducer --- public/components/explorer/logExplorer.tsx | 2 +- public/components/explorer/reducers/index.ts | 14 ++++++++++++++ public/framework/redux/reducers/index.ts | 15 ++++++++++++++- public/framework/redux/store/index.ts | 13 ++++++++++++- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 public/components/explorer/reducers/index.ts diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index c3f14f39c..9881eb273 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -11,7 +11,7 @@ import './logExplorer.scss'; import React, { useEffect, useMemo } from 'react'; -import Redux from 'redux'; +import {} from 'react-redux'; import _ from 'lodash'; import $ from 'jquery'; import { diff --git a/public/components/explorer/reducers/index.ts b/public/components/explorer/reducers/index.ts new file mode 100644 index 000000000..beb31e537 --- /dev/null +++ b/public/components/explorer/reducers/index.ts @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const explorerReducers = { + +}; \ No newline at end of file diff --git a/public/framework/redux/reducers/index.ts b/public/framework/redux/reducers/index.ts index b02ab2460..37064f534 100644 --- a/public/framework/redux/reducers/index.ts +++ b/public/framework/redux/reducers/index.ts @@ -1,5 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + import { combineReducers } from 'redux'; +import { explorerReducers } from '../../../components/explorer/reducers'; + export const rootReducer = combineReducers({ - + ...explorerReducers, }); \ No newline at end of file diff --git a/public/framework/redux/store/index.ts b/public/framework/redux/store/index.ts index f902d4a49..42b77af30 100644 --- a/public/framework/redux/store/index.ts +++ b/public/framework/redux/store/index.ts @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + import { createStore, compose, @@ -7,7 +18,7 @@ import { rootReducer } from '../reducers'; const initialState = {}; -export const configureStore = (initialState: any) => { +export const configureStore = (initialState: {}) => { const middleware: Array = []; From b03447943fccd6ad1c97439db13901029a407d95 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Wed, 28 Jul 2021 13:52:05 -0700 Subject: [PATCH 08/16] refactorings for redux --- package.json | 1 + public/common/types/explorer.ts | 1 + public/components/app.tsx | 55 +--- .../explorer/actions/fetchActions.ts | 5 + public/components/explorer/actions/index.ts | 1 + public/components/explorer/explorer.tsx | 118 ++++++-- public/components/explorer/hooks/index.ts | 1 + .../explorer/hooks/useFetchQueryResponse.ts | 80 ++++++ public/components/explorer/logExplorer.tsx | 265 +++++++----------- .../explorer/reducers/fetchReducers.ts | 14 + public/components/explorer/reducers/index.ts | 4 +- .../explorer/reducers/queryReducers.ts | 16 ++ .../explorer/reducers/queryTabReducer.ts | 0 .../components/explorer/slices/fieldSlice.ts | 67 +++++ public/components/explorer/slices/index.ts | 13 + .../explorer/slices/queryResultSlice.ts | 48 ++++ .../components/explorer/slices/querySlice.ts | 51 ++++ .../explorer/slices/queryTabSlice.ts | 52 ++++ public/components/index.tsx | 2 + public/framework/redux/reducers/index.ts | 21 +- public/framework/redux/store/index.ts | 56 ++-- public/framework/redux/store/sharedState.ts | 17 ++ public/plugin.ts | 9 +- public/services/requests/ppl.ts | 13 +- yarn.lock | 44 +++ 25 files changed, 668 insertions(+), 286 deletions(-) create mode 100644 public/components/explorer/actions/fetchActions.ts create mode 100644 public/components/explorer/actions/index.ts create mode 100644 public/components/explorer/hooks/index.ts create mode 100644 public/components/explorer/hooks/useFetchQueryResponse.ts create mode 100644 public/components/explorer/reducers/fetchReducers.ts create mode 100644 public/components/explorer/reducers/queryReducers.ts create mode 100644 public/components/explorer/reducers/queryTabReducer.ts create mode 100644 public/components/explorer/slices/fieldSlice.ts create mode 100644 public/components/explorer/slices/index.ts create mode 100644 public/components/explorer/slices/queryResultSlice.ts create mode 100644 public/components/explorer/slices/querySlice.ts create mode 100644 public/components/explorer/slices/queryTabSlice.ts create mode 100644 public/framework/redux/store/sharedState.ts diff --git a/package.json b/package.json index 064dd66c1..790437338 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "yarn": "^1.22.10" }, "dependencies": { + "@reduxjs/toolkit": "^1.6.1", "plotly.js-dist": "^2.2.0", "react-plotly.js": "^2.5.1" }, diff --git a/public/common/types/explorer.ts b/public/common/types/explorer.ts index 9564d9d53..38cca9966 100644 --- a/public/common/types/explorer.ts +++ b/public/common/types/explorer.ts @@ -54,6 +54,7 @@ export interface IExplorerFields { export interface IExplorerProps { tabId: string; + pplService: any, query: any; explorerData: any; explorerFields: any; diff --git a/public/components/app.tsx b/public/components/app.tsx index c927126b4..1b5b9bfde 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -9,13 +9,13 @@ * GitHub history for details. */ -import React, { useState, useRef } from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import _ from 'lodash'; import { I18nProvider } from '@osd/i18n/react'; import { HashRouter, Route, Switch } from 'react-router-dom'; -import { store } from '../framework/redux/store'; +import store from '../framework/redux/store'; import { CoreStart } from '../../../../src/core/public'; import { renderPageWithSidebar } from './common/side_nav'; @@ -25,52 +25,19 @@ import { Home as OperationalPanelsHome} from './operational_panels/home'; import { Home as EventExplorerHome } from './explorer/home'; import { LogExplorer } from './explorer/logExplorer'; import { observabilityTitle } from '../../common'; -import { - ITabQueryResults, - ITabQueries, - IExplorerTabFields -} from '../common/types/explorer'; -import { - TAB_ID_TXT_PFX, - RAW_QUERY, - SELECTED_FIELDS, - UNSELECTED_FIELDS -} from '../common/constants/explorer'; interface ObservabilityAppDeps { CoreStart: CoreStart; + pplService: any } export const App = ({ - CoreStart + CoreStart, + pplService, }: ObservabilityAppDeps) => { const { chrome, http } = CoreStart; - // event explorer states - const initialTabId: string = getTabId(TAB_ID_TXT_PFX); - const [tabIds, setTabIds] = useState>([initialTabId]); - const [queries, setQueries] = useState({ - [initialTabId]: { - [RAW_QUERY]: '' - } - }); - const [queryResults, setQueryResults] = useState({ - [initialTabId]: {} - }); - const [fields, setFields] = useState({ - [initialTabId]: { - [SELECTED_FIELDS]: [], - [UNSELECTED_FIELDS]: [] - } - }); - const curQueriesRef = useRef(queries); - const [curSelectedTabId, setCurSelectedTab] = useState(initialTabId); - - function getTabId (prefix: string) { - return _.uniqueId(prefix); - } - const parentBreadcrumb = { text: observabilityTitle, href: 'observability#/' @@ -141,17 +108,7 @@ export const App = ({ path='/event/explorer' render={(props) => } /> diff --git a/public/components/explorer/actions/fetchActions.ts b/public/components/explorer/actions/fetchActions.ts new file mode 100644 index 000000000..4dd10125c --- /dev/null +++ b/public/components/explorer/actions/fetchActions.ts @@ -0,0 +1,5 @@ +import { + createAction +} from '@reduxjs/toolkit'; + +export const fetchSuccess = createAction('QUERY_RESULT/FETCH_SUCCESS'); \ No newline at end of file diff --git a/public/components/explorer/actions/index.ts b/public/components/explorer/actions/index.ts new file mode 100644 index 000000000..16059dae7 --- /dev/null +++ b/public/components/explorer/actions/index.ts @@ -0,0 +1 @@ +export * from './fetchActions'; \ No newline at end of file diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index f22eda24d..a088ef6ee 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -10,6 +10,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import _ from 'lodash'; import { FormattedMessage @@ -37,13 +38,44 @@ import { TAB_CHART_TITLE, TAB_EVENT_TITLE, TAB_EVENT_ID_TXT_PFX, - TAB_CHART_ID_TXT_PFX + TAB_CHART_ID_TXT_PFX, + RAW_QUERY, + SELECTED_FIELDS, + UNSELECTED_FIELDS } from '../../common/constants/explorer'; +import { useFetchQueryResponse } from './hooks'; +import { + changeQuery, + selectQueries +} from './slices/querySlice'; +import { selectQueryResult } from './slices/queryResultSlice'; +import { selectFields, updateFields } from './slices/fieldSlice'; const TAB_EVENT_ID = _.uniqueId(TAB_EVENT_ID_TXT_PFX); const TAB_CHART_ID = _.uniqueId(TAB_CHART_ID_TXT_PFX); -export const Explorer = (props: IExplorerProps) => { +export const Explorer = ({ + pplService, + tabId +}: IExplorerProps) => { + + const dispatch = useDispatch(); + + const requestParams = { + tabId, + }; + const { + isLoading, + getQueryResponse + } = useFetchQueryResponse({ + pplService, + requestParams + }); + + const query = useSelector(selectQueries)[tabId][RAW_QUERY]; + const explorerData = useSelector(selectQueryResult)[tabId]; + const explorerFields = useSelector(selectFields)[tabId]; + const [selectedContentTabId, setSelectedContentTab] = useState(TAB_EVENT_ID); const [startTime, setStartTime] = useState('now-15m'); const [endTime, setEndTime] = useState('now'); @@ -59,9 +91,39 @@ export const Explorer = (props: IExplorerProps) => { [setFixedScrollEl] ); - const handleAddField = (field: IField) => props.addField(field, props.tabId); + const handleAddField = (field: IField) => toggleFields(field, UNSELECTED_FIELDS, SELECTED_FIELDS); + + const handleRemoveField = (field: IField) => toggleFields(field, SELECTED_FIELDS, UNSELECTED_FIELDS); + + /** + * Toggle fields between selected and unselected sets + * @param field field to be toggled + * @param FieldSetToRemove set where this field to be removed from + * @param FieldSetToAdd set where this field to be added + */ + const toggleFields = ( + field: IField, + FieldSetToRemove: string, + FieldSetToAdd: string + ) => { - const handleRemoveField = (field: IField) => props.removeField(field, props.tabId); + const nextFields = _.cloneDeep(explorerFields); + const thisFieldSet = nextFields[FieldSetToRemove]; + const nextFieldSet = thisFieldSet.filter((fd: IField) => fd.name !== field.name); + nextFields[FieldSetToRemove] = nextFieldSet; + nextFields[FieldSetToAdd].push(field); + + dispatch( + updateFields( + { + tabId, + data: { + ...nextFields + } + } + ) + ); + }; const handleLiveStreamChecked = () => setLiveStreamChecked(!liveStreamChecked); @@ -86,8 +148,8 @@ export const Explorer = (props: IExplorerProps) => { {!isSidebarClosed && (
handleAddField(field) } handleRemoveField={ (field: IField) => handleRemoveField(field) } /> @@ -110,7 +172,7 @@ export const Explorer = (props: IExplorerProps) => { />
- { (props.explorerData && !_.isEmpty(props.explorerData)) ? ( + { (explorerData && !_.isEmpty(explorerData)) ? (
{/* {
​ @@ -182,8 +244,8 @@ export const Explorer = (props: IExplorerProps) => { const getExplorerVis = () => { return ( ); }; @@ -211,8 +273,8 @@ export const Explorer = (props: IExplorerProps) => { return getMainContentTabs(); }, [ - props.explorerData, - props.explorerFields, + explorerData, + explorerFields, isSidebarClosed ] ); @@ -248,13 +310,31 @@ export const Explorer = (props: IExplorerProps) => { ]; const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedContentTab(selectedTab.id); + + const handleQuerySearch = (tabId: string) => { + getQueryResponse(); + } + + const handleQueryChange = (query, tabId) => { + dispatch( + changeQuery( + { + tabId, + query: { + [RAW_QUERY]: query + } + } + ) + ); + } + return (

testing

{ props.setSearchQuery(query, props.tabId) } } - handleQuerySearch={ () => { props.querySearch(props.tabId) } } + query={ query } + handleQueryChange={ (query: string) => { handleQueryChange(query, tabId) } } + handleQuerySearch={ () => { handleQuerySearch(tabId) } } startTime={ startTime } endTime={ endTime } setStartTime={ setStartTime } diff --git a/public/components/explorer/hooks/index.ts b/public/components/explorer/hooks/index.ts new file mode 100644 index 000000000..2193a0fc4 --- /dev/null +++ b/public/components/explorer/hooks/index.ts @@ -0,0 +1 @@ +export * from './useFetchQueryResponse'; \ No newline at end of file diff --git a/public/components/explorer/hooks/useFetchQueryResponse.ts b/public/components/explorer/hooks/useFetchQueryResponse.ts new file mode 100644 index 000000000..f00428a73 --- /dev/null +++ b/public/components/explorer/hooks/useFetchQueryResponse.ts @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { useState } from 'react'; +import { batch } from 'react-redux'; +import { + useDispatch, + useSelector +} from 'react-redux'; +import { + RAW_QUERY, + SELECTED_FIELDS, + UNSELECTED_FIELDS +} from '../../../common/constants/explorer'; +import { fetchSuccess } from '../slices/queryResultSlice'; +import { selectQueries } from '../slices/querySlice'; +import { + updateFields, +} from '../slices/fieldSlice'; + +export const useFetchQueryResponse = ({ + pplService, + requestParams = {} +}: any) => { + + const dispatch = useDispatch(); + const [isLoading, setIsLoading] = useState(false); + const queries = useSelector(selectQueries); + const rawQuery = queries[requestParams.tabId][RAW_QUERY]; + + const getQueryResponse = async () => { + setIsLoading(true); + await pplService.fetch({ + query: rawQuery + }) + .then((res) => { + batch(() => { + dispatch( + fetchSuccess( + { + tabId: requestParams.tabId, + data: res + } + ) + ); + dispatch( + updateFields( + { + tabId: requestParams.tabId, + data: { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: res?.schema + } + } + ) + ); + }); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setIsLoading(false); + }); + } + + return { + isLoading, + getQueryResponse + }; +}; + diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index 9881eb273..e49989c18 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -11,7 +11,7 @@ import './logExplorer.scss'; import React, { useEffect, useMemo } from 'react'; -import {} from 'react-redux'; +import { useDispatch, useSelector, batch } from 'react-redux'; import _ from 'lodash'; import $ from 'jquery'; import { @@ -21,33 +21,37 @@ import { EuiTabbedContent } from '@elastic/eui'; import { Explorer } from './explorer'; -import { handlePplRequest } from '../../services/requests/ppl'; -import { - IField, -} from '../../common/types/explorer'; import { TAB_TITLE, - TAB_ID_TXT_PFX, - RAW_QUERY, - SELECTED_FIELDS, - UNSELECTED_FIELDS + TAB_ID_TXT_PFX } from '../../common/constants/explorer'; +import { + selectQueryTabs, + addTab, + setSelectedQueryTab, + removeTab +} from './slices/queryTabSlice'; +import { + init as fieldsInit, + remove as fieldsRemove +} from './slices/fieldSlice'; +import { + remove as queryRemove, + init as queryInit +} from './slices/querySlice'; +import { + init as queryResultInit, + remove as queryResultRemove, +} from './slices/queryResultSlice'; export const LogExplorer = ({ - http, - tabIds, - queries, - queryResults, - fields, - curQueriesRef, - curSelectedTabId, - setTabIds, - setQueries, - setQueryResults, - setFields, - setCurSelectedTab + pplService, }: any) => { + const dispatch = useDispatch(); + const tabIds = useSelector(selectQueryTabs)['queryTabIds']; + const curSelectedTabId = useSelector(selectQueryTabs)['selectedQueryTab']; + // Append add-new-tab link to the end of the tab list, and remove it once tabs state changes useEffect(() => { const addNewLink = $('
+ Add new').on('click', () => { @@ -59,7 +63,15 @@ export const LogExplorer = ({ } }, [tabIds]); - const handleTabClick = (selectedTab: EuiTabbedContentTab) => setCurSelectedTab(selectedTab.id); + const handleTabClick = (selectedTab: EuiTabbedContentTab) => { + dispatch( + setSelectedQueryTab( + { + tabId: selectedTab.id + } + ) + ); + }; const handleTabClose = (TabIdToBeClosed: string) => { @@ -77,38 +89,37 @@ export const LogExplorer = ({ } else if (index > 0) { newIdToFocus = tabIds[index - 1]; } - setCurSelectedTab(newIdToFocus); - // Clean up state data for this tab - setTabIds((staleTabIds) => { - return staleTabIds.filter((id) => { - if (id === TabIdToBeClosed) { - return false; - } - return id !== TabIdToBeClosed; - }); - }); - setQueries(staleQueries => { - const newQueries = { - ...staleQueries, - }; - delete newQueries[TabIdToBeClosed]; - curQueriesRef.current = newQueries; - return newQueries; - }); - setQueryResults(staleQueryResults => { - const newQueryResults = { - ...staleQueryResults - }; - delete newQueryResults[TabIdToBeClosed]; - return newQueryResults; - }); - setFields(staleFields => { - const newFields = { - ...staleFields - }; - delete newFields[TabIdToBeClosed]; - return newFields + batch(() => { + dispatch( + queryRemove( + { + tabId: TabIdToBeClosed, + } + ) + ); + dispatch( + fieldsRemove( + { + tabId: TabIdToBeClosed, + } + ) + ); + dispatch( + queryResultRemove( + { + tabId: TabIdToBeClosed, + } + ) + ); + dispatch( + removeTab( + { + tabId: TabIdToBeClosed, + newSelectedQueryTab: newIdToFocus + } + ) + ); }); }; @@ -117,119 +128,47 @@ export const LogExplorer = ({ } function addNewTab () { - const tabId: string = getTabId(TAB_ID_TXT_PFX); - - setTabIds(staleTabIds => { - return [...staleTabIds, tabId]; - }); - setQueries(staleQueries => { - const newQueries = { - ...staleQueries, - [tabId]: { - [RAW_QUERY]: '' - } - }; - curQueriesRef.current = newQueries; - return newQueries; - }); - setQueryResults(staleQueryResults => { - return { - ...staleQueryResults, - [tabId]: {} - }; - }); - setFields(staleFields => { - return { - ...staleFields, - [tabId]: { - [SELECTED_FIELDS]: [], - [UNSELECTED_FIELDS]: [] - } - }; - }); - }; - - const handleQuerySearch = async (tabId: string) => { - const latestQueries = curQueriesRef.current; - const res = await handlePplRequest(http, { query: latestQueries[tabId][RAW_QUERY].trim() }); - console.log('res: ', res); - setQueryResults(staleQueryResults => { - return { - ...staleQueryResults, - [tabId]: res - }; - }); - setFields(staleFields => { - return { - ...staleFields, - [tabId]: { - [SELECTED_FIELDS]: [], - [UNSELECTED_FIELDS]: res?.schema || [] - } - }; - }); - }; - - const setSearchQuery = (query: string, tabId: string) => { - setQueries(staleQueries => { - const newQueries = { - ...staleQueries, - [tabId]: { - [RAW_QUERY]: query - } - }; - curQueriesRef.current = newQueries; - return newQueries; - }); - }; - - const handleAddField = (field: IField, tabId: string) => toggleFields(field, tabId, UNSELECTED_FIELDS, SELECTED_FIELDS); - - const handleRemoveField = (field: IField, tabId: string) => toggleFields(field, tabId, SELECTED_FIELDS, UNSELECTED_FIELDS); - - /** - * Toggle fields between selected and unselected sets - * @param field field to be toggled - * @param tabId id of the tab that triggers fields selecting and removing - * @param FieldSetToRemove set where this field to be removed from - * @param FieldSetToAdd set where this field to be added - */ - const toggleFields = ( - field: IField, - tabId: string, - FieldSetToRemove: string, - FieldSetToAdd: string - ) => { - setFields(staleFields => { - - const nextFields = _.cloneDeep(staleFields); - - const thisFieldSet = nextFields[tabId][FieldSetToRemove]; - const nextFieldSet = thisFieldSet.filter((fd: IField) => fd.name !== field.name); - nextFields[tabId][FieldSetToRemove] = nextFieldSet; - nextFields[tabId][FieldSetToAdd].push(field); - - return nextFields; + const tabId: string = getTabId(TAB_ID_TXT_PFX); + batch(() => { + dispatch( + queryInit( + { + tabId, + } + ) + ); + dispatch( + queryResultInit( + { + tabId, + } + ) + ); + dispatch( + fieldsInit( + { + tabId, + } + ) + ); + dispatch( + addTab( + { + tabId, + } + ) + ); }); }; function getQueryTab ({ tabTitle, tabId, - fields, - queryResults, - setSearchQuery, handleTabClose, - handleQuerySearch, }: { tabTitle: string, tabId: string, - fields: any, - queries: any, - queryResults: any, - setSearchQuery: (query: string, tabId: string) => void, handleTabClose: (TabIdToBeClosed: string) => void, - handleQuerySearch: (tabId: string) => void, }) { return { id: tabId, @@ -253,14 +192,8 @@ export const LogExplorer = ({ <> { setSearchQuery(query, tabId) } } - querySearch={ (tabId: string) => { handleQuerySearch(tabId) } } - addField={ (field: IField, tabId: string) => { handleAddField(field, tabId) } } - removeField={ (field: IField, tabId: string) => { handleRemoveField(field, tabId) } } /> ) }; @@ -272,21 +205,13 @@ export const LogExplorer = ({ { tabTitle: TAB_TITLE, tabId, - queries, - fields, - queryResults, - setSearchQuery, handleTabClose, - handleQuerySearch, } ); }); }, [ - queries, tabIds, - queryResults, - fields ] ); diff --git a/public/components/explorer/reducers/fetchReducers.ts b/public/components/explorer/reducers/fetchReducers.ts new file mode 100644 index 000000000..3b50693ef --- /dev/null +++ b/public/components/explorer/reducers/fetchReducers.ts @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const fetchSuccess = (state, { payload }) => { + state[payload.tabId] = payload.data; +}; \ No newline at end of file diff --git a/public/components/explorer/reducers/index.ts b/public/components/explorer/reducers/index.ts index beb31e537..918054756 100644 --- a/public/components/explorer/reducers/index.ts +++ b/public/components/explorer/reducers/index.ts @@ -9,6 +9,4 @@ * GitHub history for details. */ -export const explorerReducers = { - -}; \ No newline at end of file +export * from './fetchReducers'; \ No newline at end of file diff --git a/public/components/explorer/reducers/queryReducers.ts b/public/components/explorer/reducers/queryReducers.ts new file mode 100644 index 000000000..60325c5e5 --- /dev/null +++ b/public/components/explorer/reducers/queryReducers.ts @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const queryChange = (state = {}, action) => { + return { + ...action.payload + }; +}; \ No newline at end of file diff --git a/public/components/explorer/reducers/queryTabReducer.ts b/public/components/explorer/reducers/queryTabReducer.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/components/explorer/slices/fieldSlice.ts b/public/components/explorer/slices/fieldSlice.ts new file mode 100644 index 000000000..54bef48b9 --- /dev/null +++ b/public/components/explorer/slices/fieldSlice.ts @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + createSlice +} from '@reduxjs/toolkit'; +import { initialTabId } from '../../../framework/redux/store/sharedState'; +import { + SELECTED_FIELDS, + UNSELECTED_FIELDS +} from '../../../common/constants/explorer'; + +const initialFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [] +}; + +const initialState = { + [initialTabId]: { + ...initialFields + } +}; + +export const fieldSlice = createSlice({ + name: 'fields', + initialState, + reducers: { + init: (state, { payload }) => { + state[payload.tabId] = { + ...initialFields + }; + }, + updateFields: (state, { payload }) => { + state[payload.tabId] = { + ...payload.data + }; + }, + reset: (state, { payload }) => { + state[payload.tabId] = { + ...initialFields + } + }, + remove: (state, { payload }) => { + delete state[payload.tabId]; + } + }, + extraReducers: (builder) => {} +}); + +export const { + init, + reset, + remove, + updateFields +} = fieldSlice.actions; + +export const selectFields = (state) => state.fields; + +export default fieldSlice.reducer; \ No newline at end of file diff --git a/public/components/explorer/slices/index.ts b/public/components/explorer/slices/index.ts new file mode 100644 index 000000000..4c057a9a5 --- /dev/null +++ b/public/components/explorer/slices/index.ts @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export * from './querySlice'; +export * from './queryResultSlice'; \ No newline at end of file diff --git a/public/components/explorer/slices/queryResultSlice.ts b/public/components/explorer/slices/queryResultSlice.ts new file mode 100644 index 000000000..a3fabfbe4 --- /dev/null +++ b/public/components/explorer/slices/queryResultSlice.ts @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + createSlice +} from '@reduxjs/toolkit'; +import { fetchSuccess as fetchSuccessReducer } from '../reducers' +import { initialTabId } from '../../../framework/redux/store/sharedState'; + +const initialState = { + [initialTabId]: {} +}; + +export const queryResultSlice = createSlice({ + name: 'queryResults', + initialState, + reducers: { + fetchSuccess: fetchSuccessReducer, + reset: (state, { payload }) => { + state[payload.tabId] = {} + }, + init: (state, { payload }) => { + state[payload.tabId] = {} + }, + remove: (state, { payload }) => { + delete state[payload.tabId]; + } + }, +}); + +export const { + fetchSuccess, + remove, + reset, + init +} = queryResultSlice.actions; + +export const selectQueryResult = (state) => state.queryResults; + +export default queryResultSlice.reducer; \ No newline at end of file diff --git a/public/components/explorer/slices/querySlice.ts b/public/components/explorer/slices/querySlice.ts new file mode 100644 index 000000000..3a62665e9 --- /dev/null +++ b/public/components/explorer/slices/querySlice.ts @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + createSlice +} from '@reduxjs/toolkit'; +import { initialTabId } from '../../../framework/redux/store/sharedState'; +import { RAW_QUERY } from '../../../common/constants/explorer'; + +const initialState = { + [initialTabId]: { + [RAW_QUERY]: '' + } +}; + +export const queriesSlice = createSlice({ + name: 'queries', + initialState, + reducers: { + changeQuery: (state, { payload }) => { + state[payload.tabId] = payload.query; + }, + init: (state, { payload }) => { + state[payload.tabId] = { + [RAW_QUERY]: '' + }; + }, + remove: (state, { payload }) => { + delete state[payload.tabId]; + } + }, + extraReducers: (builder) => {} +}); + +export const { + changeQuery, + remove, + init +} = queriesSlice.actions; + +export const selectQueries = (state) => state.queries; + +export default queriesSlice.reducer; \ No newline at end of file diff --git a/public/components/explorer/slices/queryTabSlice.ts b/public/components/explorer/slices/queryTabSlice.ts new file mode 100644 index 000000000..1966e9937 --- /dev/null +++ b/public/components/explorer/slices/queryTabSlice.ts @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + createSlice +} from '@reduxjs/toolkit'; +import { initialTabId } from '../../../framework/redux/store/sharedState'; + +const initialState = { + queryTabIds: [initialTabId], + selectedQueryTab: initialTabId +}; + +export const queryTabsSlice = createSlice({ + name: 'queryTabs', + initialState, + reducers: { + addTab: (state, { payload }) => { + state['queryTabIds'].push(payload.tabId); + state['selectedQueryTab'] = payload.tabId; + }, + removeTab: (state, { payload }) => { + state['queryTabIds'] = state['queryTabIds'].filter((tabId) => { + if (tabId === payload.tabId) return false; + return true; + }); + state['selectedQueryTab'] = payload['newSelectedQueryTab']; + }, + setSelectedQueryTab: (state, { payload }) => { + state['selectedQueryTab'] = payload.tabId; + } + }, + extraReducers: (builder) => {} +}); + +export const { + addTab, + removeTab, + setSelectedQueryTab +} = queryTabsSlice.actions; + +export const selectQueryTabs = (state) => state.explorerTabs; + +export default queryTabsSlice.reducer; \ No newline at end of file diff --git a/public/components/index.tsx b/public/components/index.tsx index d80d8f19d..483122e13 100644 --- a/public/components/index.tsx +++ b/public/components/index.tsx @@ -20,10 +20,12 @@ import { App } from './app'; export const Observability = ( CoreStart: CoreStart, AppMountParameters: AppMountParameters, + pplService: any ) => { ReactDOM.render( , AppMountParameters.element ); diff --git a/public/framework/redux/reducers/index.ts b/public/framework/redux/reducers/index.ts index 37064f534..d9a18fd4d 100644 --- a/public/framework/redux/reducers/index.ts +++ b/public/framework/redux/reducers/index.ts @@ -11,8 +11,21 @@ import { combineReducers } from 'redux'; -import { explorerReducers } from '../../../components/explorer/reducers'; +import queriesReducer from '../../../components/explorer/slices/querySlice'; +import queryResultsReducer from '../../../components/explorer/slices/queryResultSlice'; +import queryTabReducer from '../../../components/explorer/slices/queryTabSlice'; +import FieldsReducer from '../../../components/explorer/slices/fieldSlice'; -export const rootReducer = combineReducers({ - ...explorerReducers, -}); \ No newline at end of file +const rootReducer = combineReducers({ + + // explorer reducers + queries: queriesReducer, + queryResults: queryResultsReducer, + explorerTabs: queryTabReducer, + fields: FieldsReducer + +}); + +export type RootState = ReturnType; + +export default rootReducer; \ No newline at end of file diff --git a/public/framework/redux/store/index.ts b/public/framework/redux/store/index.ts index 42b77af30..74c886a9b 100644 --- a/public/framework/redux/store/index.ts +++ b/public/framework/redux/store/index.ts @@ -9,41 +9,25 @@ * GitHub history for details. */ -import { - createStore, - compose, - applyMiddleware -} from 'redux'; -import { rootReducer } from '../reducers'; - -const initialState = {}; - -export const configureStore = (initialState: {}) => { - - const middleware: Array = []; - - const composeEnhancers = - typeof window === 'object' && - process.env.NODE_ENV === 'development' && - window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? - window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ - - // add extendsion options here - name: 'Observability', - // actionsBlacklist: ['REDUX_STORAGE_SAVE'] - - }) : compose; - - const enhancer = composeEnhancers( - applyMiddleware(...middleware), - // other store enhancers if any - ); - - return createStore( - rootReducer, - initialState, - enhancer - ); +import rootReducer from '../reducers'; +import { configureStore } from '@reduxjs/toolkit'; + +const store = configureStore( + { + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), + devTools: process.env.NODE_ENV !== 'production', + enhancers: [], + } +); + +if (process.env.NODE_ENV === 'development' && module.hot) { + module.hot.accept('./rootReducer', () => { + const newRootReducer = require('./rootReducer').default + store.replaceReducer(newRootReducer) + }) } -export const store = configureStore(initialState); \ No newline at end of file +export type AppDispatch = typeof store.dispatch; + +export default store; diff --git a/public/framework/redux/store/sharedState.ts b/public/framework/redux/store/sharedState.ts new file mode 100644 index 000000000..78ffa3898 --- /dev/null +++ b/public/framework/redux/store/sharedState.ts @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { uniqueId } from 'lodash'; +import { + TAB_ID_TXT_PFX +} from '../../../common/constants/explorer' + +export const initialTabId: string = uniqueId(TAB_ID_TXT_PFX); \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 50535eed7..0b869b25b 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -26,6 +26,7 @@ import { observabilityTitle, observabilityPluginOrder } from '../common/index'; +import PPLService from './services/requests/ppl'; // import { // XYVisualization // } from './services/visualizations' @@ -52,7 +53,13 @@ export class ObservabilityPlugin implements Plugin { - return http + return this.http .post( `${PPL_BASE}${PPL_SEARCH}`, { @@ -27,4 +31,5 @@ export const handlePplRequest = async ( } ) .catch(error => console.log(error)); -}; + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0393478eb..c6ee281f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/runtime@^7.9.2": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" + integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== + dependencies: + regenerator-runtime "^0.13.4" + "@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -46,6 +53,16 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@reduxjs/toolkit@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9" + integrity sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A== + dependencies: + immer "^9.0.1" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -678,6 +695,11 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +immer@^9.0.1: + version "9.0.5" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec" + integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ== + indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -1155,6 +1177,23 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" @@ -1162,6 +1201,11 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" From e080853f2941193d2ec56c2f82e38a8cfdf758fd Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 29 Jul 2021 09:42:59 -0700 Subject: [PATCH 09/16] minor code cleanup --- public/common/types/explorer.ts | 11 +-- public/components/app.tsx | 3 - public/components/explorer/explorer.tsx | 16 ++- public/components/explorer/logExplorer.tsx | 107 +++++---------------- public/plugin.ts | 16 +-- 5 files changed, 34 insertions(+), 119 deletions(-) diff --git a/public/common/types/explorer.ts b/public/common/types/explorer.ts index 38cca9966..79b8de6b1 100644 --- a/public/common/types/explorer.ts +++ b/public/common/types/explorer.ts @@ -49,19 +49,12 @@ export interface IExplorerTabFields { export interface IExplorerFields { [SELECTED_FIELDS]: Array; - [UNSELECTED_FIELDS]: Array + [UNSELECTED_FIELDS]: Array; } export interface IExplorerProps { tabId: string; - pplService: any, - query: any; - explorerData: any; - explorerFields: any; - setSearchQuery: (query: string, tabId: string) => void; - querySearch: (tabId: string) => void; - addField: (field: IField, tabId: string) => void; - removeField: (field: IField, tabId: string) => void + pplService: any; } export interface LogExplorer { diff --git a/public/components/app.tsx b/public/components/app.tsx index 1b5b9bfde..60404eecb 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -14,9 +14,7 @@ import { Provider } from 'react-redux'; import _ from 'lodash'; import { I18nProvider } from '@osd/i18n/react'; import { HashRouter, Route, Switch } from 'react-router-dom'; - import store from '../framework/redux/store'; - import { CoreStart } from '../../../../src/core/public'; import { renderPageWithSidebar } from './common/side_nav'; import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; @@ -37,7 +35,6 @@ export const App = ({ }: ObservabilityAppDeps) => { const { chrome, http } = CoreStart; - const parentBreadcrumb = { text: observabilityTitle, href: 'observability#/' diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index a088ef6ee..1f42f22bd 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -113,16 +113,12 @@ export const Explorer = ({ nextFields[FieldSetToRemove] = nextFieldSet; nextFields[FieldSetToAdd].push(field); - dispatch( - updateFields( - { - tabId, - data: { - ...nextFields - } - } - ) - ); + dispatch(updateFields({ + tabId, + data: { + ...nextFields + } + })); }; const handleLiveStreamChecked = () => setLiveStreamChecked(!liveStreamChecked); diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index e49989c18..a8236b8d6 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -12,7 +12,10 @@ import './logExplorer.scss'; import React, { useEffect, useMemo } from 'react'; import { useDispatch, useSelector, batch } from 'react-redux'; -import _ from 'lodash'; +import { + uniqueId, + map +} from 'lodash'; import $ from 'jquery'; import { EuiIcon, @@ -32,16 +35,16 @@ import { removeTab } from './slices/queryTabSlice'; import { - init as fieldsInit, - remove as fieldsRemove + init as initFields, + remove as removefields } from './slices/fieldSlice'; import { - remove as queryRemove, - init as queryInit + init as initQuery, + remove as removeQuery } from './slices/querySlice'; import { - init as queryResultInit, - remove as queryResultRemove, + init as initQueryResult, + remove as removeQueryResult, } from './slices/queryResultSlice'; export const LogExplorer = ({ @@ -64,13 +67,7 @@ export const LogExplorer = ({ }, [tabIds]); const handleTabClick = (selectedTab: EuiTabbedContentTab) => { - dispatch( - setSelectedQueryTab( - { - tabId: selectedTab.id - } - ) - ); + dispatch(setSelectedQueryTab({ tabId: selectedTab.id })); }; const handleTabClose = (TabIdToBeClosed: string) => { @@ -91,73 +88,23 @@ export const LogExplorer = ({ } batch(() => { - dispatch( - queryRemove( - { - tabId: TabIdToBeClosed, - } - ) - ); - dispatch( - fieldsRemove( - { - tabId: TabIdToBeClosed, - } - ) - ); - dispatch( - queryResultRemove( - { - tabId: TabIdToBeClosed, - } - ) - ); - dispatch( - removeTab( - { - tabId: TabIdToBeClosed, - newSelectedQueryTab: newIdToFocus - } - ) - ); + dispatch(removeQuery({ tabId: TabIdToBeClosed, })); + dispatch(removefields({ tabId: TabIdToBeClosed, })); + dispatch(removeQueryResult({ tabId: TabIdToBeClosed, })); + dispatch(removeTab({ + tabId: TabIdToBeClosed, + newSelectedQueryTab: newIdToFocus + })); }); }; - function getTabId (prefix: string) { - return _.uniqueId(prefix); - } - function addNewTab () { - const tabId: string = getTabId(TAB_ID_TXT_PFX); + const tabId: string = uniqueId(TAB_ID_TXT_PFX); batch(() => { - dispatch( - queryInit( - { - tabId, - } - ) - ); - dispatch( - queryResultInit( - { - tabId, - } - ) - ); - dispatch( - fieldsInit( - { - tabId, - } - ) - ); - dispatch( - addTab( - { - tabId, - } - ) - ); + dispatch(initQuery({ tabId, })); + dispatch(initQueryResult({ tabId, })); + dispatch(initFields({ tabId, })); + dispatch(addTab({ tabId, })); }); }; @@ -200,7 +147,7 @@ export const LogExplorer = ({ } const memorizedTabs = useMemo(() => { - return _.map(tabIds, (tabId) => { + return map(tabIds, (tabId) => { return getQueryTab( { tabTitle: TAB_TITLE, @@ -209,11 +156,7 @@ export const LogExplorer = ({ } ); }); - }, - [ - tabIds, - ] - ); + }, [ tabIds ]); return ( <> diff --git a/public/plugin.ts b/public/plugin.ts index 0b869b25b..d129ca559 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -13,8 +13,7 @@ import { Plugin, CoreSetup, CoreStart, - AppMountParameters, - PluginInitializerContext, + AppMountParameters, DEFAULT_APP_CATEGORIES } from '../../../src/core/public'; import { @@ -27,24 +26,11 @@ import { observabilityPluginOrder } from '../common/index'; import PPLService from './services/requests/ppl'; -// import { -// XYVisualization -// } from './services/visualizations' export class ObservabilityPlugin implements Plugin { - // private xyVisualization; - - constructor() { - // this.xyVisualization = new XYVisualization(); - } - public setup(core: CoreSetup): ObservabilitySetup { - /** setup all services **/ - // Visualization setup - // this.xyVisualization.setup(); - core.application.register({ id: observabilityID, title: observabilityTitle, From 10bcf399fb9b833b756d3ba7b92fdc8094949b93 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Tue, 3 Aug 2021 18:59:58 -0700 Subject: [PATCH 10/16] adjusted chart styling, added timespan selector --- public/components/explorer/explorer.tsx | 78 +++++++++++++++++-- .../timechart_header/timechart_header.tsx | 59 +++++++------- .../countDistribution/countDistribution.tsx | 39 ++++++++++ .../visualizations/countDistribution/index.ts | 12 +++ .../workspace_panel/workspace_panel.tsx | 4 +- .../{visualization => charts}/bar.tsx | 18 +++-- .../{visualization => charts}/index.ts | 0 .../{visualization => charts}/line.tsx | 2 +- .../visualization.tsx | 0 .../visualizations/plotly/plot_template.tsx | 67 ---------------- .../visualization/plotly/plot.tsx | 67 ---------------- public/services/requests/ppl.ts | 5 +- 12 files changed, 169 insertions(+), 182 deletions(-) create mode 100644 public/components/explorer/visualizations/countDistribution/countDistribution.tsx create mode 100644 public/components/explorer/visualizations/countDistribution/index.ts rename public/components/visualizations/{visualization => charts}/bar.tsx (62%) rename public/components/visualizations/{visualization => charts}/index.ts (100%) rename public/components/visualizations/{visualization => charts}/line.tsx (96%) rename public/components/visualizations/{visualization => charts}/visualization.tsx (100%) delete mode 100644 public/components/visualizations/plotly/plot_template.tsx delete mode 100644 public/components/visualizations/visualization/plotly/plot.tsx diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 1f42f22bd..64f359e65 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -19,15 +19,19 @@ import { EuiText, EuiButtonIcon, EuiTabbedContent, - EuiTabbedContentTab + EuiTabbedContentTab, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer } from '@elastic/eui'; import classNames from 'classnames'; import { Search } from '../common/seach/search'; -import { Bar as CountDistribution } from '../visualizations/visualization/bar'; +import { CountDistribution } from './visualizations/countDistribution'; import { DataGrid } from './dataGrid'; import { Sidebar } from './sidebar'; import { NoResults } from './noResults'; import { HitsCounter } from './hits_counter/hits_counter'; +import { TimechartHeader } from './timechart_header'; import { ExplorerVisualizations } from './visualizations'; import { IField, @@ -133,6 +137,7 @@ export const Explorer = ({ }); const getMainContent = () => { + return (
@@ -171,12 +176,68 @@ export const Explorer = ({ { (explorerData && !_.isEmpty(explorerData)) ? (
- {/* {} } - /> */} - {/* */} + + + {} } + /> + + + {}} + stateInterval="auto" + /> + + +
setSelectedContentTab(selectedTab.id); const handleQuerySearch = (tabId: string) => { + if (query.includes('stats')) return; getQueryResponse(); } diff --git a/public/components/explorer/timechart_header/timechart_header.tsx b/public/components/explorer/timechart_header/timechart_header.tsx index 9229501a4..1fe370610 100644 --- a/public/components/explorer/timechart_header/timechart_header.tsx +++ b/public/components/explorer/timechart_header/timechart_header.tsx @@ -64,6 +64,7 @@ export function TimechartHeader({ onChangeInterval, stateInterval, }: TimechartHeaderProps) { + const [interval, setInterval] = useState(stateInterval); const toMoment = useCallback( (datetime: string) => { @@ -87,9 +88,9 @@ export function TimechartHeader({ onChangeInterval(e.target.value); }; - if (!timeRange || !bucketInterval) { - return null; - } + // if (!timeRange || !bucketInterval) { + // return null; + // } return ( @@ -102,13 +103,13 @@ export function TimechartHeader({ delay="long" > - {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ + {/* {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ interval !== 'auto' ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { defaultMessage: 'per', }) : '' - }`} + }`} */} @@ -131,30 +132,30 @@ export function TimechartHeader({ })} value={interval} onChange={handleIntervalChange} - append={ - bucketInterval.scaled ? ( - 1 - ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: bucketInterval.description, - }, - })} - color="warning" - size="s" - type="alert" - /> - ) : undefined + append={ undefined + // bucketInterval.scaled ? ( + // 1 + // ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { + // defaultMessage: 'buckets that are too large', + // }) + // : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { + // defaultMessage: 'too many buckets', + // }), + // bucketIntervalDescription: bucketInterval.description, + // }, + // })} + // color="warning" + // size="s" + // type="alert" + // /> + // ) : undefined } /> diff --git a/public/components/explorer/visualizations/countDistribution/countDistribution.tsx b/public/components/explorer/visualizations/countDistribution/countDistribution.tsx new file mode 100644 index 000000000..1ab1dba80 --- /dev/null +++ b/public/components/explorer/visualizations/countDistribution/countDistribution.tsx @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import { Bar } from '../../../visualizations/charts/bar'; + +export const CountDistribution = (props: any) => { + + const xvalues = ['13:00:00', '13:00:30', '13:01:00', '13:01:30', '13:02:00', '13:02:30','13:03:00', '13:03:30', '13:04:00', '13:04:30', '13:05:00', '13:05:30', '13:06:00', '13:06:30', '13:07:00']; + const yvalues = [12, 2, 7, 6, 0, 0, 8, 28, 47, 33, 13, 10, 11, 27, 32]; + const layout = { + showlegend: true, + margin: { + l: 60, + r: 10, + b: 15, + t: 30, + pad: 0, + }, + height: 220 + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/public/components/explorer/visualizations/countDistribution/index.ts b/public/components/explorer/visualizations/countDistribution/index.ts new file mode 100644 index 000000000..8af18bc68 --- /dev/null +++ b/public/components/explorer/visualizations/countDistribution/index.ts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export * from './countDistribution'; \ No newline at end of file diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx index 3eb32b2ff..13e926c1d 100644 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx @@ -43,8 +43,8 @@ import { import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; // import { DropIllustration } from '../../../assets/drop_illustration'; // import { getOriginalRequestErrorMessage } from '../../error_helper'; -import { Bar } from '../../../visualizations/visualization/bar'; -import { Line } from '../../../visualizations/visualization/line'; +import { Bar } from '../../../visualizations/charts/bar'; +import { Line } from '../../../visualizations/charts/line'; import { LensIconChartBar } from '../assets/chart_bar'; import { LensIconChartLine } from '../assets/chart_line'; // import { vis } from 'src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks'; diff --git a/public/components/visualizations/visualization/bar.tsx b/public/components/visualizations/charts/bar.tsx similarity index 62% rename from public/components/visualizations/visualization/bar.tsx rename to public/components/visualizations/charts/bar.tsx index 04c7ea254..cf5b0b557 100644 --- a/public/components/visualizations/visualization/bar.tsx +++ b/public/components/visualizations/charts/bar.tsx @@ -10,9 +10,14 @@ */ import React from 'react'; -import { Plt } from '../plotly/plot_template'; +import { Plt } from '../plotly/plot'; -export const Bar = (props: any) => { +export const Bar = ({ + xvalues, + yvalues, + name, + layoutConfig, +}: any) => { return ( { marker: { color: '#006BB4' }, - x: ['13:00:00', '13:00:30', '13:01:00', '13:01:30', '13:02:00', '13:02:30','13:03:00', '13:03:30', '13:04:00', '13:04:30', '13:05:00', '13:05:30', '13:06:00', '13:06:30', '13:07:00'], - y: [12, 2, 7, 6, 0, 0, 8, 28, 47, 33, 13, 10, 11, 27, 32], + x: xvalues, + y: yvalues, type: 'bar', - name: 'Count Distribution' + name, } ]} layout={{ - width: '100%', - height: '100%', xaxis: { fixedrange: true, showgrid: false, @@ -39,6 +42,7 @@ export const Bar = (props: any) => { showgrid: false, visible: true }, + ...layoutConfig }} /> ); diff --git a/public/components/visualizations/visualization/index.ts b/public/components/visualizations/charts/index.ts similarity index 100% rename from public/components/visualizations/visualization/index.ts rename to public/components/visualizations/charts/index.ts diff --git a/public/components/visualizations/visualization/line.tsx b/public/components/visualizations/charts/line.tsx similarity index 96% rename from public/components/visualizations/visualization/line.tsx rename to public/components/visualizations/charts/line.tsx index f9de9bb6e..5e4007bd1 100644 --- a/public/components/visualizations/visualization/line.tsx +++ b/public/components/visualizations/charts/line.tsx @@ -10,7 +10,7 @@ */ import React, { useMemo } from 'react'; -import { Plt } from '../plotly/plot_template'; +import { Plt } from '../plotly/plot'; export const Line = (props: any) => { diff --git a/public/components/visualizations/visualization/visualization.tsx b/public/components/visualizations/charts/visualization.tsx similarity index 100% rename from public/components/visualizations/visualization/visualization.tsx rename to public/components/visualizations/charts/visualization.tsx diff --git a/public/components/visualizations/plotly/plot_template.tsx b/public/components/visualizations/plotly/plot_template.tsx deleted file mode 100644 index 31a05edcc..000000000 --- a/public/components/visualizations/plotly/plot_template.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import React from 'react'; -import plotComponentFactory from 'react-plotly.js/factory'; -import Plotly from 'plotly.js-dist'; - -interface PltProps { - data: Plotly.Data[]; - layout?: Partial; - onHoverHandler?: (event: Readonly) => void; - onUnhoverHandler?: (event: Readonly) => void; - onClickHandler?: (event: Readonly) => void; - height?: string; -} - -export function Plt(props: PltProps) { - const PlotComponent = plotComponentFactory(Plotly); - - return ( - - ); -} diff --git a/public/components/visualizations/visualization/plotly/plot.tsx b/public/components/visualizations/visualization/plotly/plot.tsx deleted file mode 100644 index 545a03772..000000000 --- a/public/components/visualizations/visualization/plotly/plot.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import React from 'react'; -import plotComponentFactory from 'react-plotly.js/factory'; -import Plotly from 'plotly.js-dist'; - -interface PltProps { - data: Plotly.Data[]; - layout?: Partial; - onHoverHandler?: (event: Readonly) => void; - onUnhoverHandler?: (event: Readonly) => void; - onClickHandler?: (event: Readonly) => void; - height?: string; -} - -export function Plt(props: PltProps) { - const PlotComponent = plotComponentFactory(Plotly); - - return ( - - ); -} diff --git a/public/services/requests/ppl.ts b/public/services/requests/ppl.ts index 7789e1339..ca9a31f93 100644 --- a/public/services/requests/ppl.ts +++ b/public/services/requests/ppl.ts @@ -21,7 +21,10 @@ export default class PPLService { this.http = http; } fetch = async ( - params: { query: string } + params: { + query: string, + format: '' + } ) => { return this.http .post( From 5101c15832b27f48d28aae9c4dcdd24891efbd58 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Wed, 4 Aug 2021 15:03:07 -0700 Subject: [PATCH 11/16] added timestamp flag and checking for charts --- public/components/explorer/explorer.tsx | 131 +++++++++++++----------- server/adaptors/pplDatasource.ts | 24 ++++- 2 files changed, 90 insertions(+), 65 deletions(-) diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 64f359e65..c1f2ea549 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -176,68 +176,75 @@ export const Explorer = ({ { (explorerData && !_.isEmpty(explorerData)) ? (
- - - {} } - /> - - - {}} - stateInterval="auto" - /> - - - + { + explorerData && explorerData['hasTimestamp'] && ( + <> + + + {} } + /> + + + {}} + stateInterval="auto" + /> + + + + + ) + } +
{ + const pplRes = this.pplDataSource; const data: any[] = []; + let hasTimestamp = false; + _.forEach(pplRes.datarows, (row) => { const record: any = {}; + for (let i = 0; i < pplRes.schema.length; i++) { + + const cur = pplRes.schema[i]; + if (typeof(row[i]) === 'object') { - record[pplRes.schema[i].name] = JSON.stringify(row[i]); + record[cur.name] = JSON.stringify(row[i]); } else if (typeof(row[i]) === 'boolean') { - record[pplRes.schema[i].name] = row[i].toString(); + record[cur.name] = row[i].toString(); } else { - record[pplRes.schema[i].name] = row[i]; + record[cur.name] = row[i]; + } + + if (cur.name && + cur.name === 'timestamp' && + cur.type && + cur.type === 'timestamp' && + !hasTimestamp + ) { + hasTimestamp = true; } } + data.push(record); }); pplRes['jsonData'] = data; + pplRes['hasTimestamp'] = hasTimestamp; }; public getDataSource = () => this.pplDataSource; From 2fa5ab30839fdc3ce554602820d8747526cf962b Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Wed, 4 Aug 2021 16:17:44 -0700 Subject: [PATCH 12/16] fixed sidebar field icon issue --- public/components/explorer/sidebar/field.tsx | 2 +- .../components/explorer/sidebar/sidebar.scss | 96 +++++++++++++++++++ .../components/explorer/sidebar/sidebar.tsx | 2 + 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 public/components/explorer/sidebar/sidebar.scss diff --git a/public/components/explorer/sidebar/field.tsx b/public/components/explorer/sidebar/field.tsx index 9a044db77..fe195d129 100644 --- a/public/components/explorer/sidebar/field.tsx +++ b/public/components/explorer/sidebar/field.tsx @@ -70,7 +70,7 @@ export const Field = (props: FieldProps) => { } > ) => { diff --git a/public/components/explorer/sidebar/sidebar.scss b/public/components/explorer/sidebar/sidebar.scss new file mode 100644 index 000000000..f130b0399 --- /dev/null +++ b/public/components/explorer/sidebar/sidebar.scss @@ -0,0 +1,96 @@ +.dscSidebar__container { + padding-left: 0 !important; + padding-right: 0 !important; + background-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; +} + +.dscIndexPattern__container { + display: flex; + align-items: center; + height: $euiSize * 3; + margin-top: -$euiSizeS; +} + +.dscIndexPattern__triggerButton { + @include euiTitle('xs'); + line-height: $euiSizeXXL; +} + +.dscFieldList { + list-style: none; + margin-bottom: 0; +} + +.dscFieldListHeader { + padding: $euiSizeS $euiSizeS 0 $euiSizeS; + background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); +} + +.dscFieldList--popular { + background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); +} + +.dscFieldChooser { + padding-left: $euiSize; +} + +.dscFieldChooser__toggle { + color: $euiColorMediumShade; + margin-left: $euiSizeS !important; +} + +.dscSidebarItem { + &:hover, + &:focus-within, + &[class*='-isActive'] { + .dscSidebarItem__action { + opacity: 1; + } + } +} + +/** + * 1. Only visually hide the action, so that it's still accessible to screen readers. + * 2. When tabbed to, this element needs to be visible for keyboard accessibility. + */ +.dscSidebarItem__action { + opacity: 0; /* 1 */ + transition: none; + + &:focus { + opacity: 1; /* 2 */ + } + font-size: $euiFontSizeXS; + padding: 2px 6px !important; + height: 22px !important; + min-width: auto !important; + .euiButton__content { + padding: 0 4px; + } +} + +.dscFieldSearch { + padding: $euiSizeS; +} + +.dscFieldSearch__toggleButton { + width: calc(100% - #{$euiSizeS}); + color: $euiColorPrimary; + padding-left: $euiSizeXS; + margin-left: $euiSizeXS; +} + +.dscFieldSearch__filterWrapper { + flex-grow: 0; +} + +.dscFieldSearch__formWrapper { + padding: $euiSizeM; +} + +.dscFieldDetails { + color: $euiTextColor; + margin-bottom: $euiSizeS; +} diff --git a/public/components/explorer/sidebar/sidebar.tsx b/public/components/explorer/sidebar/sidebar.tsx index 0461ebd66..cfcbfe508 100644 --- a/public/components/explorer/sidebar/sidebar.tsx +++ b/public/components/explorer/sidebar/sidebar.tsx @@ -9,6 +9,8 @@ * GitHub history for details. */ +import './sidebar.scss'; + import React, { useState } from 'react'; import _ from 'lodash'; import { From 355e767a5ea23addf2a0b8a93e863b870f0661aa Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 5 Aug 2021 16:46:26 -0700 Subject: [PATCH 13/16] code cleanup --- public/common/constants/explorer.ts | 12 +- public/components/common/seach/queryBar.tsx | 1 - .../explorer/actions/fetchActions.ts | 5 - public/components/explorer/actions/index.ts | 1 - public/components/explorer/explorer.tsx | 1 - public/components/explorer/hooks/index.ts | 11 + .../explorer/hooks/useFetchQueryResponse.ts | 28 +- .../explorer/reducers/queryTabReducer.ts | 0 .../components/explorer/sidebar/sidebar.scss | 11 + .../components/explorer/sidebar/sidebar.tsx | 4 +- .../components/explorer/slices/fieldSlice.ts | 5 +- public/components/explorer/slices/index.ts | 13 - .../explorer/slices/queryResultSlice.ts | 5 +- .../components/explorer/slices/querySlice.ts | 7 +- .../explorer/slices/queryTabSlice.ts | 18 +- .../explorer/visualizations/_mixins.scss | 11 + .../explorer/visualizations/_variables.scss | 11 + .../explorer/visualizations/app.scss | 11 + .../visualizations/assets/axis_bottom.tsx | 30 - .../visualizations/assets/axis_left.tsx | 31 - .../visualizations/assets/axis_right.tsx | 31 - .../visualizations/assets/axis_top.tsx | 34 - .../visualizations/assets/chart_area.tsx | 30 - .../assets/chart_area_percentage.tsx | 34 - .../assets/chart_area_stacked.tsx | 34 - .../visualizations/assets/chart_bar.tsx | 11 +- .../assets/chart_bar_horizontal.tsx | 34 - .../chart_bar_horizontal_percentage.tsx | 34 - .../assets/chart_bar_horizontal_stacked.tsx | 34 - .../assets/chart_bar_percentage.tsx | 34 - .../assets/chart_bar_stacked.tsx | 34 - .../visualizations/assets/chart_datatable.tsx | 34 - .../visualizations/assets/chart_donut.tsx | 30 - .../visualizations/assets/chart_line.tsx | 11 +- .../visualizations/assets/chart_metric.tsx | 30 - .../visualizations/assets/chart_mixed_xy.tsx | 34 - .../visualizations/assets/chart_pie.tsx | 30 - .../visualizations/assets/chart_treemap.tsx | 34 - .../assets/drop_illustration.tsx | 48 -- .../explorer/visualizations/assets/legend.tsx | 11 +- .../assets/lens_app_graphic_dark_2x.png | Bin 82733 -> 0 bytes .../assets/lens_app_graphic_light_2x.png | Bin 94444 -> 0 bytes .../config_panel/config_panel.scss | 11 + .../config_panel/config_panel.tsx | 28 +- .../visualizations/config_panel/index.ts | 11 +- .../countDistribution/countDistribution.tsx | 1 + .../explorer/visualizations/datapanel.scss | 11 + .../explorer/visualizations/datapanel.tsx | 86 +- .../explorer/visualizations/fieldList.tsx | 12 +- .../explorer/visualizations/field_item.scss | 11 + .../explorer/visualizations/field_item.tsx | 449 +--------- .../explorer/visualizations/field_list.scss | 11 + .../explorer/visualizations/field_list.tsx | 195 ----- .../visualizations/fields_accordion.tsx | 58 +- .../explorer/visualizations/frameLayout.scss | 11 + .../explorer/visualizations/frameLayout.tsx | 1 - .../explorer/visualizations/index.tsx | 22 +- .../visualizations/lens_field_icon.test.tsx | 16 +- .../visualizations/lens_field_icon.tsx | 17 +- .../visualizations/workspacePanel.tsx | 20 - .../workspace_panel/chartSwitch.tsx | 56 +- .../workspace_panel/chart_switch.scss | 11 + .../workspace_panel/chart_switch.test.tsx | 649 -------------- .../workspace_panel/chart_switch.tsx | 332 ------- .../visualizations/workspace_panel/index.ts | 11 +- .../workspace_panel/workspace_panel.test.tsx | 811 ------------------ .../workspace_panel/workspace_panel.tsx | 295 +------ .../workspace_panel_wrapper.scss | 11 + .../workspace_panel_wrapper.test.tsx | 71 -- .../workspace_panel_wrapper.tsx | 80 +- .../components/visualizations/charts/index.ts | 1 - .../components/visualizations/charts/line.tsx | 17 +- .../visualizations/charts/visualization.tsx | 24 - public/plugin.ts | 1 - public/services/visualizations/index.ts | 1 - .../visualizations/visualizationBase.ts | 28 - .../visualizations/xyVisualization.ts | 30 - 77 files changed, 328 insertions(+), 3894 deletions(-) delete mode 100644 public/components/explorer/actions/fetchActions.ts delete mode 100644 public/components/explorer/actions/index.ts delete mode 100644 public/components/explorer/reducers/queryTabReducer.ts delete mode 100644 public/components/explorer/slices/index.ts delete mode 100644 public/components/explorer/visualizations/assets/axis_bottom.tsx delete mode 100644 public/components/explorer/visualizations/assets/axis_left.tsx delete mode 100644 public/components/explorer/visualizations/assets/axis_right.tsx delete mode 100644 public/components/explorer/visualizations/assets/axis_top.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_area.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_area_percentage.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_area_stacked.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_bar_percentage.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_bar_stacked.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_datatable.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_donut.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_metric.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_mixed_xy.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_pie.tsx delete mode 100644 public/components/explorer/visualizations/assets/chart_treemap.tsx delete mode 100644 public/components/explorer/visualizations/assets/drop_illustration.tsx delete mode 100644 public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png delete mode 100644 public/components/explorer/visualizations/assets/lens_app_graphic_light_2x.png delete mode 100644 public/components/explorer/visualizations/field_list.tsx delete mode 100644 public/components/explorer/visualizations/workspacePanel.tsx delete mode 100644 public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx delete mode 100644 public/components/explorer/visualizations/workspace_panel/chart_switch.tsx delete mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx delete mode 100644 public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx delete mode 100644 public/components/visualizations/charts/index.ts delete mode 100644 public/components/visualizations/charts/visualization.tsx delete mode 100644 public/services/visualizations/index.ts delete mode 100644 public/services/visualizations/visualizationBase.ts delete mode 100644 public/services/visualizations/xyVisualization.ts diff --git a/public/common/constants/explorer.ts b/public/common/constants/explorer.ts index b863a1055..aca47a402 100644 --- a/public/common/constants/explorer.ts +++ b/public/common/constants/explorer.ts @@ -17,4 +17,14 @@ export const TAB_TITLE = 'New query'; export const TAB_CHART_TITLE = 'Visualizations'; export const TAB_EVENT_TITLE = 'Events'; export const TAB_EVENT_ID_TXT_PFX = 'main-content-events-'; -export const TAB_CHART_ID_TXT_PFX = 'main-content-charts-'; \ No newline at end of file +export const TAB_CHART_ID_TXT_PFX = 'main-content-charts-'; + +// redux +export const SELECTED_QUERY_TAB = 'selectedQueryTab'; +export const QUERY_TAB_IDS = 'queryTabIds'; +export const NEW_SELECTED_QUERY_TAB = 'newSelectedQueryTab'; +export const REDUX_EXPL_SLICE_QUERIES = 'queries'; +export const REDUX_EXPL_SLICE_QUERY_RESULT = 'queryResults'; +export const REDUX_EXPL_SLICE_FIELDS = 'fields'; +export const REDUX_EXPL_SLICE_QUERY_TABS = 'queryTabs'; + diff --git a/public/components/common/seach/queryBar.tsx b/public/components/common/seach/queryBar.tsx index 21d0b4711..e606fa76d 100644 --- a/public/components/common/seach/queryBar.tsx +++ b/public/components/common/seach/queryBar.tsx @@ -38,7 +38,6 @@ export function QueryBar(props: IQueryBarProps) { data-test-subj="search-bar-input-box" value={ query[RAW_QUERY] } onChange={(e) => { - console.log('changed value: ', e.target.value); handleQueryChange(e.target.value); }} onSearch={() => { diff --git a/public/components/explorer/actions/fetchActions.ts b/public/components/explorer/actions/fetchActions.ts deleted file mode 100644 index 4dd10125c..000000000 --- a/public/components/explorer/actions/fetchActions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { - createAction -} from '@reduxjs/toolkit'; - -export const fetchSuccess = createAction('QUERY_RESULT/FETCH_SUCCESS'); \ No newline at end of file diff --git a/public/components/explorer/actions/index.ts b/public/components/explorer/actions/index.ts deleted file mode 100644 index 16059dae7..000000000 --- a/public/components/explorer/actions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './fetchActions'; \ No newline at end of file diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index c1f2ea549..86c72ffca 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -376,7 +376,6 @@ export const Explorer = ({ const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedContentTab(selectedTab.id); const handleQuerySearch = (tabId: string) => { - if (query.includes('stats')) return; getQueryResponse(); } diff --git a/public/components/explorer/hooks/index.ts b/public/components/explorer/hooks/index.ts index 2193a0fc4..0643a8cde 100644 --- a/public/components/explorer/hooks/index.ts +++ b/public/components/explorer/hooks/index.ts @@ -1 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + export * from './useFetchQueryResponse'; \ No newline at end of file diff --git a/public/components/explorer/hooks/useFetchQueryResponse.ts b/public/components/explorer/hooks/useFetchQueryResponse.ts index f00428a73..3cf94886d 100644 --- a/public/components/explorer/hooks/useFetchQueryResponse.ts +++ b/public/components/explorer/hooks/useFetchQueryResponse.ts @@ -38,30 +38,24 @@ export const useFetchQueryResponse = ({ const getQueryResponse = async () => { setIsLoading(true); - await pplService.fetch({ + await pplService.fetch({ query: rawQuery }) .then((res) => { batch(() => { dispatch( - fetchSuccess( - { - tabId: requestParams.tabId, - data: res - } - ) - ); + fetchSuccess({ + tabId: requestParams.tabId, + data: res + })); dispatch( - updateFields( - { - tabId: requestParams.tabId, - data: { - [SELECTED_FIELDS]: [], - [UNSELECTED_FIELDS]: res?.schema - } + updateFields({ + tabId: requestParams.tabId, + data: { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: res?.schema } - ) - ); + })); }); }) .catch((err) => { diff --git a/public/components/explorer/reducers/queryTabReducer.ts b/public/components/explorer/reducers/queryTabReducer.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/public/components/explorer/sidebar/sidebar.scss b/public/components/explorer/sidebar/sidebar.scss index f130b0399..a6a04f25b 100644 --- a/public/components/explorer/sidebar/sidebar.scss +++ b/public/components/explorer/sidebar/sidebar.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + .dscSidebar__container { padding-left: 0 !important; padding-right: 0 !important; diff --git a/public/components/explorer/sidebar/sidebar.tsx b/public/components/explorer/sidebar/sidebar.tsx index cfcbfe508..d86815121 100644 --- a/public/components/explorer/sidebar/sidebar.tsx +++ b/public/components/explorer/sidebar/sidebar.tsx @@ -58,7 +58,7 @@ export const Sidebar = (props: any) => { aria-labelledby="selected_fields" data-test-subj={`fieldList-selected`} > - { explorerFields.selectedFields.map(field => { + { explorerFields.selectedFields && explorerFields.selectedFields.map(field => { return (
  • { data-test-subj={`fieldList-unpopular`} > { - explorerFields.unselectedFields.map((col) => { + explorerFields.unselectedFields && explorerFields.unselectedFields.map((col) => { return (
  • { diff --git a/public/components/explorer/slices/index.ts b/public/components/explorer/slices/index.ts deleted file mode 100644 index 4c057a9a5..000000000 --- a/public/components/explorer/slices/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -export * from './querySlice'; -export * from './queryResultSlice'; \ No newline at end of file diff --git a/public/components/explorer/slices/queryResultSlice.ts b/public/components/explorer/slices/queryResultSlice.ts index a3fabfbe4..ce6b509ae 100644 --- a/public/components/explorer/slices/queryResultSlice.ts +++ b/public/components/explorer/slices/queryResultSlice.ts @@ -14,13 +14,16 @@ import { } from '@reduxjs/toolkit'; import { fetchSuccess as fetchSuccessReducer } from '../reducers' import { initialTabId } from '../../../framework/redux/store/sharedState'; +import { + REDUX_EXPL_SLICE_QUERY_RESULT +} from '../../../common/constants/explorer'; const initialState = { [initialTabId]: {} }; export const queryResultSlice = createSlice({ - name: 'queryResults', + name: REDUX_EXPL_SLICE_QUERY_RESULT, initialState, reducers: { fetchSuccess: fetchSuccessReducer, diff --git a/public/components/explorer/slices/querySlice.ts b/public/components/explorer/slices/querySlice.ts index 3a62665e9..0901f031e 100644 --- a/public/components/explorer/slices/querySlice.ts +++ b/public/components/explorer/slices/querySlice.ts @@ -13,7 +13,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { initialTabId } from '../../../framework/redux/store/sharedState'; -import { RAW_QUERY } from '../../../common/constants/explorer'; +import { + RAW_QUERY, + REDUX_EXPL_SLICE_QUERIES +} from '../../../common/constants/explorer'; const initialState = { [initialTabId]: { @@ -22,7 +25,7 @@ const initialState = { }; export const queriesSlice = createSlice({ - name: 'queries', + name: REDUX_EXPL_SLICE_QUERIES, initialState, reducers: { changeQuery: (state, { payload }) => { diff --git a/public/components/explorer/slices/queryTabSlice.ts b/public/components/explorer/slices/queryTabSlice.ts index 1966e9937..9575af7e6 100644 --- a/public/components/explorer/slices/queryTabSlice.ts +++ b/public/components/explorer/slices/queryTabSlice.ts @@ -13,6 +13,12 @@ import { createSlice } from '@reduxjs/toolkit'; import { initialTabId } from '../../../framework/redux/store/sharedState'; +import { + SELECTED_QUERY_TAB, + QUERY_TAB_IDS, + NEW_SELECTED_QUERY_TAB, + REDUX_EXPL_SLICE_QUERY_TABS +} from '../../../common/constants/explorer'; const initialState = { queryTabIds: [initialTabId], @@ -20,22 +26,22 @@ const initialState = { }; export const queryTabsSlice = createSlice({ - name: 'queryTabs', + name: REDUX_EXPL_SLICE_QUERY_TABS, initialState, reducers: { addTab: (state, { payload }) => { - state['queryTabIds'].push(payload.tabId); - state['selectedQueryTab'] = payload.tabId; + state[QUERY_TAB_IDS].push(payload.tabId); + state[SELECTED_QUERY_TAB] = payload.tabId; }, removeTab: (state, { payload }) => { - state['queryTabIds'] = state['queryTabIds'].filter((tabId) => { + state[QUERY_TAB_IDS] = state[QUERY_TAB_IDS].filter((tabId) => { if (tabId === payload.tabId) return false; return true; }); - state['selectedQueryTab'] = payload['newSelectedQueryTab']; + state[SELECTED_QUERY_TAB] = payload[NEW_SELECTED_QUERY_TAB]; }, setSelectedQueryTab: (state, { payload }) => { - state['selectedQueryTab'] = payload.tabId; + state[SELECTED_QUERY_TAB] = payload.tabId; } }, extraReducers: (builder) => {} diff --git a/public/components/explorer/visualizations/_mixins.scss b/public/components/explorer/visualizations/_mixins.scss index 0db72d118..df29a144a 100644 --- a/public/components/explorer/visualizations/_mixins.scss +++ b/public/components/explorer/visualizations/_mixins.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + // sass-lint:disable-block indentation, no-color-keywords // SASSTODO: Create this in EUI diff --git a/public/components/explorer/visualizations/_variables.scss b/public/components/explorer/visualizations/_variables.scss index 5a4869bb8..3c7eaaeec 100644 --- a/public/components/explorer/visualizations/_variables.scss +++ b/public/components/explorer/visualizations/_variables.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + $lnsPanelMinWidth: $euiSize * 18; // These sizes also match canvas' page thumbnails for consistency diff --git a/public/components/explorer/visualizations/app.scss b/public/components/explorer/visualizations/app.scss index 8416577a6..58d77793a 100644 --- a/public/components/explorer/visualizations/app.scss +++ b/public/components/explorer/visualizations/app.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + .lnsAppWrapper { display: flex; flex-direction: column; diff --git a/public/components/explorer/visualizations/assets/axis_bottom.tsx b/public/components/explorer/visualizations/assets/axis_bottom.tsx deleted file mode 100644 index 9529a93e4..000000000 --- a/public/components/explorer/visualizations/assets/axis_bottom.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 * as React from 'react'; - -export const EuiIconAxisBottom = ({ - title, - titleId, - ...props -}: { - title: string; - titleId: string; -}) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/axis_left.tsx b/public/components/explorer/visualizations/assets/axis_left.tsx deleted file mode 100644 index d1ec0b76a..000000000 --- a/public/components/explorer/visualizations/assets/axis_left.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 * as React from 'react'; - -export const EuiIconAxisLeft = ({ - title, - titleId, - ...props -}: { - title: string; - titleId: string; -}) => ( - - {title ? {title} : null} - - - - -); diff --git a/public/components/explorer/visualizations/assets/axis_right.tsx b/public/components/explorer/visualizations/assets/axis_right.tsx deleted file mode 100644 index e61f87b96..000000000 --- a/public/components/explorer/visualizations/assets/axis_right.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 * as React from 'react'; - -export const EuiIconAxisRight = ({ - title, - titleId, - ...props -}: { - title: string; - titleId: string; -}) => ( - - {title ? {title} : null} - - - - -); diff --git a/public/components/explorer/visualizations/assets/axis_top.tsx b/public/components/explorer/visualizations/assets/axis_top.tsx deleted file mode 100644 index 90fbdc4a2..000000000 --- a/public/components/explorer/visualizations/assets/axis_top.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 * as React from 'react'; - -export const EuiIconAxisTop = ({ - title, - titleId, - ...props -}: { - title: string; - titleId: string; -}) => ( - - {title ? {title} : null} - - - - - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_area.tsx b/public/components/explorer/visualizations/assets/chart_area.tsx deleted file mode 100644 index ae817e979..000000000 --- a/public/components/explorer/visualizations/assets/chart_area.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartArea = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_area_percentage.tsx b/public/components/explorer/visualizations/assets/chart_area_percentage.tsx deleted file mode 100644 index 45c208d5d..000000000 --- a/public/components/explorer/visualizations/assets/chart_area_percentage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartAreaPercentage = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_area_stacked.tsx b/public/components/explorer/visualizations/assets/chart_area_stacked.tsx deleted file mode 100644 index 0320ad7e9..000000000 --- a/public/components/explorer/visualizations/assets/chart_area_stacked.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartAreaStacked = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_bar.tsx b/public/components/explorer/visualizations/assets/chart_bar.tsx index 9408f77bd..f9068aada 100644 --- a/public/components/explorer/visualizations/assets/chart_bar.tsx +++ b/public/components/explorer/visualizations/assets/chart_bar.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx deleted file mode 100644 index 7ec48b107..000000000 --- a/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartBarHorizontal = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx deleted file mode 100644 index 6ce09265d..000000000 --- a/public/components/explorer/visualizations/assets/chart_bar_horizontal_percentage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartBarHorizontalPercentage = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx b/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx deleted file mode 100644 index c862121fd..000000000 --- a/public/components/explorer/visualizations/assets/chart_bar_horizontal_stacked.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartBarHorizontalStacked = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx b/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx deleted file mode 100644 index b7d6a0ed6..000000000 --- a/public/components/explorer/visualizations/assets/chart_bar_percentage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartBarPercentage = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx b/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx deleted file mode 100644 index edf8e6751..000000000 --- a/public/components/explorer/visualizations/assets/chart_bar_stacked.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartBarStacked = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_datatable.tsx b/public/components/explorer/visualizations/assets/chart_datatable.tsx deleted file mode 100644 index 48cc844ea..000000000 --- a/public/components/explorer/visualizations/assets/chart_datatable.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartDatatable = ({ - title, - titleId, - ...props -}: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_donut.tsx b/public/components/explorer/visualizations/assets/chart_donut.tsx deleted file mode 100644 index 9482161de..000000000 --- a/public/components/explorer/visualizations/assets/chart_donut.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartDonut = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_line.tsx b/public/components/explorer/visualizations/assets/chart_line.tsx index 5b57e1fe2..912b448d0 100644 --- a/public/components/explorer/visualizations/assets/chart_line.tsx +++ b/public/components/explorer/visualizations/assets/chart_line.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/explorer/visualizations/assets/chart_metric.tsx b/public/components/explorer/visualizations/assets/chart_metric.tsx deleted file mode 100644 index 9faa4d658..000000000 --- a/public/components/explorer/visualizations/assets/chart_metric.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartMetric = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx b/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx deleted file mode 100644 index 08eac8eb1..000000000 --- a/public/components/explorer/visualizations/assets/chart_mixed_xy.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartMixedXy = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_pie.tsx b/public/components/explorer/visualizations/assets/chart_pie.tsx deleted file mode 100644 index cc26df441..000000000 --- a/public/components/explorer/visualizations/assets/chart_pie.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartPie = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - -); diff --git a/public/components/explorer/visualizations/assets/chart_treemap.tsx b/public/components/explorer/visualizations/assets/chart_treemap.tsx deleted file mode 100644 index 57205e941..000000000 --- a/public/components/explorer/visualizations/assets/chart_treemap.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiIconProps } from '@elastic/eui'; - -export const LensIconChartTreemap = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - - -); diff --git a/public/components/explorer/visualizations/assets/drop_illustration.tsx b/public/components/explorer/visualizations/assets/drop_illustration.tsx deleted file mode 100644 index 1076f4875..000000000 --- a/public/components/explorer/visualizations/assets/drop_illustration.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 * as React from 'react'; -import { EuiIconProps } from '@elastic/eui'; - -export const DropIllustration = ({ title, titleId, ...props }: Omit) => ( - - {title ? {title} : null} - - - - - - - - -); diff --git a/public/components/explorer/visualizations/assets/legend.tsx b/public/components/explorer/visualizations/assets/legend.tsx index d73e68839..00a7bf54f 100644 --- a/public/components/explorer/visualizations/assets/legend.tsx +++ b/public/components/explorer/visualizations/assets/legend.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import * as React from 'react'; diff --git a/public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png b/public/components/explorer/visualizations/assets/lens_app_graphic_dark_2x.png deleted file mode 100644 index 2c2c71b82180a7574427c284cfffa91771a2296a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82733 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

    D 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

    s4&bcDbHgys`Q8|Ssnb#9qa?* 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 }); -function LayerPanels( - props: ConfigPanelWrapperProps & { - activeDatasourceId: string; - } -) { +function LayerPanels(props: any) { const { queryResults } = props; @@ -48,7 +50,9 @@ function LayerPanels( { panelItems.map((item) => { return ( - <> +

    - +
    ); }) } diff --git a/public/components/explorer/visualizations/config_panel/index.ts b/public/components/explorer/visualizations/config_panel/index.ts index 754b3fb5c..9cd601215 100644 --- a/public/components/explorer/visualizations/config_panel/index.ts +++ b/public/components/explorer/visualizations/config_panel/index.ts @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ export { ConfigPanelWrapper } from './config_panel'; diff --git a/public/components/explorer/visualizations/countDistribution/countDistribution.tsx b/public/components/explorer/visualizations/countDistribution/countDistribution.tsx index 1ab1dba80..cd50cc3f4 100644 --- a/public/components/explorer/visualizations/countDistribution/countDistribution.tsx +++ b/public/components/explorer/visualizations/countDistribution/countDistribution.tsx @@ -14,6 +14,7 @@ import { Bar } from '../../../visualizations/charts/bar'; export const CountDistribution = (props: any) => { + // hardcode for now const xvalues = ['13:00:00', '13:00:30', '13:01:00', '13:01:30', '13:02:00', '13:02:30','13:03:00', '13:03:30', '13:04:00', '13:04:30', '13:05:00', '13:05:30', '13:06:00', '13:06:30', '13:07:00']; const yvalues = [12, 2, 7, 6, 0, 0, 8, 28, 47, 33, 13, 10, 11, 27, 32]; const layout = { diff --git a/public/components/explorer/visualizations/datapanel.scss b/public/components/explorer/visualizations/datapanel.scss index df73789ea..ce79adf9d 100644 --- a/public/components/explorer/visualizations/datapanel.scss +++ b/public/components/explorer/visualizations/datapanel.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + .lnsInnerIndexPatternDataPanel { width: 100%; height: 100%; diff --git a/public/components/explorer/visualizations/datapanel.tsx b/public/components/explorer/visualizations/datapanel.tsx index a29984d00..c341f0206 100644 --- a/public/components/explorer/visualizations/datapanel.tsx +++ b/public/components/explorer/visualizations/datapanel.tsx @@ -17,9 +17,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormControlLayout, - EuiSpacer, - EuiAccordion, - EuiFilterGroup + EuiSpacer } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FieldList } from './fieldList'; @@ -28,8 +26,6 @@ export const DataPanel = (props: any) => { const { schema } = props.queryResults; - // const fieldsGroup = schema - return ( { direction="column" responsive={false} > - - {/*
    - index picker -
    */} -
    { 'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', { defaultMessage: 'Clear name and type filters', }), - onClick: () => { - // trackUiEvent('indexpattern_filters_cleared'); - // clearLocalState(); - }, + onClick: () => {}, }} > { defaultMessage: 'Search field names', description: 'Search the list of fields in the index pattern for the provided text', })} - // value={localState.nameFilter} value={''} - onChange={(e) => { - // setLocalState({ ...localState, nameFilter: e.target.value }); - }} + onChange={(e) => {}} aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { defaultMessage: 'Search fields', })} /> - - - - {/* setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))} - button={ - { - setLocalState((s) => ({ - ...s, - isTypeFilterOpen: !localState.isTypeFilterOpen, - })); - }} - > - {fieldFiltersLabel} - - } - > - ( - { - trackUiEvent('indexpattern_type_filter_toggled'); - setLocalState((s) => ({ - ...s, - typeFilter: localState.typeFilter.includes(type) - ? localState.typeFilter.filter((t) => t !== type) - : [...localState.typeFilter, type], - })); - }} - > - - {fieldTypeNames[type]} - - - ))} - /> - */} - - {/* -
    - {schema && schema.map((item) => item.name)} -
    -
    */} -
    ); diff --git a/public/components/explorer/visualizations/fieldList.tsx b/public/components/explorer/visualizations/fieldList.tsx index 960f3f074..4bcd113b0 100644 --- a/public/components/explorer/visualizations/fieldList.tsx +++ b/public/components/explorer/visualizations/fieldList.tsx @@ -9,20 +9,16 @@ * GitHub history for details. */ -// import './field_list.scss'; -import { throttle } from 'lodash'; -import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; -import { EuiSpacer } from '@elastic/eui'; -import { FieldItem } from './field_item'; +import React from 'react'; import { FieldsAccordion } from './fields_accordion'; export const FieldList = ( { schema, id - } + }: any ) => { - + return (
    ; - // topValues?: BucketedAggregation; -} - function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows // the browser to efficiently word-wrap right after the dot @@ -84,80 +31,18 @@ function wrapOnDot(str?: string) { return str ? str.replace(/\./g, '.\u200B') : ''; } -export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { +export const InnerFieldItem = function InnerFieldItem(props: any) { const { - // core, field, - // indexPattern, highlight, exists, - // query, - // dateRange, - // filters, hideDetails, } = props; const [infoIsOpen, setOpen] = useState(false); - const [state, setState] = useState({ - isLoading: false, - }); - - function fetchData() { - if (state.isLoading) { - return; - } - - setState((s) => ({ ...s, isLoading: true })); - - // core.http - // .post(`/api/lens/index_stats/${indexPattern.title}/field`, { - // body: JSON.stringify({ - // // dslQuery: esQuery.buildEsQuery( - // // indexPattern as IIndexPattern, - // // query, - // // filters, - // // esQuery.getEsQueryConfig(core.uiSettings) - // // ), - // fromDate: dateRange.fromDate, - // toDate: dateRange.toDate, - // timeFieldName: indexPattern.timeFieldName, - // field, - // }), - // }) - // .then((results: FieldStatsResponse) => { - // setState((s) => ({ - // ...s, - // isLoading: false, - // totalDocuments: results.totalDocuments, - // sampledDocuments: results.sampledDocuments, - // sampledValues: results.sampledValues, - // histogram: results.histogram, - // topValues: results.topValues, - // })); - // }) - // .catch(() => { - // setState((s) => ({ ...s, isLoading: false })); - // }); - } - - function togglePopover() { - if (hideDetails) { - return; - } + function togglePopover() {} - setOpen(!infoIsOpen); - if (!infoIsOpen) { - // trackUiEvent('indexpattern_field_info_click'); - fetchData(); - } - } - - // const value = React.useMemo(() => ({ field, indexPatternId: indexPattern.id } as DraggedField), [ - // field, - // indexPattern.id, - // ]); - // const lensFieldIcon = ; const lensFieldIcon = ; const lensInfoIcon = ( @@ -211,7 +95,6 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { fieldIcon={lensFieldIcon} fieldName={ - {/* {wrapOnDot(field.displayName)} */} {wrapOnDot(field.name)} } @@ -224,318 +107,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { anchorPosition="rightUp" panelClassName="lnsFieldItem__fieldPanel" > - {/* */} ); }; export const FieldItem = debouncedComponent(InnerFieldItem); - -// function FieldItemPopoverContents(props: State & FieldItemProps) { -// const { -// histogram, -// topValues, -// indexPattern, -// field, -// dateRange, -// core, -// sampledValues, -// chartsThemeService, -// data: { fieldFormats }, -// } = props; - -// const chartTheme = chartsThemeService.useChartsTheme(); -// const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); -// // let histogramDefault = !!props.histogram; - -// const totalValuesCount = -// topValues && topValues.buckets.reduce((prev, bucket) => bucket.count + prev, 0); -// const otherCount = sampledValues && totalValuesCount ? sampledValues - totalValuesCount : 0; - -// if ( -// totalValuesCount && -// histogram && -// histogram.buckets.length && -// topValues && -// topValues.buckets.length -// ) { -// // Default to histogram when top values are less than 10% of total -// // histogramDefault = otherCount / totalValuesCount > 0.9; -// } - -// // const [showingHistogram, setShowingHistogram] = useState(histogramDefault); - -// let formatter: { convert: (data: unknown) => string }; -// if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) { -// const FormatType = fieldFormats.getType(indexPattern.fieldFormatMap[field.name].id); -// if (FormatType) { -// formatter = new FormatType( -// indexPattern.fieldFormatMap[field.name].params, -// core.uiSettings.get.bind(core.uiSettings) -// ); -// } else { -// formatter = { convert: (data: unknown) => JSON.stringify(data) }; -// } -// } else { -// // formatter = fieldFormats.getDefaultInstance( -// // field.type as KBN_FIELD_TYPES, -// // field.esTypes as ES_FIELD_TYPES[] -// // ); -// } - -// const fromDate = DateMath.parse(dateRange.fromDate); -// const toDate = DateMath.parse(dateRange.toDate); - -// let title = <>; - -// if (props.isLoading) { -// return ; -// } else if ( -// // (!props.histogram || props.histogram.buckets.length === 0) && -// // (!props.topValues || props.topValues.buckets.length === 0) -// ) { -// return ( -// -// {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { -// defaultMessage: -// 'This field is empty because it doesn’t exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.', -// })} -// -// ); -// } - -// if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { -// title = ( -// { -// // setShowingHistogram(optionId === 'histogram'); -// }} -// // idSelected={showingHistogram ? 'histogram' : 'topValues'} -// /> -// ); -// } else if (field.type === 'date') { -// title = ( -// <> -// {i18n.translate('xpack.lens.indexPattern.fieldTimeDistributionLabel', { -// defaultMessage: 'Time distribution', -// })} -// -// ); -// } else if (topValues && topValues.buckets.length) { -// title = ( -// <> -// {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { -// defaultMessage: 'Top values', -// })} -// -// ); -// } - -// function wrapInPopover(el: React.ReactElement) { -// return ( -// <> -// {title ? {title} : <>} -// {el} - -// {props.totalDocuments ? ( -// -// -// {props.sampledDocuments && ( -// <> -// {i18n.translate('xpack.lens.indexPattern.percentageOfLabel', { -// defaultMessage: '{percentage}% of', -// values: { -// percentage: Math.round((props.sampledDocuments / props.totalDocuments) * 100), -// }, -// })} -// -// )}{' '} -// -// {/* {fieldFormats -// .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER]) -// .convert(props.totalDocuments)} */} -// {' '} -// {i18n.translate('xpack.lens.indexPattern.ofDocumentsLabel', { -// defaultMessage: 'documents', -// })} -// -// -// ) : ( -// <> -// )} -// -// ); -// } - -// if (histogram && histogram.buckets.length) { -// const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { -// defaultMessage: 'Count', -// }); - -// if (field.type === 'date') { -// return wrapInPopover( -// -// - -// - -// -// -// ); -// // } else if (showingHistogram || !topValues || !topValues.buckets.length) { -// } else if (!topValues || !topValues.buckets.length) { -// return wrapInPopover( -// -// - -// formatter.convert(d)} -// /> - -// -// -// ); -// } -// } - -// if (props.topValues && props.topValues.buckets.length) { -// return wrapInPopover( -//
    -// {props.topValues.buckets.map((topValue) => { -// const formatted = formatter.convert(topValue.key); -// return ( -//
    -// -// -// {formatted === '' ? ( -// -// -// {i18n.translate('xpack.lens.indexPattern.fieldPanelEmptyStringValue', { -// defaultMessage: 'Empty string', -// })} -// -// -// ) : ( -// -// -// {formatted} -// -// -// )} -// -// -// -// {Math.round((topValue.count / props.sampledValues!) * 100)}% -// -// -// - -// -//
    -// ); -// })} -// {otherCount ? ( -// <> -// -// -// -// {i18n.translate('xpack.lens.indexPattern.otherDocsLabel', { -// defaultMessage: 'Other', -// })} -// -// - -// -// -// {Math.round((otherCount / props.sampledValues!) * 100)}% -// -// -// - -// -// -// ) : ( -// <> -// )} -//
    -// ); -// } -// return <>; -// } diff --git a/public/components/explorer/visualizations/field_list.scss b/public/components/explorer/visualizations/field_list.scss index f28581b83..dc8759656 100644 --- a/public/components/explorer/visualizations/field_list.scss +++ b/public/components/explorer/visualizations/field_list.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + /** * 1. Don't cut off the shadow of the field items */ diff --git a/public/components/explorer/visualizations/field_list.tsx b/public/components/explorer/visualizations/field_list.tsx deleted file mode 100644 index eb7730677..000000000 --- a/public/components/explorer/visualizations/field_list.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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 './field_list.scss'; -import { throttle } from 'lodash'; -import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; -import { EuiSpacer } from '@elastic/eui'; -import { FieldItem } from './field_item'; -import { NoFieldsCallout } from './no_fields_callout'; -import { IndexPatternField } from './types'; -import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; -const PAGINATION_SIZE = 50; - -export interface FieldsGroup { - specialFields: IndexPatternField[]; - availableFields: IndexPatternField[]; - emptyFields: IndexPatternField[]; - metaFields: IndexPatternField[]; -} - -export type FieldGroups = Record< - string, - { - fields: IndexPatternField[]; - fieldCount: number; - showInAccordion: boolean; - isInitiallyOpen: boolean; - title: string; - isAffectedByGlobalFilter: boolean; - isAffectedByTimeFilter: boolean; - hideDetails?: boolean; - defaultNoFieldsMessage?: string; - } ->; - -function getDisplayedFieldsLength( - fieldGroups: FieldGroups, - accordionState: Partial> -) { - return Object.entries(fieldGroups) - .filter(([key]) => accordionState[key]) - .reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0); -} - -export function FieldList({ - exists, - fieldGroups, - existenceFetchFailed, - fieldProps, - hasSyncedExistingFields, - filter, - currentIndexPatternId, - existFieldsInIndex, -}: { - exists: (field: IndexPatternField) => boolean; - fieldGroups: FieldGroups; - fieldProps: FieldItemSharedProps; - hasSyncedExistingFields: boolean; - existenceFetchFailed?: boolean; - filter: { - nameFilter: string; - typeFilter: string[]; - }; - currentIndexPatternId: string; - existFieldsInIndex: boolean; -}) { - const [pageSize, setPageSize] = useState(PAGINATION_SIZE); - const [scrollContainer, setScrollContainer] = useState(undefined); - const [accordionState, setAccordionState] = useState>>(() => - Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) - ) - ); - - useEffect(() => { - // Reset the scroll if we have made material changes to the field list - if (scrollContainer) { - scrollContainer.scrollTop = 0; - setPageSize(PAGINATION_SIZE); - } - }, [filter.nameFilter, filter.typeFilter, currentIndexPatternId, scrollContainer]); - - const lazyScroll = useCallback(() => { - if (scrollContainer) { - const nearBottom = - scrollContainer.scrollTop + scrollContainer.clientHeight > - scrollContainer.scrollHeight * 0.9; - if (nearBottom) { - setPageSize( - Math.max( - PAGINATION_SIZE, - Math.min( - pageSize + PAGINATION_SIZE * 0.5, - getDisplayedFieldsLength(fieldGroups, accordionState) - ) - ) - ); - } - } - }, [scrollContainer, pageSize, setPageSize, fieldGroups, accordionState]); - - const paginatedFields = useMemo(() => { - let remainingItems = pageSize; - return Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup]) => { - if (!accordionState[key] || remainingItems <= 0) { - return [key, []]; - } - const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); - remainingItems = remainingItems - slicedFieldList.length; - return [key, slicedFieldList]; - }) - ); - }, [pageSize, fieldGroups, accordionState]); - - return ( -
    { - if (el && !el.dataset.dynamicScroll) { - el.dataset.dynamicScroll = 'true'; - setScrollContainer(el); - } - }} - onScroll={throttle(lazyScroll, 100)} - > -
    - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => !showInAccordion) - .flatMap(([, { fields }]) => - fields.map((field) => ( - - )) - )} - - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup]) => ( - - { - setAccordionState((s) => ({ - ...s, - [key]: open, - })); - const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { - ...accordionState, - [key]: open, - }); - setPageSize( - Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) - ); - }} - showExistenceFetchError={existenceFetchFailed} - renderCallout={ - - } - /> - - - ))} -
    -
    - ); -} diff --git a/public/components/explorer/visualizations/fields_accordion.tsx b/public/components/explorer/visualizations/fields_accordion.tsx index 26fa32e47..3d551d23a 100644 --- a/public/components/explorer/visualizations/fields_accordion.tsx +++ b/public/components/explorer/visualizations/fields_accordion.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import './datapanel.scss'; @@ -15,57 +20,15 @@ import { EuiLoadingSpinner, EuiIconTip, } from '@elastic/eui'; -// import { DataPublicPluginStart } from 'src/plugins/data/public'; -// import { IndexPatternField } from './types'; import { FieldItem } from './field_item'; -// import { Query, Filter } from '../../../../../src/plugins/data/public'; -// import { DatasourceDataPanelProps } from '../types'; -// import { IndexPattern } from './types'; -// import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -// import { schema } from 'packages/osd-config-schema/target/types'; - -export interface FieldItemSharedProps { - // core: DatasourceDataPanelProps['core']; - // data: DataPublicPluginStart; - // chartsThemeService: ChartsPluginSetup['theme']; - // indexPattern: IndexPattern; - // highlight?: string; - // query: Query; - // dateRange: DatasourceDataPanelProps['dateRange']; - // filters: Filter[]; -} - -export interface FieldsAccordionProps { - initialIsOpen: boolean; - onToggle: (open: boolean) => void; - id: string; - label: string; - hasLoaded: boolean; - fieldsCount: number; - isFiltered: boolean; - // paginatedFields: IndexPatternField[]; - fieldProps: FieldItemSharedProps; - renderCallout: JSX.Element; - // exists: (field: IndexPatternField) => boolean; - showExistenceFetchError?: boolean; - hideDetails?: boolean; -} export const InnerFieldsAccordion = function InnerFieldsAccordion({ - // initialIsOpen, - // onToggle, id, label, - // hasLoaded, - // fieldsCount, isFiltered, paginatedFields, - // fieldProps, - // renderCallout, - // exists, - // hideDetails, showExistenceFetchError, -}) { +}: any) { const renderField = useCallback( (field) => ( diff --git a/public/components/explorer/visualizations/frameLayout.scss b/public/components/explorer/visualizations/frameLayout.scss index a742aa43c..6ae4c2d30 100644 --- a/public/components/explorer/visualizations/frameLayout.scss +++ b/public/components/explorer/visualizations/frameLayout.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + $lnsPanelMinWidth: $euiSize * 18; // These sizes also match canvas' page thumbnails for consistency diff --git a/public/components/explorer/visualizations/frameLayout.tsx b/public/components/explorer/visualizations/frameLayout.tsx index 11a74aedb..c0b29ee87 100644 --- a/public/components/explorer/visualizations/frameLayout.tsx +++ b/public/components/explorer/visualizations/frameLayout.tsx @@ -30,7 +30,6 @@ export function FrameLayout(props: FrameLayoutProps) { {props.workspacePanel} - {/* {props.suggestionsPanel} */} {props.configPanel} diff --git a/public/components/explorer/visualizations/index.tsx b/public/components/explorer/visualizations/index.tsx index 9cbf4c3a4..dc97f4e83 100644 --- a/public/components/explorer/visualizations/index.tsx +++ b/public/components/explorer/visualizations/index.tsx @@ -18,22 +18,6 @@ import { FrameLayout } from './frameLayout'; import { DataPanel } from './datapanel'; import { WorkspacePanel } from './workspace_panel'; import { ConfigPanelWrapper } from './config_panel'; -// import {} - -// const VIS_MAPS = { -// 'lnsXY': { -// visualizationTypes: [ -// { -// id: 'bar', -// label: 'Bar' -// }, -// { -// id: 'line', -// label: 'Line' -// } -// ] -// } -// }; export const ExplorerVisualizations = (props: any) => { @@ -43,11 +27,7 @@ export const ExplorerVisualizations = (props: any) => { queryResults={ props.queryResults } />} workspacePanel={ - {} } - query={ props.query } - // visualizationMap={ VIS_MAPS } - /> + } configPanel={ { - return ( - <> - central panel - - ); -}; \ No newline at end of file diff --git a/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx b/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx index 356db5471..18ff51dc3 100644 --- a/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx +++ b/public/components/explorer/visualizations/workspace_panel/chartSwitch.tsx @@ -1,9 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + import './chart_switch.scss'; import React, { useState } from 'react'; -import { uniqueId } from 'lodash'; import { i18n } from '@osd/i18n'; -// import { FormattedMessage } from '@osd/i18n/react'; import { EuiPopover, EuiPopoverTitle, @@ -11,31 +20,13 @@ import { EuiFlexItem, EuiKeyPadMenu, EuiKeyPadMenuItem, - EuiIcon, - // EuiSelectableMessage + EuiIcon } from '@elastic/eui'; import { ToolbarButton } from '../shared_components/toolbar_button'; -// import { LensIconChartBar } from '../assets/chart_bar'; -// import { LensIconChartLine } from '../assets/chart_line'; function VisualizationSummary(vis: any) { - // const visualization = props.visualizationMap[props.visualizationId || '']; - - // if (!visualization) { - // return ( - // <> - // {i18n.translate('xpack.lens.configPanel.selectVisualization', { - // defaultMessage: 'Select a visualization', - // })} - // - // ); - // } - - // const description = visualization.getDescription(props.visualizationState); - return ( <> - { vis.label } @@ -49,7 +40,6 @@ export const ChartSwitch = ({ }: any) => { const [flyoutOpen, setFlyoutOpen] = useState(false); - // const [vis, setVis] = useState(visualizationTypes[0]); const popoverWrappedSwitch = ( - {/* - setSearchTerm(e.target.value)} - /> - */} @@ -98,7 +78,6 @@ export const ChartSwitch = ({ title={v.fullLabel} role="menuitem" data-test-subj={`lnsChartSwitchPopover_${v.id}`} - // onClick={() => commitSelection(v.selection)} onClick={() => { setVis(v); setFlyoutOpen(false); @@ -123,17 +102,6 @@ export const ChartSwitch = ({ ))} - {/* {searchTerm && (visualizationTypes || []).length === 0 && ( - - {searchTerm}, - }} - /> - - )} */} ); diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.scss b/public/components/explorer/visualizations/workspace_panel/chart_switch.scss index e0031d051..3e3dcae2f 100644 --- a/public/components/explorer/visualizations/workspace_panel/chart_switch.scss +++ b/public/components/explorer/visualizations/workspace_panel/chart_switch.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + .lnsChartSwitch__header { > * { display: flex; diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx b/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx deleted file mode 100644 index c78de9d14..000000000 --- a/public/components/explorer/visualizations/workspace_panel/chart_switch.test.tsx +++ /dev/null @@ -1,649 +0,0 @@ -/* - * 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 { ReactWrapper } from 'enzyme'; -import { - createMockVisualization, - createMockFramePublicAPI, - createMockDatasource, -} from '../../mocks'; -import { EuiKeyPadMenuItem } from '@elastic/eui'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; -import { Action } from '../state_management'; -import { ChartSwitch } from './chart_switch'; - -describe('chart_switch', () => { - function generateVisualization(id: string): jest.Mocked { - return { - ...createMockVisualization(), - id, - getVisualizationTypeId: jest.fn((_state) => id), - visualizationTypes: [ - { - icon: 'empty', - id, - label: `Label ${id}`, - }, - ], - initialize: jest.fn((_frame, state?: unknown) => { - return state || `${id} initial state`; - }), - getSuggestions: jest.fn((options) => { - return [ - { - score: 1, - title: '', - state: `suggestion ${id}`, - previewIcon: 'empty', - }, - ]; - }), - }; - } - - /** - * There are three visualizations. Each one has the same suggestion behavior: - * - * visA: suggests an empty state - * visB: suggests an empty state - * visC: - * - Never switches to subvisC2 - * - Allows a switch to subvisC3 - * - Allows a switch to subvisC1 - */ - function mockVisualizations() { - return { - visA: generateVisualization('visA'), - visB: generateVisualization('visB'), - visC: { - ...generateVisualization('visC'), - initialize: jest.fn((_frame, state) => state ?? { type: 'subvisC1' }), - visualizationTypes: [ - { - icon: 'empty', - id: 'subvisC1', - label: 'C1', - }, - { - icon: 'empty', - id: 'subvisC2', - label: 'C2', - }, - { - icon: 'empty', - id: 'subvisC3', - label: 'C3', - }, - ], - getVisualizationTypeId: jest.fn((state) => state.type), - getSuggestions: jest.fn((options) => { - if (options.subVisualizationId === 'subvisC2') { - return []; - } - // Multiple suggestions need to be filtered - return [ - { - score: 1, - title: 'Primary suggestion', - state: { type: 'subvisC3' }, - previewIcon: 'empty', - }, - { - score: 1, - title: '', - state: { type: 'subvisC1', notPrimary: true }, - previewIcon: 'empty', - }, - ]; - }), - }, - }; - } - - function mockFrame(layers: string[]) { - return { - ...createMockFramePublicAPI(), - datasourceLayers: layers.reduce( - (acc, layerId) => ({ - ...acc, - [layerId]: ({ - getTableSpec: jest.fn(() => { - return [{ columnId: 2 }]; - }), - getOperationForColumnId() { - return {}; - }, - } as unknown) as DatasourcePublicAPI, - }), - {} as Record - ), - } as FramePublicAPI; - } - - function mockDatasourceMap() { - const datasource = createMockDatasource('testDatasource'); - datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ - { - state: {}, - table: { - columns: [], - isMultiRow: true, - layerId: 'a', - changeType: 'unchanged', - }, - keptLayerIds: ['a'], - }, - ]); - return { - testDatasource: datasource, - }; - } - - function mockDatasourceStates() { - return { - testDatasource: { - state: {}, - isLoading: false, - }, - }; - } - - function showFlyout(component: ReactWrapper) { - component.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click'); - } - - function switchTo(subType: string, component: ReactWrapper) { - showFlyout(component); - component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click'); - } - - function getMenuItem(subType: string, component: ReactWrapper) { - showFlyout(component); - return component - .find(EuiKeyPadMenuItem) - .find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`) - .first(); - } - - it('should use suggested state if there is a suggestion from the target visualization', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const component = mount( - - ); - - switchTo('visB', component); - - expect(dispatch).toHaveBeenCalledWith({ - initialState: 'suggestion visB', - newVisualizationId: 'visB', - type: 'SWITCH_VISUALIZATION', - datasourceId: 'testDatasource', - datasourceState: {}, - }); - }); - - it('should use initial state if there is no suggestion from the target visualization', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - visualizations.visB.getSuggestions.mockReturnValueOnce([]); - const frame = mockFrame(['a']); - (frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]); - - const component = mount( - - ); - - switchTo('visB', component); - - expect(frame.removeLayers).toHaveBeenCalledWith(['a']); - - expect(dispatch).toHaveBeenCalledWith({ - initialState: 'visB initial state', - newVisualizationId: 'visB', - type: 'SWITCH_VISUALIZATION', - }); - }); - - it('should indicate data loss if not all columns will be used', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const frame = mockFrame(['a']); - - const datasourceMap = mockDatasourceMap(); - datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ - { - state: {}, - table: { - columns: [ - { - columnId: 'col1', - operation: { - label: '', - dataType: 'string', - isBucketed: true, - }, - }, - { - columnId: 'col2', - operation: { - label: '', - dataType: 'number', - isBucketed: false, - }, - }, - ], - layerId: 'first', - isMultiRow: true, - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - datasourceMap.testDatasource.publicAPIMock.getTableSpec.mockReturnValue([ - { columnId: 'col1' }, - { columnId: 'col2' }, - { columnId: 'col3' }, - ]); - - const component = mount( - - ); - - expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); - }); - - it('should indicate data loss if not all layers will be used', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const frame = mockFrame(['a', 'b']); - - const component = mount( - - ); - - expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); - }); - - it('should support multi-layer suggestions without data loss', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const frame = mockFrame(['a', 'b']); - - const datasourceMap = mockDatasourceMap(); - datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ - { - state: {}, - table: { - columns: [ - { - columnId: 'a', - operation: { - label: '', - dataType: 'string', - isBucketed: true, - }, - }, - ], - isMultiRow: true, - layerId: 'a', - changeType: 'unchanged', - }, - keptLayerIds: ['a', 'b'], - }, - ]); - - const component = mount( - - ); - - expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); - }); - - it('should indicate data loss if no data will be used', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - visualizations.visB.getSuggestions.mockReturnValueOnce([]); - const frame = mockFrame(['a']); - - const component = mount( - - ); - - expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert'); - }); - - it('should not indicate data loss if there is no data', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - visualizations.visB.getSuggestions.mockReturnValueOnce([]); - const frame = mockFrame(['a']); - (frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]); - - const component = mount( - - ); - - expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined(); - }); - - it('should not show a warning when the subvisualization is the same', () => { - const dispatch = jest.fn(); - const frame = mockFrame(['a', 'b', 'c']); - const visualizations = mockVisualizations(); - visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); - const switchVisualizationType = jest.fn(() => ({ type: 'subvisC1' })); - - visualizations.visC.switchVisualizationType = switchVisualizationType; - - const component = mount( - - ); - - expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); - }); - - it('should get suggestions when switching subvisualization', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - visualizations.visB.getSuggestions.mockReturnValueOnce([]); - const frame = mockFrame(['a', 'b', 'c']); - - const component = mount( - - ); - - switchTo('visB', component); - - expect(frame.removeLayers).toHaveBeenCalledTimes(1); - expect(frame.removeLayers).toHaveBeenCalledWith(['a', 'b', 'c']); - - expect(visualizations.visB.getSuggestions).toHaveBeenCalledWith( - expect.objectContaining({ - keptLayerIds: ['a'], - }) - ); - - expect(dispatch).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'SWITCH_VISUALIZATION', - initialState: 'visB initial state', - }) - ); - }); - - it('should not remove layers when switching between subtypes', () => { - const dispatch = jest.fn(); - const frame = mockFrame(['a', 'b', 'c']); - const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(() => 'switched'); - - visualizations.visC.switchVisualizationType = switchVisualizationType; - - const component = mount( - - ); - - switchTo('subvisC3', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', { type: 'subvisC3' }); - expect(dispatch).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'SWITCH_VISUALIZATION', - initialState: 'switched', - }) - ); - expect(frame.removeLayers).not.toHaveBeenCalled(); - }); - - it('should not remove layers and initialize with existing state when switching between subtypes without data', () => { - const dispatch = jest.fn(); - const frame = mockFrame(['a']); - frame.datasourceLayers.a.getTableSpec = jest.fn().mockReturnValue([]); - const visualizations = mockVisualizations(); - visualizations.visC.getSuggestions = jest.fn().mockReturnValue([]); - visualizations.visC.switchVisualizationType = jest.fn(() => 'switched'); - - const component = mount( - - ); - - switchTo('subvisC3', component); - - expect(visualizations.visC.switchVisualizationType).toHaveBeenCalledWith('subvisC3', { - type: 'subvisC1', - }); - expect(frame.removeLayers).not.toHaveBeenCalled(); - }); - - it('should switch to the updated datasource state', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const frame = mockFrame(['a', 'b']); - - const datasourceMap = mockDatasourceMap(); - datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ - { - state: 'testDatasource suggestion', - table: { - columns: [ - { - columnId: 'col1', - operation: { - label: '', - dataType: 'string', - isBucketed: true, - }, - }, - { - columnId: 'col2', - operation: { - label: '', - dataType: 'number', - isBucketed: false, - }, - }, - ], - layerId: 'a', - isMultiRow: true, - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - - const component = mount( - - ); - - switchTo('visB', component); - - expect(dispatch).toHaveBeenCalledWith({ - type: 'SWITCH_VISUALIZATION', - newVisualizationId: 'visB', - datasourceId: 'testDatasource', - datasourceState: 'testDatasource suggestion', - initialState: 'suggestion visB', - } as Action); - }); - - it('should ensure the new visualization has the proper subtype', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn( - (visualizationType, state) => `${state} ${visualizationType}` - ); - - visualizations.visB.switchVisualizationType = switchVisualizationType; - - const component = mount( - - ); - - switchTo('visB', component); - - expect(dispatch).toHaveBeenCalledWith({ - initialState: 'suggestion visB visB', - newVisualizationId: 'visB', - type: 'SWITCH_VISUALIZATION', - datasourceId: 'testDatasource', - datasourceState: {}, - }); - }); - - it('should use the suggestion that matches the subtype', () => { - const dispatch = jest.fn(); - const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(); - - visualizations.visC.switchVisualizationType = switchVisualizationType; - - const component = mount( - - ); - - switchTo('subvisC1', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC1', { - type: 'subvisC1', - notPrimary: true, - }); - }); - - it('should show all visualization types', () => { - const component = mount( - - ); - - showFlyout(component); - - const allDisplayed = ['visA', 'visB', 'subvisC1', 'subvisC2', 'subvisC3'].every( - (subType) => component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0 - ); - - expect(allDisplayed).toBeTruthy(); - }); -}); diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx b/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx deleted file mode 100644 index dec6fb077..000000000 --- a/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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 './chart_switch.scss'; -import React, { useState, useMemo } from 'react'; -import { - EuiIcon, - EuiPopover, - EuiPopoverTitle, - EuiKeyPadMenu, - EuiKeyPadMenuItem, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiSelectableMessage, -} from '@elastic/eui'; -import { flatten } from 'lodash'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage } from '@osd/i18n/react'; -// import { Visualization, FramePublicAPI, Datasource } from '../../../types'; -// import { Action } from '../state_management'; -// import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; -// import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { ToolbarButton } from '../shared_components'; - -interface VisualizationSelection { - visualizationId: string; - subVisualizationId: string; - getVisualizationState: () => unknown; - keptLayerIds: string[]; - dataLoss: 'nothing' | 'layers' | 'everything' | 'columns'; - datasourceId?: string; - datasourceState?: unknown; - sameDatasources?: boolean; -} - -interface Props { - // dispatch: (action: Action) => void; - // visualizationMap: Record; - visualizationId: string | null; - visualizationState: unknown; - // framePublicAPI: FramePublicAPI; - // datasourceMap: Record; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; -} - -function VisualizationSummary(props: Props) { - // const visualization = props.visualizationMap[props.visualizationId || '']; - const visualization = null; - if (!visualization) { - return ( - <> - {i18n.translate('xpack.lens.configPanel.selectVisualization', { - defaultMessage: 'Select a visualization', - })} - - ); - } - - const description = visualization.getDescription(props.visualizationState); - - return ( - <> - {description.icon && ( - - )} - {description.label} - - ); -} - -export function ChartSwitch(props: Props) { - const [flyoutOpen, setFlyoutOpen] = useState(false); - - const commitSelection = (selection: VisualizationSelection) => { - setFlyoutOpen(false); - - // trackUiEvent(`chart_switch`); - - switchToSuggestion( - props.dispatch, - { - ...selection, - visualizationState: selection.getVisualizationState(), - }, - 'SWITCH_VISUALIZATION' - ); - - if ( - (!selection.datasourceId && !selection.sameDatasources) || - selection.dataLoss === 'everything' - ) { - // props.framePublicAPI.removeLayers(Object.keys(props.framePublicAPI.datasourceLayers)); - } - }; - - function getSelection( - visualizationId: string, - subVisualizationId: string - ): VisualizationSelection { - const newVisualization = props.visualizationMap[visualizationId]; - const switchVisType = - props.visualizationMap[visualizationId].switchVisualizationType || - ((_type: string, initialState: unknown) => initialState); - const layers = Object.entries(props.framePublicAPI.datasourceLayers); - const containsData = layers.some( - ([_layerId, datasource]) => datasource.getTableSpec().length > 0 - ); - // Always show the active visualization as a valid selection - if ( - props.visualizationId === visualizationId && - props.visualizationState && - newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId - ) { - return { - visualizationId, - subVisualizationId, - dataLoss: 'nothing', - keptLayerIds: Object.keys(props.framePublicAPI.datasourceLayers), - getVisualizationState: () => switchVisType(subVisualizationId, props.visualizationState), - sameDatasources: true, - }; - } - - const topSuggestion = getTopSuggestion( - props, - visualizationId, - newVisualization, - subVisualizationId - ); - - let dataLoss: VisualizationSelection['dataLoss']; - - if (!containsData) { - dataLoss = 'nothing'; - } else if (!topSuggestion) { - dataLoss = 'everything'; - } else if (layers.length > 1 && layers.length !== topSuggestion.keptLayerIds.length) { - dataLoss = 'layers'; - } else if (topSuggestion.columns !== layers[0][1].getTableSpec().length) { - dataLoss = 'columns'; - } else { - dataLoss = 'nothing'; - } - - return { - visualizationId, - subVisualizationId, - dataLoss, - getVisualizationState: topSuggestion - ? () => - switchVisType( - subVisualizationId, - newVisualization.initialize(props.framePublicAPI, topSuggestion.visualizationState) - ) - : () => { - return switchVisType( - subVisualizationId, - newVisualization.initialize( - props.framePublicAPI, - props.visualizationId === newVisualization.id ? props.visualizationState : undefined - ) - ); - }, - keptLayerIds: topSuggestion ? topSuggestion.keptLayerIds : [], - datasourceState: topSuggestion ? topSuggestion.datasourceState : undefined, - datasourceId: topSuggestion ? topSuggestion.datasourceId : undefined, - sameDatasources: dataLoss === 'nothing' && props.visualizationId === newVisualization.id, - }; - } - - const [searchTerm, setSearchTerm] = useState(''); - - const visualizationTypes = useMemo( - () => - flyoutOpen && - flatten( - Object.values(props.visualizationMap).map((v) => - v.visualizationTypes.map((t) => ({ - visualizationId: v.id, - ...t, - icon: t.icon, - })) - ) - ) - .filter( - (visualizationType) => - visualizationType.label.toLowerCase().includes(searchTerm.toLowerCase()) || - (visualizationType.fullLabel && - visualizationType.fullLabel.toLowerCase().includes(searchTerm.toLowerCase())) - ) - .map((visualizationType) => ({ - ...visualizationType, - selection: getSelection(visualizationType.visualizationId, visualizationType.id), - })), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - flyoutOpen, - props.visualizationMap, - props.framePublicAPI, - props.visualizationId, - props.visualizationState, - searchTerm, - ] - ); - - const popover = ( - setFlyoutOpen(!flyoutOpen)} - data-test-subj="lnsChartSwitchPopover" - fontWeight="bold" - > - - - } - isOpen={flyoutOpen} - closePopover={() => setFlyoutOpen(false)} - anchorPosition="downLeft" - > - - - - {i18n.translate('xpack.lens.configPanel.chartType', { - defaultMessage: 'Chart type', - })} - - - setSearchTerm(e.target.value)} - /> - - - - - {(visualizationTypes || []).map((v) => ( - {v.label}} - title={v.fullLabel} - role="menuitem" - data-test-subj={`lnsChartSwitchPopover_${v.id}`} - onClick={() => commitSelection(v.selection)} - betaBadgeLabel={ - v.selection.dataLoss !== 'nothing' - ? i18n.translate('xpack.lens.chartSwitch.dataLossLabel', { - defaultMessage: 'Data loss', - }) - : undefined - } - betaBadgeTooltipContent={ - v.selection.dataLoss !== 'nothing' - ? i18n.translate('xpack.lens.chartSwitch.dataLossDescription', { - defaultMessage: 'Switching to this chart will lose some of the configuration', - }) - : undefined - } - betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined} - > - - - ))} - - {searchTerm && (visualizationTypes || []).length === 0 && ( - - {searchTerm}, - }} - /> - - )} - - ); - - return
    {popover}
    ; -} - -function getTopSuggestion( - props: Props, - visualizationId: string, - newVisualization: Visualization, - subVisualizationId?: string -): Suggestion | undefined { - const unfilteredSuggestions = getSuggestions({ - datasourceMap: props.datasourceMap, - datasourceStates: props.datasourceStates, - visualizationMap: { [visualizationId]: newVisualization }, - activeVisualizationId: props.visualizationId, - visualizationState: props.visualizationState, - subVisualizationId, - }); - const suggestions = unfilteredSuggestions.filter((suggestion) => { - // don't use extended versions of current data table on switching between visualizations - // to avoid confusing the user. - return ( - suggestion.changeType !== 'extended' && - newVisualization.getVisualizationTypeId(suggestion.visualizationState) === subVisualizationId - ); - }); - - // We prefer unchanged or reduced suggestions when switching - // charts since that allows you to switch from A to B and back - // to A with the greatest chance of preserving your original state. - return ( - suggestions.find((s) => s.changeType === 'unchanged') || - suggestions.find((s) => s.changeType === 'reduced') || - suggestions[0] - ); -} diff --git a/public/components/explorer/visualizations/workspace_panel/index.ts b/public/components/explorer/visualizations/workspace_panel/index.ts index d23afd412..54b8e6f9c 100644 --- a/public/components/explorer/visualizations/workspace_panel/index.ts +++ b/public/components/explorer/visualizations/workspace_panel/index.ts @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ export { WorkspacePanel } from './workspace_panel'; diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx deleted file mode 100644 index 82205e930..000000000 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel.test.tsx +++ /dev/null @@ -1,811 +0,0 @@ -/* - * 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 { act } from 'react-dom/test-utils'; -import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; -import { FramePublicAPI, TableSuggestion, Visualization } from '../../../types'; -import { - createMockVisualization, - createMockDatasource, - createExpressionRendererMock, - DatasourceMock, - createMockFramePublicAPI, -} from '../../mocks'; - -jest.mock('../../../debounced_component', () => { - return { - debouncedComponent: (fn: unknown) => fn, - }; -}); - -import { WorkspacePanel, WorkspacePanelProps } from './workspace_panel'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { ReactWrapper } from 'enzyme'; -import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; -import { Ast } from '@kbn/interpreter/common'; -import { coreMock } from 'src/core/public/mocks'; -import { - DataPublicPluginStart, - esFilters, - IFieldType, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; -import { TriggerId, UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; -import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; -import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; - -describe('workspace_panel', () => { - let mockVisualization: jest.Mocked; - let mockVisualization2: jest.Mocked; - let mockDatasource: DatasourceMock; - - let expressionRendererMock: jest.Mock; - let uiActionsMock: jest.Mocked; - let dataMock: jest.Mocked; - let trigger: jest.Mocked>; - - let instance: ReactWrapper; - - beforeEach(() => { - trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; - uiActionsMock = uiActionsPluginMock.createStartContract(); - dataMock = dataPluginMock.createStartContract(); - uiActionsMock.getTrigger.mockReturnValue(trigger); - mockVisualization = createMockVisualization(); - mockVisualization2 = createMockVisualization(); - - mockDatasource = createMockDatasource('a'); - - expressionRendererMock = createExpressionRendererMock(); - }); - - afterEach(() => { - instance.unmount(); - }); - - it('should render an explanatory text if no visualization is active', () => { - instance = mount( - {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); - - it('should render an explanatory text if the visualization does not produce an expression', () => { - instance = mount( - null }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); - - it('should render an explanatory text if the datasource does not produce an expression', () => { - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); - - it('should render the resulting expression using the expression renderer', () => { - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "kibana", - "type": "function", - }, - Object { - "arguments": Object { - "layerIds": Array [ - "first", - ], - "tables": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - ], - "type": "expression", - }, - ], - }, - "function": "lens_merge_tables", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", - } - `); - }); - - it('should execute a trigger on expression event', () => { - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; - - const eventData = {}; - onEvent({ name: 'brush', data: eventData }); - - expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); - expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); - }); - - it('should include data fetching for each layer in the expression', () => { - const mockDatasource2 = createMockDatasource('a'); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - second: mockDatasource2.publicAPIMock, - }; - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - - mockDatasource2.toExpression.mockReturnValue('datasource2'); - mockDatasource2.getLayers.mockReturnValue(['second', 'third']); - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds - ).toEqual(['first', 'second', 'third']); - expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables - ).toMatchInlineSnapshot(` - Array [ - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - ], - "type": "expression", - }, - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource2", - "type": "function", - }, - ], - "type": "expression", - }, - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource2", - "type": "function", - }, - ], - "type": "expression", - }, - ] - `); - }); - - it('should run the expression again if the date range changes', async () => { - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - mockDatasource.getLayers.mockReturnValue(['first']); - - mockDatasource.toExpression - .mockReturnValueOnce('datasource') - .mockReturnValueOnce('datasource second'); - - expressionRendererMock = jest.fn((_arg) => ); - - await act(async () => { - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - }); - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - - await act(async () => { - instance.setProps({ - framePublicAPI: { - ...framePublicAPI, - dateRange: { fromDate: 'now-90d', toDate: 'now-30d' }, - }, - }); - }); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(2); - }); - - it('should run the expression again if the filters change', async () => { - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - mockDatasource.getLayers.mockReturnValue(['first']); - - mockDatasource.toExpression - .mockReturnValueOnce('datasource') - .mockReturnValueOnce('datasource second'); - - expressionRendererMock = jest.fn((_arg) => ); - await act(async () => { - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - }); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - - await act(async () => { - instance.setProps({ - framePublicAPI: { - ...framePublicAPI, - filters: [esFilters.buildExistsFilter(field, indexPattern)], - }, - }); - }); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(2); - }); - - it('should show an error message if the expression fails to parse', () => { - mockDatasource.toExpression.mockReturnValue('|||'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - - expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); - expect(instance.find(expressionRendererMock)).toHaveLength(0); - }); - - it('should not attempt to run the expression again if it does not change', async () => { - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - - await act(async () => { - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - }); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - }); - - it('should attempt to run the expression again if it changes', async () => { - mockDatasource.toExpression.mockReturnValue('datasource'); - mockDatasource.getLayers.mockReturnValue(['first']); - const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - - await act(async () => { - instance = mount( - 'vis' }, - }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={coreMock.createSetup()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - /> - ); - }); - - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(1); - - expressionRendererMock.mockImplementation((_) => { - return ; - }); - - instance.setProps({ visualizationState: {} }); - instance.update(); - - expect(expressionRendererMock).toHaveBeenCalledTimes(2); - - expect(instance.find(expressionRendererMock)).toHaveLength(1); - }); - - describe('suggestions from dropping in workspace panel', () => { - let mockDispatch: jest.Mock; - let frame: jest.Mocked; - - const draggedField: unknown = {}; - - beforeEach(() => { - frame = createMockFramePublicAPI(); - mockDispatch = jest.fn(); - }); - - function initComponent(draggingContext: unknown = draggedField) { - instance = mount( - {}}> - - - ); - } - - it('should immediately transition if exactly one suggestion is returned', () => { - const expectedTable: TableSuggestion = { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }; - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: expectedTable, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - - instance.find(DragDrop).prop('onDrop')!(draggedField); - - expect(mockDatasource.getDatasourceSuggestionsForField).toHaveBeenCalledTimes(1); - expect(mockVisualization.getSuggestions).toHaveBeenCalledWith( - expect.objectContaining({ - table: expectedTable, - }) - ); - expect(mockDispatch).toHaveBeenCalledWith({ - type: 'SWITCH_VISUALIZATION', - newVisualizationId: 'vis', - initialState: {}, - datasourceState: {}, - datasourceId: 'mock', - }); - }); - - it('should allow to drop if there are suggestions', () => { - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); - }); - - it('should refuse to drop if there only suggestions from other visualizations if there are data tables', () => { - frame.datasourceLayers.a = mockDatasource.publicAPIMock; - mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization2.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); - }); - - it('should allow to drop if there are suggestions from active visualization even if there are data tables', () => { - frame.datasourceLayers.a = mockDatasource.publicAPIMock; - mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'a' }]); - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - layerId: '1', - columns: [], - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'my title', - state: {}, - previewIcon: 'empty', - }, - ]); - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); - }); - - it('should refuse to drop if there are no suggestions', () => { - initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); - }); - - it('should immediately transition to the first suggestion if there are multiple', () => { - mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ - { - state: {}, - table: { - isMultiRow: true, - columns: [], - layerId: '1', - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - { - state: {}, - table: { - isMultiRow: true, - columns: [], - layerId: '1', - changeType: 'unchanged', - }, - keptLayerIds: [], - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.5, - title: 'second suggestion', - state: {}, - previewIcon: 'empty', - }, - ]); - mockVisualization.getSuggestions.mockReturnValueOnce([ - { - score: 0.8, - title: 'first suggestion', - state: { - isFirst: true, - }, - previewIcon: 'empty', - }, - ]); - - initComponent(); - instance.find(DragDrop).prop('onDrop')!(draggedField); - - expect(mockDispatch).toHaveBeenCalledWith({ - type: 'SWITCH_VISUALIZATION', - newVisualizationId: 'vis', - initialState: { - isFirst: true, - }, - datasourceState: {}, - datasourceId: 'mock', - }); - }); - }); -}); diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx index 13e926c1d..8a7efb1ad 100644 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx @@ -1,53 +1,22 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ -import React, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; +import React, { useState } from 'react'; import { uniqueId } from 'lodash'; -import classNames from 'classnames'; -import { FormattedMessage } from '@osd/i18n/react'; -import { Ast } from '@osd/interpreter/common'; -import { i18n } from '@osd/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -// import { CoreStart, CoreSetup } from 'kibana/public'; -import { ExecutionContextSearch } from 'src/plugins/expressions'; -import { - ExpressionRendererEvent, - ExpressionRenderError, - ReactExpressionRendererType, -} from '../../../../../../../src/plugins/expressions/public'; -// import { Action } from '../state_management'; -// import { -// Datasource, -// Visualization, -// FramePublicAPI, -// isLensBrushEvent, -// isLensFilterEvent, -// } from '../../../types'; -import { DragDrop, DragContext } from '../drag_drop'; -// import { getSuggestions, switchToSuggestion } from '../suggestion_helpers'; -// import { buildExpression } from '../expression_helpers'; -import { debouncedComponent } from '../../../common/debounced_component'; -// import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { - UiActionsStart, - VisualizeFieldContext, -} from '../../../../../../../src/plugins/ui_actions/public'; -// import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public'; -import { - DataPublicPluginStart, - TimefilterContract, -} from '../../../../../../../src/plugins/data/public'; +import { DragDrop } from '../drag_drop'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; -// import { DropIllustration } from '../../../assets/drop_illustration'; -// import { getOriginalRequestErrorMessage } from '../../error_helper'; import { Bar } from '../../../visualizations/charts/bar'; import { Line } from '../../../visualizations/charts/line'; import { LensIconChartBar } from '../assets/chart_bar'; import { LensIconChartLine } from '../assets/chart_line'; -// import { vis } from 'src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks'; const visualizationTypes = [ { @@ -59,7 +28,12 @@ const visualizationTypes = [ selection: { dataLoss: 'nothing' }, - chart: Bar + chart: }, { id: 'line', @@ -74,149 +48,24 @@ const visualizationTypes = [ } ]; -export interface WorkspacePanelProps { - activeVisualizationId: string | null; - // visualizationMap: Record; - visualizationState: unknown; - activeDatasourceId: string | null; - // datasourceMap: Record; - datasourceStates: Record< - string, - { - state: unknown; - isLoading: boolean; - } - >; - // framePublicAPI: FramePublicAPI; - // dispatch: (action: Action) => void; - ExpressionRenderer: ReactExpressionRendererType; - // core: CoreStart | CoreSetup; - plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; - title?: string; - visualizeTriggerFieldContext?: VisualizeFieldContext; -} - -interface WorkspaceState { - expressionBuildError: string | undefined; - expandError: boolean; -} - // Exported for testing purposes only. -export function WorkspacePanel({ - // activeDatasourceId, - // activeVisualizationId, - // visualizationMap, - // visualizationState, - // datasourceMap, - // datasourceStates, - // framePublicAPI, - // dispatch, - // core, - // plugins, - ExpressionRenderer: ExpressionRendererComponent, - title, - // visualizeTriggerFieldContext, -}: WorkspacePanelProps) { - const dragDropContext = useContext(DragContext); +export function WorkspacePanel({ title }: any) { const [vis, setVis] = useState(visualizationTypes[0]); - console.log('outer vis: ', vis); - - function onDrop() { - // if (suggestionForDraggedField) { - // trackUiEvent('drop_onto_workspace'); - // trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); - // switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); - // } - } - - function renderEmptyWorkspace() { - return ( - -

    - - {true - ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { - // defaultMessage: 'Drop some fields here to start', - defaultMessage: 'Use PPL stats commandin query to render visualization', - }) - : i18n.translate('xpack.lens.editorFrame.emptyWorkspaceSimple', { - defaultMessage: 'Drop field here', - })} - -

    - {/* */} - {true === null && ( - <> -

    - {i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { - defaultMessage: 'Lens is a new tool for creating visualization', - })} -

    -

    - - - {i18n.translate('xpack.lens.editorFrame.goToForums', { - defaultMessage: 'Make requests and give feedback', - })} - - -

    - - )} -
    - ); - } + function onDrop() {} function renderVisualization() { - // we don't want to render the emptyWorkspace on visualizing field from Discover - // as it is specific for the drag and drop functionality and can confuse the users - // return renderEmptyWorkspace(); - // if (expression === null && !visualizeTriggerFieldContext) { - // return renderEmptyWorkspace(); - // } - // return ( - // - // ); - // console.log('vis: ', vis); - // return ; - return vis.chart(); + return vis.chart; } return ( {}} emptyExpression={true} setVis={ setVis } vis={ vis } visualizationTypes={ visualizationTypes } - // visualizationState={visualizationState} - // visualizationId={activeVisualizationId} - // datasourceStates={datasourceStates} - // datasourceMap={datasourceMap} - // visualizationMap={visualizationMap} >
    {renderVisualization()} - {/* {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()} */} - {/* {renderEmptyWorkspace()} */}
    ); -} - -export const InnerVisualizationWrapper = ({ - expression, - framePublicAPI, - timefilter, - onEvent, - setLocalState, - localState, - ExpressionRendererComponent, -}: { - expression: Ast | null | undefined; - // framePublicAPI: FramePublicAPI; - timefilter: TimefilterContract; - onEvent: (event: ExpressionRendererEvent) => void; - setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void; - localState: WorkspaceState; - ExpressionRendererComponent: ReactExpressionRendererType; -}) => { - const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]); - - const context: ExecutionContextSearch = useMemo( - () => ({ - query: framePublicAPI.query, - timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, - }, - filters: framePublicAPI.filters, - }), - [ - framePublicAPI.query, - framePublicAPI.dateRange.fromDate, - framePublicAPI.dateRange.toDate, - framePublicAPI.filters, - ] - ); - - if (localState.expressionBuildError) { - return ( - - - - - - - - {localState.expressionBuildError} - - ); - } - return ( -
    - { - // const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; - return ( - - - - - - - - {false ? ( - - { - setLocalState((prevState: WorkspaceState) => ({ - ...prevState, - expandError: !prevState.expandError, - })); - }} - > - {i18n.translate('xpack.lens.editorFrame.expandRenderingErrorButton', { - defaultMessage: 'Show details of error', - })} - - - {/* {localState.expandError ? visibleErrorMessage : null} */} - - ) : null} - - ); - }} - /> -
    - ); -}; - -export const VisualizationWrapper = debouncedComponent(InnerVisualizationWrapper); +} \ No newline at end of file diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss index 21b7da48f..689e4fdf1 100644 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + @import '../mixins'; .lnsWorkspacePanelWrapper { diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx deleted file mode 100644 index f7ae77536..000000000 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { Visualization } from '../../../types'; -import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../mocks'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { ReactWrapper } from 'enzyme'; -import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper'; - -describe('workspace_panel_wrapper', () => { - let mockVisualization: jest.Mocked; - let mockFrameAPI: FrameMock; - let instance: ReactWrapper; - - beforeEach(() => { - mockVisualization = createMockVisualization(); - mockFrameAPI = createMockFramePublicAPI(); - }); - - afterEach(() => { - instance.unmount(); - }); - - it('should render its children', () => { - const MyChild = () => The child elements; - instance = mount( - - - - ); - - expect(instance.find(MyChild)).toHaveLength(1); - }); - - it('should call the toolbar renderer if provided', () => { - const renderToolbarMock = jest.fn(); - const visState = { internalState: 123 }; - instance = mount( - } - visualizationId="myVis" - visualizationMap={{ myVis: { ...mockVisualization, renderToolbar: renderToolbarMock } }} - datasourceMap={{}} - datasourceStates={{}} - emptyExpression={false} - /> - ); - - expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), { - state: visState, - frame: mockFrameAPI, - setState: expect.anything(), - }); - }); -}); diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx index 8cf2aff26..165b7beac 100644 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx +++ b/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx @@ -1,12 +1,17 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import './workspace_panel_wrapper.scss'; -import React, { useCallback } from 'react'; +import React from 'react'; import { i18n } from '@osd/i18n'; import classNames from 'classnames'; import { @@ -16,61 +21,16 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -// import { Datasource, FramePublicAPI, Visualization } from '../../../types'; -// import { NativeRenderer } from '../../../native_renderer'; -// import { Action } from '../state_management'; import { ChartSwitch } from './chartSwitch'; -export interface WorkspacePanelWrapperProps { - children: React.ReactNode | React.ReactNode[]; - // framePublicAPI: FramePublicAPI; - visualizationState: unknown; - // dispatch: (action: Action) => void; - emptyExpression: boolean; - title?: string; - // visualizationMap: Record; - visualizationId: string | null; - // datasourceMap: Record; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; -} - export function WorkspacePanelWrapper({ children, - // framePublicAPI, - // visualizationState, - // dispatch, title, emptyExpression, setVis, vis, visualizationTypes - // visualizationId, - // visualizationMap, - // datasourceMap, - // datasourceStates, -}: WorkspacePanelWrapperProps) { - // const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; - const setVisualizationState = useCallback( - (newState: unknown) => { - // if (!activeVisualization) { - // return; - // } - // dispatch({ - // type: 'UPDATE_VISUALIZATION_STATE', - // visualizationId: activeVisualization.id, - // newState, - // clearStagedPreview: false, - // }); - }, - [] - // [dispatch, activeVisualization] - ); +}: any) { return ( <>
    @@ -87,28 +47,8 @@ export function WorkspacePanelWrapper({ setVis={ setVis } vis={ vis } visualizationTypes={ visualizationTypes } - // visualizationMap={visualizationMap} - // visualizationId={visualizationId} - // visualizationState={visualizationState} - // datasourceMap={datasourceMap} - // datasourceStates={datasourceStates} - // dispatch={dispatch} - // framePublicAPI={framePublicAPI} /> - {/* {activeVisualization && activeVisualization.renderToolbar && ( - - - - )} */}
    diff --git a/public/components/visualizations/charts/index.ts b/public/components/visualizations/charts/index.ts deleted file mode 100644 index 2500a2228..000000000 --- a/public/components/visualizations/charts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './visualization'; \ No newline at end of file diff --git a/public/components/visualizations/charts/line.tsx b/public/components/visualizations/charts/line.tsx index 5e4007bd1..ac79bc106 100644 --- a/public/components/visualizations/charts/line.tsx +++ b/public/components/visualizations/charts/line.tsx @@ -9,7 +9,7 @@ * GitHub history for details. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { Plt } from '../plotly/plot'; export const Line = (props: any) => { @@ -18,6 +18,7 @@ export const Line = (props: any) => { { yaxis: { fixedrange: true, showgrid: false, - visible: true, - // range: [0, maxY * 1.1], - }, - // margin: { - // l: 0, - // r: 0, - // b: 0, - // t: 0, - // pad: 0, - // }, - // height: '100%', - // width: '100%', + visible: true + } }} /> ); diff --git a/public/components/visualizations/charts/visualization.tsx b/public/components/visualizations/charts/visualization.tsx deleted file mode 100644 index 86d2ed98b..000000000 --- a/public/components/visualizations/charts/visualization.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import React from 'react'; - -export const Visualization = ( - { - Chart, - ...visConfig - }: { - Chart: React.ReactDOM, - visConfig: any - } -) => { - return ; -}; \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index d129ca559..8e8ea7c16 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -40,7 +40,6 @@ export class ObservabilityPlugin implements Plugin this.visId; - - getCategory = () => this.category; - - getVisConfig = () => this.visConfig; - - getTypes = () => this.types; - -} \ No newline at end of file diff --git a/public/services/visualizations/xyVisualization.ts b/public/services/visualizations/xyVisualization.ts deleted file mode 100644 index d1940333c..000000000 --- a/public/services/visualizations/xyVisualization.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { VisualizationBase } from './visualizationBase'; - -export class XYVisualization extends VisualizationBase { - constructor( - visualizationId: string, - category: string, - configuration?: any, - types?:any - ) { - super( - visualizationId, - category, - configuration, - types - ); - } - - setup = () => {}; -} \ No newline at end of file From 3cfe933078e92db256581afde12bf10cf220fd89 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 5 Aug 2021 23:47:38 -0700 Subject: [PATCH 14/16] license and minor changes --- .../debounced_component.test.tsx | 11 +- .../debounced_component.tsx | 11 +- .../common/debounced_component/index.ts | 11 +- public/components/explorer/explorer.tsx | 19 +-- .../explorer/hooks/useFetchQueryResponse.ts | 24 ++-- .../visualizations/drag_drop/drag_drop.scss | 11 ++ .../drag_drop/drag_drop.test.tsx | 11 +- .../visualizations/drag_drop/drag_drop.tsx | 11 +- .../visualizations/drag_drop/index.ts | 11 +- .../drag_drop/providers.test.tsx | 11 +- .../visualizations/drag_drop/providers.tsx | 11 +- .../lens_ui_telemetry/factory.test.ts | 109 ---------------- .../lens_ui_telemetry/factory.ts | 122 ------------------ .../visualizations/lens_ui_telemetry/index.ts | 7 - .../shared_components/empty_placeholder.tsx | 13 +- .../visualizations/shared_components/index.ts | 11 +- .../legend_settings_popover.test.tsx | 11 +- .../legend_settings_popover.tsx | 11 +- .../shared_components/toolbar_button.scss | 11 ++ .../shared_components/toolbar_button.tsx | 11 +- .../shared_components/toolbar_popover.tsx | 11 +- 21 files changed, 153 insertions(+), 306 deletions(-) delete mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts delete mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/factory.ts delete mode 100644 public/components/explorer/visualizations/lens_ui_telemetry/index.ts diff --git a/public/components/common/debounced_component/debounced_component.test.tsx b/public/components/common/debounced_component/debounced_component.test.tsx index 929dd8e43..4ee78b406 100644 --- a/public/components/common/debounced_component/debounced_component.test.tsx +++ b/public/components/common/debounced_component/debounced_component.test.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/common/debounced_component/debounced_component.tsx b/public/components/common/debounced_component/debounced_component.tsx index 0e148798c..80af8fc9d 100644 --- a/public/components/common/debounced_component/debounced_component.tsx +++ b/public/components/common/debounced_component/debounced_component.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React, { useState, useMemo, useEffect, memo, FunctionComponent } from 'react'; diff --git a/public/components/common/debounced_component/index.ts b/public/components/common/debounced_component/index.ts index ed940fed5..d23b30a26 100644 --- a/public/components/common/debounced_component/index.ts +++ b/public/components/common/debounced_component/index.ts @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ export * from './debounced_component'; diff --git a/public/components/explorer/explorer.tsx b/public/components/explorer/explorer.tsx index 86c72ffca..56d399b92 100644 --- a/public/components/explorer/explorer.tsx +++ b/public/components/explorer/explorer.tsx @@ -21,8 +21,7 @@ import { EuiTabbedContent, EuiTabbedContentTab, EuiFlexGroup, - EuiFlexItem, - EuiSpacer + EuiFlexItem } from '@elastic/eui'; import classNames from 'classnames'; import { Search } from '../common/seach/search'; @@ -380,16 +379,12 @@ export const Explorer = ({ } const handleQueryChange = (query, tabId) => { - dispatch( - changeQuery( - { - tabId, - query: { - [RAW_QUERY]: query - } - } - ) - ); + dispatch(changeQuery({ + tabId, + query: { + [RAW_QUERY]: query + } + })); } return ( diff --git a/public/components/explorer/hooks/useFetchQueryResponse.ts b/public/components/explorer/hooks/useFetchQueryResponse.ts index 3cf94886d..fd231f316 100644 --- a/public/components/explorer/hooks/useFetchQueryResponse.ts +++ b/public/components/explorer/hooks/useFetchQueryResponse.ts @@ -43,19 +43,17 @@ export const useFetchQueryResponse = ({ }) .then((res) => { batch(() => { - dispatch( - fetchSuccess({ - tabId: requestParams.tabId, - data: res - })); - dispatch( - updateFields({ - tabId: requestParams.tabId, - data: { - [SELECTED_FIELDS]: [], - [UNSELECTED_FIELDS]: res?.schema - } - })); + dispatch(fetchSuccess({ + tabId: requestParams.tabId, + data: res + })); + dispatch(updateFields({ + tabId: requestParams.tabId, + data: { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: res?.schema + } + })); }); }) .catch((err) => { diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.scss b/public/components/explorer/visualizations/drag_drop/drag_drop.scss index 410aaef9a..1b503f020 100644 --- a/public/components/explorer/visualizations/drag_drop/drag_drop.scss +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + @import '../variables'; @import '../mixins'; diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx b/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx index b1cc4c06c..a6bb722a1 100644 --- a/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.tsx b/public/components/explorer/visualizations/drag_drop/drag_drop.tsx index 69f03a254..c87f83757 100644 --- a/public/components/explorer/visualizations/drag_drop/drag_drop.tsx +++ b/public/components/explorer/visualizations/drag_drop/drag_drop.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import './drag_drop.scss'; diff --git a/public/components/explorer/visualizations/drag_drop/index.ts b/public/components/explorer/visualizations/drag_drop/index.ts index e597bb8b6..35184784f 100644 --- a/public/components/explorer/visualizations/drag_drop/index.ts +++ b/public/components/explorer/visualizations/drag_drop/index.ts @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ export * from './providers'; diff --git a/public/components/explorer/visualizations/drag_drop/providers.test.tsx b/public/components/explorer/visualizations/drag_drop/providers.test.tsx index 2a8735be4..3527011b5 100644 --- a/public/components/explorer/visualizations/drag_drop/providers.test.tsx +++ b/public/components/explorer/visualizations/drag_drop/providers.test.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React, { useContext } from 'react'; diff --git a/public/components/explorer/visualizations/drag_drop/providers.tsx b/public/components/explorer/visualizations/drag_drop/providers.tsx index 3e2b73122..80b6fbf7c 100644 --- a/public/components/explorer/visualizations/drag_drop/providers.tsx +++ b/public/components/explorer/visualizations/drag_drop/providers.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React, { useState, useMemo } from 'react'; diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts b/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts deleted file mode 100644 index fa7747dd1..000000000 --- a/public/components/explorer/visualizations/lens_ui_telemetry/factory.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 { - LensReportManager, - setReportManager, - stopReportManager, - trackUiEvent, - trackSuggestionEvent, -} from './factory'; -import { coreMock } from 'src/core/public/mocks'; -import { HttpSetup } from 'kibana/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; - -jest.useFakeTimers(); - -const createMockStorage = () => { - let lastData = { events: {}, suggestionEvents: {} }; - return { - get: jest.fn().mockImplementation(() => lastData), - set: jest.fn().mockImplementation((key, value) => { - lastData = value; - }), - remove: jest.fn(), - clear: jest.fn(), - }; -}; - -describe('Lens UI telemetry', () => { - let storage: jest.Mocked; - let http: jest.Mocked; - let dateSpy: jest.SpyInstance; - - beforeEach(() => { - dateSpy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2019, 9, 23)).valueOf()); - - storage = createMockStorage(); - http = coreMock.createSetup().http; - http.post.mockClear(); - const fakeManager = new LensReportManager({ - http, - storage, - }); - setReportManager(fakeManager); - }); - - afterEach(() => { - stopReportManager(); - dateSpy.mockRestore(); - }); - - it('should write immediately and track local state', () => { - trackUiEvent('loaded'); - - expect(storage.set).toHaveBeenCalledWith('lens-ui-telemetry', { - events: expect.any(Object), - suggestionEvents: {}, - }); - - trackSuggestionEvent('reload'); - - expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', { - events: expect.any(Object), - suggestionEvents: expect.any(Object), - }); - }); - - it('should post the results after waiting 10 seconds, if there is data', async () => { - jest.runOnlyPendingTimers(); - - http.post.mockResolvedValue({}); - - expect(http.post).not.toHaveBeenCalled(); - expect(storage.set).toHaveBeenCalledTimes(0); - - trackUiEvent('load'); - expect(storage.set).toHaveBeenCalledTimes(1); - - jest.runOnlyPendingTimers(); - - expect(http.post).toHaveBeenCalledWith(`/api/lens/stats`, { - body: JSON.stringify({ - events: { - '2019-10-23': { - load: 1, - }, - }, - suggestionEvents: {}, - }), - }); - }); - - it('should keep its local state after an http error', () => { - http.post.mockRejectedValue('http error'); - - trackUiEvent('load'); - expect(storage.set).toHaveBeenCalledTimes(1); - - jest.runOnlyPendingTimers(); - - expect(http.post).toHaveBeenCalled(); - expect(storage.set).toHaveBeenCalledTimes(1); - }); -}); diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts b/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts deleted file mode 100644 index 8f9ce7f2c..000000000 --- a/public/components/explorer/visualizations/lens_ui_telemetry/factory.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 moment from 'moment'; -import { HttpSetup } from 'kibana/public'; - -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { BASE_API_URL } from '../../common'; - -const STORAGE_KEY = 'lens-ui-telemetry'; - -let reportManager: LensReportManager; - -export function setReportManager(newManager: LensReportManager) { - if (reportManager) { - reportManager.stop(); - } - reportManager = newManager; -} - -export function stopReportManager() { - if (reportManager) { - reportManager.stop(); - } -} - -export function trackUiEvent(name: string) { - if (reportManager) { - reportManager.trackEvent(name); - } -} - -export function trackSuggestionEvent(name: string) { - if (reportManager) { - reportManager.trackSuggestionEvent(name); - } -} - -export class LensReportManager { - private events: Record> = {}; - private suggestionEvents: Record> = {}; - - private storage: IStorageWrapper; - private http: HttpSetup; - private timer: ReturnType; - - constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) { - this.storage = storage; - this.http = http; - - this.readFromStorage(); - - this.timer = setInterval(() => { - this.postToServer(); - }, 10000); - } - - public trackEvent(name: string) { - this.readFromStorage(); - this.trackTo(this.events, name); - } - - public trackSuggestionEvent(name: string) { - this.readFromStorage(); - this.trackTo(this.suggestionEvents, name); - } - - public stop() { - if (this.timer) { - clearInterval(this.timer); - } - } - - private readFromStorage() { - const data = this.storage.get(STORAGE_KEY); - if (data && typeof data.events === 'object' && typeof data.suggestionEvents === 'object') { - this.events = data.events; - this.suggestionEvents = data.suggestionEvents; - } - } - - private async postToServer() { - this.readFromStorage(); - if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { - try { - await this.http.post(`${BASE_API_URL}/stats`, { - body: JSON.stringify({ - events: this.events, - suggestionEvents: this.suggestionEvents, - }), - }); - this.events = {}; - this.suggestionEvents = {}; - this.write(); - } catch (e) { - // Silent error because events will be reported during the next timer - } - } - } - - private trackTo(target: Record>, name: string) { - const date = moment().utc().format('YYYY-MM-DD'); - if (!target[date]) { - target[date] = { - [name]: 1, - }; - } else if (!target[date][name]) { - target[date][name] = 1; - } else { - target[date][name] += 1; - } - - this.write(); - } - - private write() { - this.storage.set(STORAGE_KEY, { events: this.events, suggestionEvents: this.suggestionEvents }); - } -} diff --git a/public/components/explorer/visualizations/lens_ui_telemetry/index.ts b/public/components/explorer/visualizations/lens_ui_telemetry/index.ts deleted file mode 100644 index 79575a59f..000000000 --- a/public/components/explorer/visualizations/lens_ui_telemetry/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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. - */ - -export * from './factory'; diff --git a/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx b/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx index a2ea5c10d..1b4369350 100644 --- a/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx +++ b/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx @@ -1,12 +1,17 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@osd/i18n/react'; export const EmptyPlaceholder = (props: { icon: IconType }) => ( <> diff --git a/public/components/explorer/visualizations/shared_components/index.ts b/public/components/explorer/visualizations/shared_components/index.ts index c0362a566..a282e7008 100644 --- a/public/components/explorer/visualizations/shared_components/index.ts +++ b/public/components/explorer/visualizations/shared_components/index.ts @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ export * from './empty_placeholder'; diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx b/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx index 1e0e6b33b..9d5a55794 100644 --- a/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx +++ b/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx b/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx index 452a75400..0090095f4 100644 --- a/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx +++ b/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React from 'react'; diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.scss b/public/components/explorer/visualizations/shared_components/toolbar_button.scss index 61b02f476..437d06258 100644 --- a/public/components/explorer/visualizations/shared_components/toolbar_button.scss +++ b/public/components/explorer/visualizations/shared_components/toolbar_button.scss @@ -1,3 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + .lnsToolbarButton { line-height: $euiButtonHeight; // Keeps alignment of text and chart icon background-color: $euiColorEmptyShade; diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx index 355116db4..c21b94f87 100644 --- a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx +++ b/public/components/explorer/visualizations/shared_components/toolbar_button.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import './toolbar_button.scss'; diff --git a/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx b/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx index 679f3a44b..1bfb4197d 100644 --- a/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx +++ b/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx @@ -1,7 +1,12 @@ /* - * 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. + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React, { useState } from 'react'; From ddf855113ed49de2b0783b271442af185e800ce1 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Fri, 6 Aug 2021 14:38:09 -0700 Subject: [PATCH 15/16] changes for code review --- package.json | 10 +++++----- public/common/types/explorer.ts | 5 ----- public/components/explorer/logExplorer.tsx | 3 ++- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 790437338..5cd821270 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "main": "index.ts", "license": "Apache-2.0", "scripts": { - "test:cypress": "cypress run" - }, - "engines": { - "node": "10.23.1", - "yarn": "^1.22.10" + "osd": "node ../../scripts/osd", + "build": "yarn plugin_helpers build", + "cypress:run": "cypress run", + "cypress:open": "cypress open", + "plugin_helpers": "node ../../scripts/plugin_helpers" }, "dependencies": { "@reduxjs/toolkit": "^1.6.1", diff --git a/public/common/types/explorer.ts b/public/common/types/explorer.ts index 79b8de6b1..f9f442623 100644 --- a/public/common/types/explorer.ts +++ b/public/common/types/explorer.ts @@ -53,10 +53,5 @@ export interface IExplorerFields { } export interface IExplorerProps { - tabId: string; pplService: any; -} - -export interface LogExplorer { - } \ No newline at end of file diff --git a/public/components/explorer/logExplorer.tsx b/public/components/explorer/logExplorer.tsx index a8236b8d6..caf8b5d53 100644 --- a/public/components/explorer/logExplorer.tsx +++ b/public/components/explorer/logExplorer.tsx @@ -24,6 +24,7 @@ import { EuiTabbedContent } from '@elastic/eui'; import { Explorer } from './explorer'; +import { IExplorerProps } from '../../common/types/explorer'; import { TAB_TITLE, TAB_ID_TXT_PFX @@ -49,7 +50,7 @@ import { export const LogExplorer = ({ pplService, -}: any) => { +}: IExplorerProps) => { const dispatch = useDispatch(); const tabIds = useSelector(selectQueryTabs)['queryTabIds']; From 74b6b5661a88a2f554c47e1e733bc925ca0ed788 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Fri, 6 Aug 2021 14:47:16 -0700 Subject: [PATCH 16/16] removed few comments --- .../timechart_header/timechart_header.tsx | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/public/components/explorer/timechart_header/timechart_header.tsx b/public/components/explorer/timechart_header/timechart_header.tsx index 1fe370610..a21ac0bbd 100644 --- a/public/components/explorer/timechart_header/timechart_header.tsx +++ b/public/components/explorer/timechart_header/timechart_header.tsx @@ -16,7 +16,6 @@ import { EuiToolTip, EuiText, EuiSelect, - EuiIconTip, } from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; @@ -88,10 +87,6 @@ export function TimechartHeader({ onChangeInterval(e.target.value); }; - // if (!timeRange || !bucketInterval) { - // return null; - // } - return ( @@ -102,15 +97,7 @@ export function TimechartHeader({ })} delay="long" > - - {/* {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ - interval !== 'auto' - ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { - defaultMessage: 'per', - }) - : '' - }`} */} - + @@ -132,31 +119,7 @@ export function TimechartHeader({ })} value={interval} onChange={handleIntervalChange} - append={ undefined - // bucketInterval.scaled ? ( - // 1 - // ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { - // defaultMessage: 'buckets that are too large', - // }) - // : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { - // defaultMessage: 'too many buckets', - // }), - // bucketIntervalDescription: bucketInterval.description, - // }, - // })} - // color="warning" - // size="s" - // type="alert" - // /> - // ) : undefined - } + append={ undefined } />