diff --git a/licenses.yaml b/licenses.yaml index 8dea5baac4719..9050ac88aaa05 100644 --- a/licenses.yaml +++ b/licenses.yaml @@ -5367,6 +5367,24 @@ version: 0.20.5 --- +name: "@druid-toolkit/visuals-core" +license_category: binary +module: web-console +license_name: Apache License version 2.0 +copyright: Imply Data +version: 0.3.3 + +--- + +name: "@druid-toolkit/visuals-react" +license_category: binary +module: web-console +license_name: Apache License version 2.0 +copyright: Imply Data +version: 0.3.3 + +--- + name: "@emotion/cache" license_category: binary module: web-console @@ -5935,6 +5953,15 @@ license_file_path: licenses/bin/dot-case.MIT --- +name: "echarts" +license_category: binary +module: web-console +license_name: Apache License version 2.0 +copyright: Apache Software Foundation +version: 5.4.2 + +--- + name: "emotion" license_category: binary module: web-console @@ -6809,7 +6836,7 @@ license_category: binary module: web-console license_name: Zero-Clause BSD copyright: Microsoft Corp. -version: 2.5.3 +version: 2.3.0 license_file_path: licenses/bin/tslib.0BSD --- @@ -6844,6 +6871,16 @@ license_file_path: licenses/bin/upper-case.MIT --- +name: "use-resize-observer" +license_category: binary +module: web-console +license_name: MIT License +copyright: Viktor Hubert +version: 9.1.0 +license_file_path: licenses/bin/use-resize-observer.MIT + +--- + name: "use-sync-external-store" license_category: binary module: web-console @@ -6884,6 +6921,16 @@ license_file_path: licenses/bin/yaml.ISC --- +name: "zrender" +license_category: binary +module: web-console +license_name: BSD-3-Clause License +copyright: Baidu Inc. +version: 5.4.3 +license_file_path: licenses/bin/zrender.BSD3 + +--- + name: "zustand" license_category: binary module: web-console diff --git a/licenses/bin/use-resize-observer.MIT b/licenses/bin/use-resize-observer.MIT new file mode 100644 index 0000000000000..24db295ce8f96 --- /dev/null +++ b/licenses/bin/use-resize-observer.MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2018 Viktor Hubert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/licenses/bin/zrender.BSD3 b/licenses/bin/zrender.BSD3 new file mode 100644 index 0000000000000..66348cbf83e16 --- /dev/null +++ b/licenses/bin/zrender.BSD3 @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Baidu Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/web-console/package-lock.json b/web-console/package-lock.json index c0e7fba1344e4..aab6715f24785 100644 --- a/web-console/package-lock.json +++ b/web-console/package-lock.json @@ -15,6 +15,8 @@ "@blueprintjs/icons": "^4.16.0", "@blueprintjs/popover2": "^1.14.9", "@druid-toolkit/query": "^0.20.5", + "@druid-toolkit/visuals-core": "^0.3.3", + "@druid-toolkit/visuals-react": "^0.3.3", "ace-builds": "~1.4.14", "axios": "^0.26.1", "classnames": "^2.2.6", @@ -24,6 +26,7 @@ "d3-axis": "^2.1.0", "d3-scale": "^3.3.0", "d3-selection": "^2.0.0", + "echarts": "^5.4.1", "file-saver": "^2.0.2", "follow-redirects": "^1.14.7", "fontsource-open-sans": "^3.0.9", @@ -2586,6 +2589,30 @@ "tslib": "^2.5.2" } }, + "node_modules/@druid-toolkit/visuals-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@druid-toolkit/visuals-core/-/visuals-core-0.3.3.tgz", + "integrity": "sha512-Oze2M6LBxNIstFQTI68qayZs6vchtRiTAtIvuyvvalh3RGUqblJ91stMvh+9FtnHUBkr6J7J2C30L3VpDd0LTQ==", + "dependencies": { + "@druid-toolkit/query": "*", + "json-bigint-native": "^1.2.0", + "zustand": "^4.3.2" + } + }, + "node_modules/@druid-toolkit/visuals-react": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@druid-toolkit/visuals-react/-/visuals-react-0.3.3.tgz", + "integrity": "sha512-1WKTA7y2bd2LWA1as9bdAk7tPHkKWkgtcH6P7yZZDzooi1wVhgLWhREpvJFHsyIsau2ZHMYDiZkiDESrc90lIA==", + "dependencies": { + "@druid-toolkit/query": "*", + "@druid-toolkit/visuals-core": "*", + "use-resize-observer": "^9.1.0", + "zustand": "^4.3.2" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, "node_modules/@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -8129,6 +8156,20 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, + "node_modules/echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.4.3" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -23217,6 +23258,18 @@ "node": ">=0.10.0" } }, + "node_modules/use-resize-observer": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", + "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", + "dependencies": { + "@juggle/resize-observer": "^3.3.1" + }, + "peerDependencies": { + "react": "16.8.0 - 18", + "react-dom": "16.8.0 - 18" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -24400,6 +24453,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/zustand": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.2.tgz", @@ -26633,6 +26699,27 @@ "tslib": "^2.5.2" } }, + "@druid-toolkit/visuals-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@druid-toolkit/visuals-core/-/visuals-core-0.3.3.tgz", + "integrity": "sha512-Oze2M6LBxNIstFQTI68qayZs6vchtRiTAtIvuyvvalh3RGUqblJ91stMvh+9FtnHUBkr6J7J2C30L3VpDd0LTQ==", + "requires": { + "@druid-toolkit/query": "*", + "json-bigint-native": "^1.2.0", + "zustand": "^4.3.2" + } + }, + "@druid-toolkit/visuals-react": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@druid-toolkit/visuals-react/-/visuals-react-0.3.3.tgz", + "integrity": "sha512-1WKTA7y2bd2LWA1as9bdAk7tPHkKWkgtcH6P7yZZDzooi1wVhgLWhREpvJFHsyIsau2ZHMYDiZkiDESrc90lIA==", + "requires": { + "@druid-toolkit/query": "*", + "@druid-toolkit/visuals-core": "*", + "use-resize-observer": "^9.1.0", + "zustand": "^4.3.2" + } + }, "@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -31038,6 +31125,22 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, + "echarts": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz", + "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==", + "requires": { + "tslib": "2.3.0", + "zrender": "5.4.3" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -42509,6 +42612,14 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-resize-observer": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", + "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", + "requires": { + "@juggle/resize-observer": "^3.3.1" + } + }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -43417,6 +43528,21 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zrender": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", + "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==", + "requires": { + "tslib": "2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "zustand": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.2.tgz", diff --git a/web-console/package.json b/web-console/package.json index 788cd2b6a50ff..3c39ae1a44d45 100644 --- a/web-console/package.json +++ b/web-console/package.json @@ -69,6 +69,8 @@ "@blueprintjs/icons": "^4.16.0", "@blueprintjs/popover2": "^1.14.9", "@druid-toolkit/query": "^0.20.5", + "@druid-toolkit/visuals-core": "^0.3.3", + "@druid-toolkit/visuals-react": "^0.3.3", "ace-builds": "~1.4.14", "axios": "^0.26.1", "classnames": "^2.2.6", @@ -78,6 +80,7 @@ "d3-axis": "^2.1.0", "d3-scale": "^3.3.0", "d3-selection": "^2.0.0", + "echarts": "^5.4.1", "file-saver": "^2.0.2", "follow-redirects": "^1.14.7", "fontsource-open-sans": "^3.0.9", diff --git a/web-console/script/licenses b/web-console/script/licenses index 35aa2f5b58860..1dc820b94c1b2 100755 --- a/web-console/script/licenses +++ b/web-console/script/licenses @@ -187,6 +187,7 @@ checker.init( if (name === 'asap') publisher = 'Contributors'; if (name === 'diff-match-patch') publisher = 'Google'; if (name === 'esutils') publisher = 'Yusuke Suzuki'; // https://github.com/estools/esutils#license + if (name === 'echarts') publisher = 'Apache Software Foundation'; } if (!publisher) { diff --git a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap index 19cc197452f04..54414ffb80d41 100644 --- a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap +++ b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap @@ -158,6 +158,19 @@ exports[`HeaderBar matches snapshot 1`] = ` shouldDismissPopover={true} text="Lookups" /> + + } defaultIsOpen={false} diff --git a/web-console/src/components/header-bar/header-bar.tsx b/web-console/src/components/header-bar/header-bar.tsx index 3e18a1d548d31..d3c58db6bb556 100644 --- a/web-console/src/components/header-bar/header-bar.tsx +++ b/web-console/src/components/header-bar/header-bar.tsx @@ -70,6 +70,7 @@ export type HeaderActiveTab = | 'services' | 'workbench' | 'sql-data-loader' + | 'explore' | 'lookups'; const DruidLogo = React.memo(function DruidLogo() { @@ -286,6 +287,15 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) { disabled={!capabilities.hasCoordinatorAccess()} selected={active === 'lookups'} /> + + ); diff --git a/web-console/src/console-application.scss b/web-console/src/console-application.scss index 7f6107e86b9bd..3c527b3e1291a 100644 --- a/web-console/src/console-application.scss +++ b/web-console/src/console-application.scss @@ -36,6 +36,10 @@ padding: 10px; } + &.thinner { + padding: 5px; + } + .app-view { position: relative; } diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx index 807a71225f7d7..8955b1c9051cb 100644 --- a/web-console/src/console-application.tsx +++ b/web-console/src/console-application.tsx @@ -19,6 +19,7 @@ import { HotkeysProvider, Intent } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; +import type { JSX } from 'react'; import React from 'react'; import type { RouteComponentProps } from 'react-router'; import { Redirect } from 'react-router'; @@ -34,6 +35,7 @@ import { AppToaster } from './singletons'; import { compact, localStorageGetJson, LocalStorageKeys, QueryManager } from './utils'; import { DatasourcesView, + ExploreView, HomeView, LoadDataView, LookupsView, @@ -220,7 +222,7 @@ export class ConsoleApplication extends React.PureComponent< private readonly wrapInViewContainer = ( active: HeaderActiveTab, el: JSX.Element, - classType: 'normal' | 'narrow-pad' | 'thin' = 'normal', + classType: 'normal' | 'narrow-pad' | 'thin' | 'thinner' = 'normal', ) => { const { capabilities } = this.state; @@ -414,6 +416,10 @@ export class ConsoleApplication extends React.PureComponent< ); }; + private readonly wrappedExploreView = () => { + return this.wrapInViewContainer('explore', , 'thinner'); + }; + render() { const { capabilities, capabilitiesLoading } = this.state; @@ -470,6 +476,11 @@ export class ConsoleApplication extends React.PureComponent< {capabilities.hasCoordinatorAccess() && ( )} + + {capabilities.hasSql() && ( + + )} + diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx index e2b94bffbd709..7c797d2c89adf 100644 --- a/web-console/src/utils/local-storage-keys.tsx +++ b/web-console/src/utils/local-storage-keys.tsx @@ -55,6 +55,9 @@ export const LocalStorageKeys = { WORKBENCH_TASK_PANEL: 'workbench-task-panel' as const, SQL_DATA_LOADER_CONTENT: 'sql-data-loader-content' as const, + + EXPLORE_CONTENT: 'explore-content' as const, + EXPLORE_ESSENCE: 'explore-essence' as const, }; export type LocalStorageKeys = (typeof LocalStorageKeys)[keyof typeof LocalStorageKeys]; diff --git a/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.scss b/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.scss new file mode 100644 index 0000000000000..f40663b76b200 --- /dev/null +++ b/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.scss @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.column-picker-menu { + .search-input { + margin: 4px; + } + + .column-menu { + height: 400px; + overflow: auto; + } +} diff --git a/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.tsx b/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.tsx new file mode 100644 index 0000000000000..16d3cbee9b138 --- /dev/null +++ b/web-console/src/views/explore-view/column-picker-menu/column-picker-menu.tsx @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IconName } from '@blueprintjs/core'; +import { Icon, InputGroup, Menu, MenuItem } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import type { ExpressionMeta } from '@druid-toolkit/visuals-core'; +import classNames from 'classnames'; +import React, { useState } from 'react'; + +import { caseInsensitiveContains, dataTypeToIcon, filterMap } from '../../../utils'; + +import './column-picker-menu.scss'; + +export interface ColumnPickerMenuProps { + className?: string; + columns: ExpressionMeta[]; + onSelectColumn(column: ExpressionMeta): void; + iconForColumn?: (column: ExpressionMeta) => IconName | undefined; + onSelectNone?: () => void; + shouldDismissPopover?: boolean; +} + +export const ColumnPickerMenu = function ColumnPickerMenu(props: ColumnPickerMenuProps) { + const { className, columns, onSelectColumn, iconForColumn, onSelectNone, shouldDismissPopover } = + props; + const [columnSearch, setColumnSearch] = useState(''); + + return ( +
+ setColumnSearch(e.target.value)} + placeholder="Search..." + autoFocus + /> + + {onSelectNone && ( + + )} + {filterMap(columns, (c, i) => { + if (!caseInsensitiveContains(c.name, columnSearch)) return; + const iconName = iconForColumn?.(c); + return ( + } + onClick={() => onSelectColumn(c)} + shouldDismissPopover={shouldDismissPopover} + /> + ); + })} + +
+ ); +}; diff --git a/web-console/src/views/explore-view/column-picker/column-picker.tsx b/web-console/src/views/explore-view/column-picker/column-picker.tsx new file mode 100644 index 0000000000000..237e84b99e4ea --- /dev/null +++ b/web-console/src/views/explore-view/column-picker/column-picker.tsx @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HTMLSelect } from '@blueprintjs/core'; +import type { ExpressionMeta } from '@druid-toolkit/visuals-core'; +import React from 'react'; + +export interface ColumnPickerProps { + availableColumns: ExpressionMeta[] | undefined; + selectedColumnName: string; + onSelectedColumnNameChange(selectedColumnName: string): void; +} + +export const ColumnPicker = React.memo(function ColumnPicker(props: ColumnPickerProps) { + const { availableColumns, selectedColumnName, onSelectedColumnNameChange } = props; + + return ( + { + onSelectedColumnNameChange(e.target.value); + }} + > + {availableColumns?.map((column, i) => ( + + )) || } + + ); +}); diff --git a/web-console/src/views/explore-view/control-pane/aggregate-menu.scss b/web-console/src/views/explore-view/control-pane/aggregate-menu.scss new file mode 100644 index 0000000000000..754bd23858bcc --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/aggregate-menu.scss @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.aggregate-menu { + .search-input { + margin: 4px; + } + + .inner-menu { + height: 400px; + overflow: auto; + } +} diff --git a/web-console/src/views/explore-view/control-pane/aggregate-menu.tsx b/web-console/src/views/explore-view/control-pane/aggregate-menu.tsx new file mode 100644 index 0000000000000..a91ac6374b760 --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/aggregate-menu.tsx @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputGroup, Menu, MenuItem } from '@blueprintjs/core'; +import { SqlFunction } from '@druid-toolkit/query'; +import type { ExpressionMeta } from '@druid-toolkit/visuals-core'; +import React, { useState } from 'react'; + +import { caseInsensitiveContains } from '../../../utils'; + +import { getPossibleAggregateForColumn } from './helpers'; + +import './aggregate-menu.scss'; + +const COUNT_AGG: ExpressionMeta = { + name: 'Count', + expression: SqlFunction.count(), + sqlType: 'BIGINT', +}; + +export interface AggregateMenuProps { + columns: ExpressionMeta[]; + onSelectAggregate(aggregate: ExpressionMeta): void; + onSelectNone?: () => void; + shouldDismissPopover?: boolean; +} + +export const AggregateMenu = function AggregateMenu(props: AggregateMenuProps) { + const { columns, onSelectAggregate, onSelectNone, shouldDismissPopover } = props; + const [columnSearch, setColumnSearch] = useState(''); + + return ( +
+ setColumnSearch(e.target.value)} + placeholder="Search..." + autoFocus + /> + + {onSelectNone && ( + + )} + onSelectAggregate(COUNT_AGG)} /> + {columns.map((c, i) => { + if (!caseInsensitiveContains(c.name, columnSearch)) return; + const possibleAggregateForColumn = getPossibleAggregateForColumn(c); + if (!possibleAggregateForColumn.length) return; + if (possibleAggregateForColumn.length === 1) { + const a = possibleAggregateForColumn[0]; + return onSelectAggregate(a)} />; + } else { + return ( + + {possibleAggregateForColumn.map((a, j) => ( + onSelectAggregate(a)} /> + ))} + + ); + } + })} + +
+ ); +}; diff --git a/web-console/src/views/explore-view/control-pane/aggregates-input.tsx b/web-console/src/views/explore-view/control-pane/aggregates-input.tsx new file mode 100644 index 0000000000000..3562cef89a934 --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/aggregates-input.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Classes, Position, Tag } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Popover2 } from '@blueprintjs/popover2'; +import type { ExpressionMeta } from '@druid-toolkit/visuals-core'; +import classNames from 'classnames'; +import React from 'react'; + +import { AggregateMenu } from './aggregate-menu'; + +export interface AggregatesInputProps { + columns: ExpressionMeta[]; + value: ExpressionMeta[]; + onValueChange(value: ExpressionMeta[]): void; + allowDuplicates?: boolean; +} + +export const AggregatesInput = function AggregatesInput(props: AggregatesInputProps) { + const { columns, value, onValueChange, allowDuplicates } = props; + + const availableColumn = allowDuplicates + ? columns + : columns.filter(o => !value.find(_ => _.name === o.name)); + + return ( +
+
+ {value.map((c, i) => ( + { + onValueChange(value.filter(v => v !== c)); + }} + > + {c.name} + + ))} + onValueChange(value.concat(c))} + /> + } + > + + +
+
+ ); +}; diff --git a/web-console/src/views/explore-view/control-pane/columns-input.scss b/web-console/src/views/explore-view/control-pane/columns-input.scss new file mode 100644 index 0000000000000..b78fb7d193427 --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/columns-input.scss @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.columns-input { + .bp4-tag-input-values .bp4-tag { + &.drop-before::after { + content: ''; + height: 24px; + width: 1px; + position: absolute; + left: -3px; + top: 50%; + transform: translate(0, -50%); + background-color: white; + } + + &.drop-after::after { + content: ''; + height: 24px; + width: 1px; + position: absolute; + right: -3px; + top: 50%; + transform: translate(0, -50%); + background-color: white; + } + } +} diff --git a/web-console/src/views/explore-view/control-pane/columns-input.tsx b/web-console/src/views/explore-view/control-pane/columns-input.tsx new file mode 100644 index 0000000000000..eff1d84f2f12b --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/columns-input.tsx @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Classes, Position, Tag } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Popover2 } from '@blueprintjs/popover2'; +import type { ExpressionMeta } from '@druid-toolkit/visuals-core'; +import classNames from 'classnames'; +import type { JSX } from 'react'; +import React, { useCallback, useState } from 'react'; + +import { ColumnPickerMenu } from '../column-picker-menu/column-picker-menu'; + +import './columns-input.scss'; + +export interface ColumnsInputProps { + columns: ExpressionMeta[]; + value: ExpressionMeta[]; + onValueChange(value: ExpressionMeta[]): void; + allowDuplicates?: boolean; + allowReordering?: boolean; + + /** + * If you want to take control of the way new columns are picked and added + */ + pickerMenu?: (columns: ExpressionMeta[]) => JSX.Element; +} + +function moveInArray(arr: any[], fromIndex: number, toIndex: number) { + arr = arr.concat(); + const element = arr[fromIndex]; + arr.splice(fromIndex, 1); + arr.splice(toIndex, 0, element); + + return arr; +} + +export const ColumnsInput = function ColumnsInput(props: ColumnsInputProps) { + const { columns, value, onValueChange, allowDuplicates, allowReordering, pickerMenu } = props; + + const availableColumns = allowDuplicates + ? columns + : columns.filter(o => !value.find(_ => _.name === o.name)); + + const [dragIndex, setDragIndex] = useState(-1); + const [dropBefore, setDropBefore] = useState(false); + const [dropIndex, setDropIndex] = useState(-1); + + const startDrag = useCallback((e: React.DragEvent, i: number) => { + e.dataTransfer.effectAllowed = 'move'; + setDragIndex(i); + }, []); + + const onDragOver = useCallback( + (e: React.DragEvent, i: number) => { + const targetRect = e.currentTarget.getBoundingClientRect(); + const before = e.clientX - targetRect.left <= targetRect.width / 2; + setDropBefore(before); + + e.preventDefault(); + + if (i === dropIndex) return; + + setDropIndex(i); + }, + [dropIndex], + ); + + const onDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (dropIndex > -1) { + let correctedDropIndex = dropIndex + (dropBefore ? 0 : 1); + if (correctedDropIndex > dragIndex) correctedDropIndex--; + + if (correctedDropIndex !== dragIndex) { + onValueChange(moveInArray(value, dragIndex, correctedDropIndex)); + } + } + setDragIndex(-1); + setDropIndex(-1); + setDropBefore(false); + }, + [dropIndex, dragIndex, onValueChange, value, dropBefore], + ); + + return ( +
+
+ {value.map((c, i) => ( + onDragOver(e, i)} + key={i} + onDragStart={e => startDrag(e, i)} + onRemove={() => { + onValueChange(value.filter(v => v !== c)); + }} + > + {c.name} + + ))} + onValueChange(value.concat(c))} + /> + ) + } + > + + +
+
+ ); +}; diff --git a/web-console/src/views/explore-view/control-pane/control-pane.scss b/web-console/src/views/explore-view/control-pane/control-pane.scss new file mode 100644 index 0000000000000..8e029b4139858 --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/control-pane.scss @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.control-pane { + .bp4-tag-input-values .bp4-tag { + vertical-align: top; + } +} diff --git a/web-console/src/views/explore-view/control-pane/control-pane.tsx b/web-console/src/views/explore-view/control-pane/control-pane.tsx new file mode 100644 index 0000000000000..736d39564c881 --- /dev/null +++ b/web-console/src/views/explore-view/control-pane/control-pane.tsx @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + ButtonGroup, + InputGroup, + Intent, + Menu, + MenuItem, + NumericInput, +} from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Popover2 } from '@blueprintjs/popover2'; +import type { + ExpressionMeta, + OptionValue, + ParameterDefinition, + RegisteredVisualModule, +} from '@druid-toolkit/visuals-core'; +import { getPluginOptionLabel } from '@druid-toolkit/visuals-core'; +import type { JSX } from 'react'; +import React from 'react'; + +import { AutoForm, FormGroupWithInfo, PopoverText } from '../../../components'; +import { AppToaster } from '../../../singletons'; +import { ColumnPickerMenu } from '../column-picker-menu/column-picker-menu'; +import { DroppableContainer } from '../droppable-container/droppable-container'; + +import { AggregateMenu } from './aggregate-menu'; +import { ColumnsInput } from './columns-input'; +import { getPossibleAggregateForColumn } from './helpers'; +import { OptionsInput } from './options-input'; + +import './control-pane.scss'; + +export interface ControlPaneProps { + columns: ExpressionMeta[]; + onUpdateParameterValues(params: Record): void; + parameterValues: Record; + visualModule: RegisteredVisualModule; +} + +export const ControlPane = function ControlPane(props: ControlPaneProps) { + const { columns, onUpdateParameterValues, parameterValues, visualModule } = props; + + function renderOptionsPropInput( + parameter: ParameterDefinition, + value: any, + onValueChange: (value: any) => void, + ): { + element: JSX.Element; + onDropColumn?: (column: ExpressionMeta) => void; + } { + switch (parameter.type) { + case 'boolean': { + return { + element: ( + + + + + ), + }; + } + + case 'number': + return { + element: ( + onValueChange(v)} + placeholder={parameter.control?.placeholder} + fill + min={parameter.min} + max={parameter.max} + /> + ), + }; + + case 'string': + return { + element: ( + onValueChange(e.target.value)} + placeholder={parameter.control?.placeholder} + fill + /> + ), + }; + + case 'option': { + const controlOptions = parameter.options || []; + const selectedOption: OptionValue | undefined = controlOptions.find(o => o === value); + return { + element: ( + + {controlOptions.map((o, i) => ( + onValueChange(o)} + /> + ))} + + } + > + } + /> + + ), + }; + } + + case 'options': { + return { + element: ( + + ), + }; + } + + case 'column': + return { + element: ( + onValueChange(undefined) + } + onSelectColumn={onValueChange} + /> + } + > + } + /> + + ), + onDropColumn: onValueChange, + }; + + case 'columns': { + return { + element: ( + + ), + onDropColumn: (column: ExpressionMeta) => { + value = value || []; + const columnName = column.name; + if ( + !parameter.allowDuplicates && + value.find((v: ExpressionMeta) => v.name === columnName) + ) { + AppToaster.show({ + intent: Intent.WARNING, + message: `"${columnName}" already selected`, + }); + return; + } + onValueChange(value.concat(column)); + }, + }; + } + + case 'aggregate': { + return { + element: ( + onValueChange(undefined) + } + /> + } + > + } + /> + + ), + onDropColumn: column => { + const aggregates = getPossibleAggregateForColumn(column); + if (!aggregates.length) return; + onValueChange(aggregates[0]); + }, + }; + } + + case 'aggregates': { + return { + element: ( + ( + onValueChange((value as ExpressionMeta[]).concat(c))} + /> + )} + /> + ), + onDropColumn: column => { + value = value || []; + const aggregates = getPossibleAggregateForColumn(column).filter( + p => !value.some((v: ExpressionMeta) => v.name === p.name), + ); + if (!aggregates.length) return; + onValueChange(value.concat(aggregates[0])); + }, + }; + } + + default: + return { + element: ( +