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),
});
};