From eaf43c24db100f7df3a2b1f031ea01bc8f183899 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Wed, 27 Nov 2019 19:33:59 +0200 Subject: [PATCH 1/5] Migrate AddToDashboard dialog to React --- .../queries/AddToDashboardDialog.jsx | 100 ++++++++++++++++++ .../queries/add-to-dashboard-dialog.less | 34 ++++++ client/app/lib/hooks/useSearchResults.js | 33 ++++++ .../app/pages/queries/add-to-dashboard.html | 19 ---- client/app/pages/queries/add-to-dashboard.js | 56 ---------- client/app/pages/queries/view.js | 11 +- 6 files changed, 170 insertions(+), 83 deletions(-) create mode 100644 client/app/components/queries/AddToDashboardDialog.jsx create mode 100644 client/app/components/queries/add-to-dashboard-dialog.less create mode 100644 client/app/lib/hooks/useSearchResults.js delete mode 100644 client/app/pages/queries/add-to-dashboard.html delete mode 100644 client/app/pages/queries/add-to-dashboard.js diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx new file mode 100644 index 0000000000..ad86da03a9 --- /dev/null +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -0,0 +1,100 @@ +import { isString } from 'lodash'; +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'antd/lib/modal'; +import Input from 'antd/lib/input'; +import List from 'antd/lib/list'; +import Icon from 'antd/lib/icon'; +import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; +import { QueryTagsControl } from '@/components/tags-control/TagsControl'; +import { Dashboard } from '@/services/dashboard'; +import notification from '@/services/notification'; +import useSearchResults from '@/lib/hooks/useSearchResults'; + +import './add-to-dashboard-dialog.less'; + +function AddToDashboardDialog({ dialog, visualization }) { + const [searchTerm, setSearchTerm] = useState(''); + + const [doSearch, dashboards] = useSearchResults((term) => { + if (isString(term) && (term.length >= 3)) { + return Dashboard.get({ q: term }).$promise.then(results => results.results); + } + return Promise.resolve([]); + }, { initialResults: [] }); + + const [selectedDashboard, setSelectedDashboard] = useState(null); + + const [saveInProgress, setSaveInProgress] = useState(false); + + useEffect(() => { doSearch(searchTerm); }, [doSearch, searchTerm]); + + function addWidgetToDashboard() { + // Load dashboard with all widgets + Dashboard.get({ slug: selectedDashboard.slug }).$promise + .then(dashboard => dashboard.addWidget(visualization)) + .then(() => { + dialog.close(); + notification.success('Widget added to dashboard.'); + }) + .catch(() => { notification.error('Widget not added.'); }) + .finally(() => { setSaveInProgress(false); }); + } + + const items = selectedDashboard ? [selectedDashboard] : dashboards; + + return ( + + + + {!selectedDashboard && ( + setSearchTerm(event.target.value)} + suffix={( + setSearchTerm('')} /> + )} + /> + )} + + {(items.length > 0) && ( + ( + setSelectedDashboard(null)} />] : []} + onClick={selectedDashboard ? null : () => setSelectedDashboard(d)} + > +
+ {d.name} + +
+
+ )} + /> + )} +
+ ); +} + +AddToDashboardDialog.propTypes = { + dialog: DialogPropType.isRequired, + visualization: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types +}; + +export default wrapDialog(AddToDashboardDialog); diff --git a/client/app/components/queries/add-to-dashboard-dialog.less b/client/app/components/queries/add-to-dashboard-dialog.less new file mode 100644 index 0000000000..85a948a0e5 --- /dev/null +++ b/client/app/components/queries/add-to-dashboard-dialog.less @@ -0,0 +1,34 @@ +@import (reference, less) '~@/assets/less/main.less'; + +.ant-list { + &.add-to-dashboard-dialog-search-results { + max-height: 300px; + overflow: auto; + margin-top: 15px; + + .ant-list-item { + padding: 12px; + cursor: pointer; + + &:hover, &:active { + @table-row-hover-bg: fade(@redash-gray, 5%); + background-color: @table-row-hover-bg; + } + } + } + + &.add-to-dashboard-dialog-selection { + .ant-list-item { + padding: 12px; + + .add-to-dashboard-dialog-item-content { + flex: 1 1 auto; + } + + .ant-list-item-action li { + margin: 0; + padding: 0; + } + } + } +} diff --git a/client/app/lib/hooks/useSearchResults.js b/client/app/lib/hooks/useSearchResults.js new file mode 100644 index 0000000000..1252a2d714 --- /dev/null +++ b/client/app/lib/hooks/useSearchResults.js @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; + +export default function useSearchResults( + fetch, + { initialResults = null, debounceTimeout = 200 } = {}, +) { + const [result, setResult] = useState(initialResults); + const [isLoading, setIsLoading] = useState(false); + + let currentSearchTerm = null; + let isDestroyed = false; + + const [doSearch] = useDebouncedCallback((searchTerm) => { + setIsLoading(true); + currentSearchTerm = searchTerm; + fetch(searchTerm) + .catch(() => null) + .then((data) => { + if ((searchTerm === currentSearchTerm) && !isDestroyed) { + setResult(data); + setIsLoading(false); + } + }); + }, debounceTimeout); + + useEffect(() => ( + // ignore all requests after component destruction + () => { isDestroyed = true; } + ), []); + + return [doSearch, result, isLoading]; +} diff --git a/client/app/pages/queries/add-to-dashboard.html b/client/app/pages/queries/add-to-dashboard.html deleted file mode 100644 index b557598a4c..0000000000 --- a/client/app/pages/queries/add-to-dashboard.html +++ /dev/null @@ -1,19 +0,0 @@ - - \ No newline at end of file diff --git a/client/app/pages/queries/add-to-dashboard.js b/client/app/pages/queries/add-to-dashboard.js deleted file mode 100644 index 2eaf6f924c..0000000000 --- a/client/app/pages/queries/add-to-dashboard.js +++ /dev/null @@ -1,56 +0,0 @@ -import template from './add-to-dashboard.html'; -import notification from '@/services/notification'; - -const AddToDashboardForm = { - controller($sce, Dashboard) { - 'ngInject'; - - this.vis = this.resolve.vis; - this.saveInProgress = false; - this.trustAsHtml = html => $sce.trustAsHtml(html); - this.onDashboardSelected = ({ slug }) => { - this.saveInProgress = true; - this.selected_query = this.resolve.query.id; - // Load dashboard with all widgets - Dashboard.get({ slug }).$promise - .then(dashboard => dashboard.addWidget(this.vis)) - .then(() => { - this.close(); - notification.success('Widget added to dashboard.'); - }) - .catch(() => { - notification.error('Widget not added.'); - }) - .finally(() => { - this.saveInProgress = false; - }); - }; - this.selectedDashboard = null; - this.searchDashboards = (searchTerm) => { - // , limitToUsersDashboards - if (!searchTerm || searchTerm.length < 3) { - return; - } - Dashboard.get( - { - search_term: searchTerm, - }, - (results) => { - this.dashboards = results.results; - }, - ); - }; - }, - bindings: { - resolve: '<', - close: '&', - dismiss: '&', - vis: '<', - }, - template, -}; -export default function init(ngModule) { - ngModule.component('addToDashboardDialog', AddToDashboardForm); -} - -init.init = true; diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index 304e32e752..0b3e57a0ec 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -8,6 +8,7 @@ import ScheduleDialog from '@/components/queries/ScheduleDialog'; import { newVisualization } from '@/visualizations'; import EditVisualizationDialog from '@/visualizations/EditVisualizationDialog'; import EmbedQueryDialog from '@/components/queries/EmbedQueryDialog'; +import AddToDashboardDialog from '@/components/queries/AddToDashboardDialog'; import PermissionsEditorDialog from '@/components/permissions-editor/PermissionsEditorDialog'; import notification from '@/services/notification'; import template from './query.html'; @@ -499,14 +500,8 @@ function QueryViewCtrl( }; $scope.openAddToDashboardForm = (visId) => { - const visualization = getVisualization(visId); - $uibModal.open({ - component: 'addToDashboardDialog', - size: 'sm', - resolve: { - query: $scope.query, - vis: visualization, - }, + AddToDashboardDialog.showModal({ + visualization: getVisualization(visId), }); }; From c9a427f5c93915a75a7bfec283e565536cf2c093 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Wed, 27 Nov 2019 19:56:31 +0200 Subject: [PATCH 2/5] AddToDashboard dialog: add dashboard link to notification --- .../components/queries/AddToDashboardDialog.jsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx index ad86da03a9..436c630062 100644 --- a/client/app/components/queries/AddToDashboardDialog.jsx +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -32,10 +32,19 @@ function AddToDashboardDialog({ dialog, visualization }) { function addWidgetToDashboard() { // Load dashboard with all widgets Dashboard.get({ slug: selectedDashboard.slug }).$promise - .then(dashboard => dashboard.addWidget(visualization)) - .then(() => { + .then((dashboard) => { + dashboard.addWidget(visualization); + return dashboard; + }) + .then((dashboard) => { dialog.close(); - notification.success('Widget added to dashboard.'); + const key = `notification-${Math.random().toString(36).substr(2, 10)}`; + notification.success('Widget added to dashboard', ( + + notification.close(key)}>{dashboard.name} + + + ), { key }); }) .catch(() => { notification.error('Widget not added.'); }) .finally(() => { setSaveInProgress(false); }); From fe636fc36a48a446c7313c6c7bd1824456faf8c1 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Wed, 27 Nov 2019 19:59:52 +0200 Subject: [PATCH 3/5] Fix error messages when creating new dashboard --- client/app/components/app-header/AppHeader.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/app-header/AppHeader.jsx b/client/app/components/app-header/AppHeader.jsx index 69a4f68045..44009d0def 100644 --- a/client/app/components/app-header/AppHeader.jsx +++ b/client/app/components/app-header/AppHeader.jsx @@ -62,7 +62,7 @@ function DesktopNavbar() { )} {currentUser.hasPermission('create_dashboard') && ( - New Dashboard + CreateDashboardDialog.showModal()}>New Dashboard )} From 08ab2ba608c3d657185c5e66bd90d9129e794cd9 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Fri, 29 Nov 2019 11:15:19 +0200 Subject: [PATCH 4/5] Show spinner when loading search results --- client/app/components/queries/AddToDashboardDialog.jsx | 5 +++-- client/app/components/queries/add-to-dashboard-dialog.less | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx index 436c630062..8d5ab6c55b 100644 --- a/client/app/components/queries/AddToDashboardDialog.jsx +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -16,7 +16,7 @@ import './add-to-dashboard-dialog.less'; function AddToDashboardDialog({ dialog, visualization }) { const [searchTerm, setSearchTerm] = useState(''); - const [doSearch, dashboards] = useSearchResults((term) => { + const [doSearch, dashboards, isLoading] = useSearchResults((term) => { if (isString(term) && (term.length >= 3)) { return Dashboard.get({ q: term }).$promise.then(results => results.results); } @@ -77,11 +77,12 @@ function AddToDashboardDialog({ dialog, visualization }) { /> )} - {(items.length > 0) && ( + {((items.length > 0) || isLoading) && ( ( Date: Wed, 4 Dec 2019 16:46:26 +0200 Subject: [PATCH 5/5] Remove search term limit --- client/app/components/queries/AddToDashboardDialog.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx index 8d5ab6c55b..bf5dec921a 100644 --- a/client/app/components/queries/AddToDashboardDialog.jsx +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -17,7 +17,7 @@ function AddToDashboardDialog({ dialog, visualization }) { const [searchTerm, setSearchTerm] = useState(''); const [doSearch, dashboards, isLoading] = useSearchResults((term) => { - if (isString(term) && (term.length >= 3)) { + if (isString(term) && (term !== '')) { return Dashboard.get({ q: term }).$promise.then(results => results.results); } return Promise.resolve([]);