mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 01:25:16 +00:00
Migrate AddToDashboard dialog to React (#4408)
This commit is contained in:
parent
94bd03dc42
commit
ba36b4e671
@ -62,7 +62,7 @@ function DesktopNavbar() {
|
||||
)}
|
||||
{currentUser.hasPermission('create_dashboard') && (
|
||||
<Menu.Item key="new-dashboard">
|
||||
<a onMouseUp={CreateDashboardDialog.showModal}>New Dashboard</a>
|
||||
<a onMouseUp={() => CreateDashboardDialog.showModal()}>New Dashboard</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="new-alert">
|
||||
|
110
client/app/components/queries/AddToDashboardDialog.jsx
Normal file
110
client/app/components/queries/AddToDashboardDialog.jsx
Normal file
@ -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', (
|
||||
<React.Fragment>
|
||||
<a href={`dashboard/${dashboard.slug}`} onClick={() => notification.close(key)}>{dashboard.name}</a>
|
||||
<QueryTagsControl isDraft={dashboard.is_draft} tags={dashboard.tags} />
|
||||
</React.Fragment>
|
||||
), { key });
|
||||
})
|
||||
.catch(() => { notification.error('Widget not added.'); })
|
||||
.finally(() => { setSaveInProgress(false); });
|
||||
}
|
||||
|
||||
const items = selectedDashboard ? [selectedDashboard] : dashboards;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...dialog.props}
|
||||
title="Add to Dashboard"
|
||||
okButtonProps={{ disabled: !selectedDashboard || saveInProgress, loading: saveInProgress }}
|
||||
cancelButtonProps={{ disabled: saveInProgress }}
|
||||
onOk={addWidgetToDashboard}
|
||||
>
|
||||
<label htmlFor="add-to-dashboard-dialog-dashboard">Choose the dashboard to add this query to:</label>
|
||||
|
||||
{!selectedDashboard && (
|
||||
<Input
|
||||
id="add-to-dashboard-dialog-dashboard"
|
||||
className="w-100"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
placeholder="Search a dashboard by name"
|
||||
value={searchTerm}
|
||||
onChange={event => setSearchTerm(event.target.value)}
|
||||
suffix={(
|
||||
<Icon type="close" className={(searchTerm === '') ? 'hidden' : null} onClick={() => setSearchTerm('')} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{((items.length > 0) || isLoading) && (
|
||||
<List
|
||||
className={selectedDashboard ? 'add-to-dashboard-dialog-selection' : 'add-to-dashboard-dialog-search-results'}
|
||||
bordered
|
||||
itemLayout="horizontal"
|
||||
loading={isLoading}
|
||||
dataSource={items}
|
||||
renderItem={d => (
|
||||
<List.Item
|
||||
key={`dashboard-${d.id}`}
|
||||
actions={selectedDashboard ? [<Icon type="close" onClick={() => setSelectedDashboard(null)} />] : []}
|
||||
onClick={selectedDashboard ? null : () => setSelectedDashboard(d)}
|
||||
>
|
||||
<div className="add-to-dashboard-dialog-item-content">
|
||||
{d.name}
|
||||
<QueryTagsControl isDraft={d.is_draft} tags={d.tags} />
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddToDashboardDialog.propTypes = {
|
||||
dialog: DialogPropType.isRequired,
|
||||
visualization: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
export default wrapDialog(AddToDashboardDialog);
|
37
client/app/components/queries/add-to-dashboard-dialog.less
Normal file
37
client/app/components/queries/add-to-dashboard-dialog.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
client/app/lib/hooks/useSearchResults.js
Normal file
33
client/app/lib/hooks/useSearchResults.js
Normal file
@ -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];
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Add to Dashboard</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="alertForm" class="form">
|
||||
<div class="form-group">
|
||||
<label>Choose the dashboard to add this query to:</label>
|
||||
<ui-select ng-model="$ctrl.dashboardList" reset-search-input="false" on-select="$ctrl.onDashboardSelected($item)" ng-disabled="$ctrl.saveInProgress">
|
||||
<ui-select-match placeholder="Search a dashboard by name">{{$select.selected.name}}</ui-select-match>
|
||||
<ui-select-choices repeat="q in $ctrl.dashboards"
|
||||
refresh="$ctrl.searchDashboards($select.search, $ctrl.limitToUsersDashboards)"
|
||||
refresh-delay="0">
|
||||
<div ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -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;
|
@ -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),
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user