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 )} diff --git a/client/app/components/queries/AddToDashboardDialog.jsx b/client/app/components/queries/AddToDashboardDialog.jsx new file mode 100644 index 0000000000..bf5dec921a --- /dev/null +++ b/client/app/components/queries/AddToDashboardDialog.jsx @@ -0,0 +1,110 @@ +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, isLoading] = useSearchResults((term) => { + if (isString(term) && (term !== '')) { + 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); + return dashboard; + }) + .then((dashboard) => { + dialog.close(); + 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); }); + } + + const items = selectedDashboard ? [selectedDashboard] : dashboards; + + return ( + + + + {!selectedDashboard && ( + setSearchTerm(event.target.value)} + suffix={( + setSearchTerm('')} /> + )} + /> + )} + + {((items.length > 0) || isLoading) && ( + ( + 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..c8d5b50140 --- /dev/null +++ b/client/app/components/queries/add-to-dashboard-dialog.less @@ -0,0 +1,37 @@ +@import (reference, less) '~@/assets/less/main.less'; + +.ant-list { + &.add-to-dashboard-dialog-search-results { + margin-top: 15px; + + .ant-list-items { + max-height: 300px; + overflow: auto; + } + + .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), }); };